一 thread
當我們提及多線程的時候會想到thread和threadpool,這都是異步操作,threadpool其實就是thread的集合,具有很多優勢,不過在任務多的時候全局隊列會存在競爭而消耗資源。thread默認爲前臺線程,主程序必須等線程跑完纔會關閉,而threadpool相反。
總結:threadpool確實比thread性能優,但是兩者都沒有很好的api區控制,如果線程執行無響應就只能等待結束,從而誕生了task任務。
二 task
1 什麼是task
.NET 4.0推出了新一代的多線程模型Task,task簡單地看就是任務,那和thread有什麼區別呢?Task的背後的實現也是使用了線程池線程,但它的性能優於ThreadPoll,因爲它使用的不是線程池的全局隊列,而是使用的本地隊列,使線程之間的資源競爭減少。同時Task提供了豐富的API來管理線程、控制。但是相對前面的兩種耗內存,Task依賴於CPU,對於多核的CPU性能遠超前兩者,單核的CPU三者的性能沒什麼差別。
2 兩種創建task的模式
創建一個task任務有兩種模式:使用factory創建會直接執行,使用new創建不會執行,必須等到start啓動之後才執行。
class Program
{
static void DownLoad(object str)
{
Console.WriteLine("DownLoad Begin ID = " + Thread.CurrentThread.ManagedThreadId + " " + str);
Thread.Sleep(1000);
Console.WriteLine("DownLoad End");
}
static void Main(string[] args)
{
//創建任務
//Task task = new Task(DownLoad, "人民日報");
//啓動任務
//task.Start();
//創建任務工廠
TaskFactory taskFactory = new TaskFactory();
//開始新的任務
taskFactory.StartNew(DownLoad, "紐約時報");
Console.WriteLine("Main");
Console.ReadKey();
}
}
3 task的生命週期
var testTask = new Task(() =>
{
Console.WriteLine("task start");
System.Threading.Thread.Sleep(2000);
});
Console.WriteLine(testTask.Status);
testTask.Start();
Console.WriteLine(testTask.Status);
Console.WriteLine(testTask.Status);
testTask.Wait();
Console.WriteLine(testTask.Status);
Console.WriteLine(testTask.Status);
/*
輸出結果:
Created
task start
Running
Running
RanToCompletion
RanToCompletion
*/
可以看出task確實是異步執行,並且wait很好地控制了task。
4 task的控制
var testTask = new Task(() =>
{
Console.WriteLine("task start");
System.Threading.Thread.Sleep(2000);
});
testTask.Start();
testTask.Wait();
var testTask = new Task(() =>
{
Console.WriteLine("task start");
System.Threading.Thread.Sleep(2000);
});
testTask.Start();
var factoryTeak = Task.Factory.StartNew(() =>
{
Console.WriteLine("factory task start");
});
Task.WaitAll(testTask, factoryTeak);
Console.WriteLine("end");
var testTask = new Task(() =>
{
Console.WriteLine("task start");
System.Threading.Thread.Sleep(2000);
});
testTask.Start();
var factoryTeak = Task.Factory.StartNew(() =>
{
Console.WriteLine("factory task start");
});
Task.WaitAny(testTask, factoryTeak);
Console.WriteLine("end");
通過wait()對單個task進行等待,Task.waitall()對多個task進行等待,waitany()執行任意一個task就往下繼續執行。
5 連續任務
var testTask = new Task(() =>
{
Console.WriteLine("task start");
System.Threading.Thread.Sleep(2000);
});
testTask.Start();
var resultTest = testTask.ContinueWith<string>((Task) =>
{
Console.WriteLine("testTask end");
return "end";
});
Console.WriteLine(resultTest.Result);
6 task的取消
首先創建一個取消task的令牌的實例,在不啓動task直接取消:
var tokenSource = new CancellationTokenSource();//創建取消task實例
var testTask = new Task(() =>
{
for (int i = 0; i < 6; i++)
{
System.Threading.Thread.Sleep(1000);
}
},tokenSource.Token);
Console.WriteLine(testTask.Status);
tokenSource.Token.Register(()=>
{
Console.WriteLine("task is to cancel");
});
tokenSource.Cancel();
Console.WriteLine(testTask.Status);
//輸出結果:
/*
Created
task is to cancel
Canceled
*/
如果task啓動了真的取消了task?
var tokenSource = new CancellationTokenSource();//創建取消task實例
var testTask = new Task(() =>
{
for (int i = 0; i <6; i++) {
System.Threading.Thread.Sleep(1000);
}
},tokenSource.Token);
Console.WriteLine(testTask.Status);
testTask.Start();
Console.WriteLine(testTask.Status);
tokenSource.Token.Register(()=>
{
Console.WriteLine("task is to cancel");
});
tokenSource.Cancel();
Console.WriteLine(testTask.Status);
for (int i = 0; i < 10; i++)
{
System.Threading.Thread.Sleep(1000);
Console.WriteLine(testTask.Status);
}
/*
輸出結果:
Created
WaitingToRun
task is to cancel
Running
Running
Running
Running
Running
Running
RanToCompletion
RanToCompletion
RanToCompletion
RanToCompletion
RanToCompletion
*/
可以看出其實並沒有取消task,此時task還在繼續跑。
7 task的嵌套
在一個任務中可以啓動子任務,兩個任務異步執行。默認情況下,子任務(即由外部任務創建的內部任務)將獨立於其父任務執行。使用TaskCreationOptions.AttachedToParent
顯式指定將任務附加到任務層次結構中的某個父級。
var parentTask = new Task(()=>
{
var childTask = new Task(() =>
{
System.Threading.Thread.Sleep(2000);
Console.WriteLine("childTask to start");
});
childTask.Start();
Console.WriteLine("parentTask to start");
});
parentTask.Start();
parentTask.Wait();
Console.WriteLine("end");
此時爲普通關聯,父task和子task沒影響
var parentTask = new Task(()=>
{
var childTask = new Task(() =>{
System.Threading.Thread.Sleep(2000);
Console.WriteLine("childTask to start");
}, TaskCreationOptions.AttachedToParent);
childTask.Start();
Console.WriteLine("parentTask to start");
} );
parentTask.Start();
parentTask.Wait();
Console.WriteLine("end");
此時爲父task和子task關聯,wait會一直等待父子task執行完。
如果父任務執行完了但是子任務沒有執行完,則父任務的狀態會被設置爲WaitingForChildrenToComplete
,只有子任務也執行完了,父任務的狀態纔會變成RunToCompletion
。
8 任務執行的結果
使用Task的泛型版本,可以返回任務的執行結果。
下面例子中的TaskWithResult的輸入爲object類型,返回一個元組Tuple<int, int>。
定義調用TaskWithResult的任務時,使用泛型類Task<Tuple<int, int>>,泛型的參數定義了返回類型。通過構造函數,傳遞TaskWithResult,構造函數的第二個參數定義了TaskWithResult的輸入值。
任務完成後,通過Result屬性獲取任務的結果。
class Program
{
static Tuple<int, int> TaskWithResult(object obj)
{
Tuple<int, int> div = (Tuple<int, int>)obj;
Thread.Sleep(1000);
return Tuple.Create<int, int>(div.Item1 + div.Item2, div.Item1 - div.Item2);
}
static void Main(string[] args)
{
var task = new Task<Tuple<int, int>>(TaskWithResult, Tuple.Create<int, int>(8, 3));
task.Start();
Console.WriteLine(task.Result);
task.Wait();
Console.WriteLine("Result: {0} {1}", task.Result.Item1, task.Result.Item2);
Console.ReadLine();
}
}
/*
執行結果
(11, 5)
result:11 5
*/
9 task死鎖的問題
我們可以設置最大等待時間,如果超過了等待時間,就不再等待,下面我們來修改代碼,設置最大等待時間爲5秒(項目中可以根據實際情況設置),如果超過5秒就輸出哪個任務出錯了。
10 對Spinlock的使用
舉例來說Parallel.for和Parallel.foreach是線程不安全的,有可能達不到你的預期,此時就需要加鎖來解決此問題,我們可以加lock和spinlock(自旋鎖)來解決。
SpinLock slock = new SpinLock(false);
var testLock= new object();
long sum1 = 0;
long sum2 = 0;
long sum3 = 0;
Parallel.For(0, 100000, i =>
{
sum1 += i;
});
Parallel.For(0, 100000, i =>
{
bool lockTaken = false;
try
{
slock.Enter(ref lockTaken);
sum2 += i;
}
finally
{
if (lockTaken)
slock.Exit(false);
}
});
Parallel.For(0, 100000, i =>
{
lock(testLock)
{
sum3 += i;
};
});
Console.WriteLine("Num1的值爲:{0}", sum1);
Console.WriteLine("Num2的值爲:{0}", sum2);
Console.WriteLine("Num3的值爲:{0}", sum3);
/*
輸出結果:
Num1的值爲:1660913202
Num2的值爲:4999950000
Num3的值爲:4999950000
Num1的值爲:2754493646
Num2的值爲:4999950000
Num3的值爲:4999950000
Num1的值爲:4999950000
Num2的值爲:4999950000
Num3的值爲:4999950000
*/
三 Thread & Task比較
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace DemoAsync{
class Program{
static void Main(string[] args)
{
Console.WriteLine("Task With Thread Start !");
for (int i = 0; i <= 5; i++)
{
Thread t = new Thread(Dotaskfunction);
t.Start();
}
Console.WriteLine("Task With Thread End !");
Console.WriteLine("Task With Task Start !");
for (int i = 0; i <= 5; i++)
{
Task.Run(() => { Dotaskfunction(); });
}
Console.WriteLine("Task With Task End !");
Console.ReadLine();
}
public static void Dotaskfunction()
{
Console.WriteLine("task has been done! ThreadID: {0},IsBackGround:{1} ", Thread.CurrentThread.ManagedThreadId,Thread.CurrentThread .IsBackground );
}
}
}
可以看到Thread方法每次的Thread Id都是不同的,而Task方法的Thread Id是重複出現的。我們知道線程的創建和銷燬是一個開銷比較大的操作,Task每次執行將不會立即創建一個新線程,而是到CLR線程池查看是 否有空閒的線程,有的話就取一個線程處理這個請求,處理完請求後再把線程放回線程池,這個線程也不會立即撤銷,而是設置爲空閒狀態,可供線程池再次調度, 從而減少開銷。
5 IsBackground作用
要點:
1、當在主線程中創建了一個線程,那麼該線程的IsBackground默認是設置爲FALSE的。
2、當主線程退出的時候,IsBackground=FALSE的線程還會繼續執行下去,直到線程執行結束。
3、只有IsBackground=TRUE的線程纔會隨着主線程的退出而退出。
4、當初始化一個線程,把Thread.IsBackground=true的時候,指示該線程爲後臺線程。後臺線程將會隨着主線程的退出而退出。
5、原理:只要所有前臺線程都終止後,CLR就會對每一個活在的後臺線程調用Abort()來徹底終止應用程序。
Net的公用語言運行時(Common Language Runtime,CLR)能區分兩種不同類型的線程:前臺線程和後臺線程。這兩者的區別就是:應用程序必須運行完所有的前臺線程纔可以退出;而對於後臺線程,應用程序則可以不考慮其是否已經運行完畢而直接退出,所有的後臺線程在應用程序退出時都會自動結束。
既然前臺線程和後臺線程有這種差別,那麼我們怎麼知道該如何設置一個線程的IsBackground屬性呢?下面是一些基本的原則:對於一些在後臺運行的線程,當程序結束時這些線程沒有必要繼續運行了,那麼這些線程就應該設置爲後臺線程。比如一個程序啓動了一個進行大量運算的線程,可是隻要程序一旦結束,那個線程就失去了繼續存在的意義,那麼那個線程就該是作爲後臺線程的。而對於一些服務於用戶界面的線程往往是要設置爲前臺線程的,因爲即使程序的主線程結束了,其他的用戶界面的線程很可能要繼續存在來顯示相關的信息,所以不能立即終止它們。這裏我只是給出了一些原則,具體到實際的運用往往需要編程者的進一步仔細斟酌。
一般後臺線程用於處理時間較短的任務,如在一個Web服務器中可以利用後臺線程來處理客戶端發過來的請求信息。而前臺線程一般用於處理需要長時間等待的任務,如在Web服務器中的監聽客戶端請求的程序,或是定時對某些系統資源進行掃描的程序。
四 threadpoll和task的結構圖
threadpool:
task:
五 Async/Await
本節內容來自這裏。
async/await特性是與Task緊密相關的,所以在瞭解async/await
前必須充分了解Task的使用。
1 示例
1、從 Main 方法執行到CountCharactersAsync(1, url1)
方法時,該方法會立即返回,然後纔會調用它內部的方法開始下載內容。該方法返回的是一個Task<int>
類型的佔位符對象,表示計劃進行的工作。這個佔位符最終會返回 int 類型的值。
2、這樣就可以不必等CountCharactersAsync(1, url1)
方法執行完成就可以繼續進行下一步操作。到執行CountCharactersAsync(2, url2)
方法時,一樣返回Task<int>
對象。
3、然後,Main
方法繼續執行三次ExtraOperation
方法,同時兩次 CountCharactersAsync
方法依然在持續工作 。
4、t1.Result
和t2.Result
是指從CountCharactersAsync
方法調用的Task<int>
對象取結果,如果還沒有結果的話,將阻塞,直有結果返回爲止。
2 async/await 結構
async/await 結構可分成三部分:
(1)調用方法:該方法調用異步方法,然後在異步方法執行其任務的時候繼續執行;
(2)異步方法:該方法異步執行工作,然後立刻返回到調用方法;
(3)await 表達式:用於異步方法內部,指出需要異步執行的任務。一個異步方法可以包含多個 await 表達式(不存在 await 表達式的話 IDE 會發出警告)。
3 What’s 異步方法
異步方法:在執行完成前立即返回調用方法,在調用方法繼續執行的過程中完成任務。
語法分析:
(1)關鍵字:方法頭使用async
修飾。
(2)要求:包含 N(N>0) 個await
表達式(不存在await
表達式的話 IDE 會發出警告),表示需要異步執行的任務,沒有的話,就和普通方法一樣執行了。
(3)返回類型:只能返回 3 種類型(void
、Task
和Task\<T>
)。Task
和Task\<T>
標識返回的對象會在將來完成工作,表示調用方法和異步方法可以繼續執行。
(4)參數:數量不限,但不能使用 out 和 ref 關鍵字。
(5)命名約定:方法後綴名應以 Async 結尾。
(6)其它:匿名方法和 Lambda 表達式也可以作爲異步對象;async 是一個上下文關鍵字;關鍵字 async 必須在返回類型前。
關於 async 關鍵字:
1、在返回類型之前包含 async 關鍵字;
2、它只是標識該方法包含一個或多個 await 表達式,即,它本身不創建異步操作;
3、它是上下文關鍵字,即可作爲變量名。
4 返回類型
1、Task<T>:調用方法要從調用中獲取一個 T 類型的值,異步方法的返回類型就必須是Task。調用方法從 Task 的 Result 屬性獲取的就是 T 類型的值。
internal class Calculator
{
private static int Add(int n, int m)
{
return n + m;
}
public static async Task<int> AddAsync(int n, int m)
{
int val = await Task.Run(() => Add(n, m));
return val;
}
}
private static void Main(string[] args)
{
Task<int> t = Calculator.AddAsync(1, 2);
//一直在幹活
Console.WriteLine($"result: {t.Result}");
Console.Read();
}
2、Task:調用方法不需要從異步方法中取返回值,但是希望檢查異步方法的狀態,那麼可以選擇可以返回 Task 類型的對象。不過,就算異步方法中包含 return 語句,也不會返回任何東西。
internal class Calculator
{
private static int Add(int n, int m)
{
return n + m;
}
public static async Task AddAsync(int n, int m)
{
int val = await Task.Run(() => Add(n, m));
Console.WriteLine($"Result: {val}");
}
}
private static void Main(string[] args)
{
Task t = Calculator.AddAsync(1, 2);
//一直在幹活
t.Wait();
Console.WriteLine("AddAsync 方法執行完成");
Console.Read();
}
(3)void:調用方法執行異步方法,但又不需要做進一步的交互。
internal class Calculator
{
private static int Add(int n, int m)
{
return n + m;
}
public static async void AddAsync(int n, int m)
{
int val = await Task.Run(() => Add(n, m));
Console.WriteLine($"Result: {val}");
}
}
private static void Main(string[] args)
{
Calculator.AddAsync(1, 2);
//一直在幹活
Thread.Sleep(1000); //掛起1秒鐘
Console.WriteLine("AddAsync 方法執行完成");
Console.Read();
}
參考文章
c#之task與thread區別及其使用:https://blog.csdn.net/qq_40677590/article/details/102797838
c#中任務Task:https://blog.csdn.net/liyazhen2011/article/details/81262582
走進異步編程的世界 - 剖析異步方法(上):https://www.cnblogs.com/liqingwen/p/5844095.html
走進異步編程的世界 - 剖析異步方法(下):cnblogs.com/liqingwen/p/5866241.html