一 簡介:
多線程的原理我就不講了,做開發(fā)處處用到。C#也提供了幾種多線程的使用方式,分別是Thread類、ThreadPool類、Parallel 類和Task類,這里只講Task,其他的知道有就可以了,就像當(dāng)初做iOS開發(fā)的時候,也把蘋果原生的幾種線程都學(xué)了,也做了博客記錄,但是在實際開發(fā)當(dāng)中這么多年卻只用了GCD這一種方式來處理,問就是好用、性能高。就像人吃飯一樣,有肉不吃干嘛非要去喝湯,對吧?
二 Task介紹
Task 是 .NET 中用于表示異步操作的類,出現(xiàn)在C#4.0時代,可以簡單看作相當(dāng)于Thead+TheadPool,其性能比直接使用Thread要好太多,它可以適用于很多種場景和功能。
比如:
- 異步執(zhí)行代碼:Task 允許在單獨的線程上執(zhí)行代碼塊,從而避免阻塞主線程,提高程序的響應(yīng)性和并發(fā)性。
- 等待異步操作完成:通過 await 關(guān)鍵字可以等待 Task 完成,從而實現(xiàn)非阻塞的異步操作。
- 并行執(zhí)行多個任務(wù):Task 可以用于并行執(zhí)行多個獨立的任務(wù),從而加快任務(wù)的完成速度,提高系統(tǒng)的性能。
- 處理異常:Task 提供了異常處理機制,可以在異步操作中捕獲和處理異常。
- 輕量級的線程管理:Task 使用線程池來管理線程,避免了頻繁創(chuàng)建和銷毀線程的開銷,使得線程的管理更加高效。
- 取消異步操作:Task 支持取消操作,可以通過 CancellationToken 來取消正在進行的異步操作。
總之,Task在多線程開發(fā)中起著非常重要的作用,一定要學(xué)好并用好。
三 Task開啟方式
- Task對象的Start()方法
Console.WriteLine("當(dāng)前線程1:{0}", Thread.CurrentThread.ManagedThreadId);
Task task1 = new Task(() =>// 無返回值
{
Console.WriteLine("當(dāng)前線程2:{0}", Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(2000);
});
task1.Start();
Console.WriteLine("主線程不受影響");
Task<string> task2 = new Task<string>(() => {// 有返回值string類型
return "task2";
});
task2.Start();
string str = task2.Result;
Console.WriteLine(str);
- Task的靜態(tài)方法(函數(shù))Run()
Task.Run(() =>
{
Console.WriteLine("這里是子線程嘍");
});
Task<string> task = Task.Run<string>(() =>
{
return"這里是子線程嘍";
});
string str = task.Result;
Console.WriteLine($"{str}");
- Task工廠
TaskFactory factory = Task.Factory;
factory.StartNew(() =>
{
Console.WriteLine("你好 task");
});
Task<string> task = factory.StartNew<string>(() =>
{
return "你好 task";
});
string str = task.Result;
Console.WriteLine($"{str}");
四 Task調(diào)用方法
Debug.WriteLine("主線程--{0}", Thread.CurrentThread.ManagedThreadId);
Task.Run(t1);
Task.Run(() => t1());
Task.Run(() => t2("ceshi"));
private void t1()
{
Debug.WriteLine("t1--{0}", Thread.CurrentThread.ManagedThreadId);
}
private void t2(string str)
{
Debug.WriteLine("t2--{0}", Thread.CurrentThread.ManagedThreadId);
}
五 Task線程等待(阻塞線程)
- Wait()
Task task1 = Task.Run(() =>
{
Thread.Sleep(1000);
Console.WriteLine("我排第二");
});
Console.WriteLine("我先打印");
task1.Wait(2000); //等待2000毫秒后再往下走 如果寫task1.Wait()就是必須要等task1子線程走完才能往下走(那我有必要開子線程嘛)
Task task2 = Task.Run(() =>
{
Thread.Sleep(1000);
Console.WriteLine("我墊底了嘛");
});
task2.Wait(500);
Console.WriteLine("我當(dāng)老三");
- WaitAll()
Console.WriteLine("來任務(wù)咯");
Task task1 = Task.Run(() => {
Thread.Sleep(1000);
Console.WriteLine("我耗時1秒");
});
Task task2 = Task.Run(() => {
Thread.Sleep(2000);
Console.WriteLine("我耗時2秒");
});
Task.WaitAll(task1, task2);// 等待task1和task2任務(wù)全走完才能往后走
Console.WriteLine("沒事,我等你們走完我再走");
- WaitAny()
Console.WriteLine("來任務(wù)咯");
Task task1 = Task.Run(() => {
Thread.Sleep(1000);
Console.WriteLine("我耗時1秒");
});
Task task2 = Task.Run(() => {
Thread.Sleep(2000);
Console.WriteLine("我耗時2秒");
});
Task.WaitAny(task1, task2);// task1和task2只要有一個任務(wù)完成就往后走
Console.WriteLine("只要有一個任務(wù)完成我就走嘍");
六 Task任務(wù)的延續(xù)
- TaskAwaiter<TResult> GetAwaiter():Task類的實例方法,返回TaskAwaiter對象。
- TResult GetResult():TaskAwaiter類的實例方法,返回線程任務(wù)的返回結(jié)果。
Task<int> task1 = Task.Run<int>(() => { return 1; });
Task<int> task2 = Task.Run<int>(() => { return 2; });
TaskAwaiter<int> task1Awaiter = task1.GetAwaiter();
TaskAwaiter<int> task2Awaiter = task2.GetAwaiter();
Task<int> task = Task.Run<int>(() =>
{
return task1Awaiter.GetResult() + task2Awaiter.GetResult();
});
Console.WriteLine($"讓我來看看你們給的數(shù)字和是:{task.Result}");
Console.WriteLine("會阻塞主線程哦");
- WhenAll(task1,task2,...):Task的靜態(tài)方法,作用是異步等待指定任務(wù)完成后,返回結(jié)果。當(dāng)線程任務(wù)有返回值時,返回Task<TResult[]>對象,否則返回Task對象。
- WhenAny()用法與WhenAll()是一樣的,不同的是只要指定的任意一個線程任務(wù)完成則立即返回結(jié)果。
// WhenAll WhenAny + ContinueWith 不阻塞主線程
Task<int> task1 = Task.Run(() => { Thread.Sleep(5000); return 1; });
Task<int> task2 = Task.Run(() => { Thread.Sleep(5000); return 2; });
Task<string> task = Task.WhenAll(task1, task2).ContinueWith((t) => {
// t:ContinueWith的返回值,一個新的延續(xù)task
Debug.WriteLine(task1.Result);
Debug.WriteLine(task2.Result);
return "3";
});
Console.WriteLine($"我立馬打印,對我沒影響");
Console.WriteLine($"我要等task走完才能打印哦:{task.Result}");
Console.WriteLine($"上面堵了,我也要等了");
}
- ContinueWith():Task類的實例方法,異步創(chuàng)建當(dāng)另一任務(wù)完成時可以執(zhí)行的延續(xù)任務(wù)。也就是當(dāng)調(diào)用對象的線程任務(wù)完成后,執(zhí)行ContinueWith()中的任務(wù)。
- ContinueWhenAny:某一個任務(wù)執(zhí)行結(jié)束后,去觸發(fā)一個動作,不卡主線程,TaskFactory實例方法,等價于WhenAny+ContinueWith
- ContinueWhenAll:所有任務(wù)執(zhí)行完成后,去觸發(fā)一個動作,不卡主線程,TaskFactory實例方法,等價于WhenAll+ContinueWith
七 Task枚舉
-
AttachedToParent:父子任務(wù)
假設(shè)遇到如下需求,線程parentTask中開啟了線程task1和task2,希望在開啟parentTask線程任務(wù)的主線程中阻塞等待parentTask、task1和task2的任務(wù)完成。
此時可以將task1和task2線程依附到parentTask線程上作為parentTask的子線程,這樣主線程在等待parentTask線程完成時,就必須同步等待task1和task2線程的任務(wù)完成
Task parentTask = new Task(() => {
Task task1 = new Task(() => { Console.WriteLine("task1任務(wù)。。。。。。"); }, TaskCreationOptions.AttachedToParent);
Task task2 = new Task(() => { Console.WriteLine("task2任務(wù)。。。。。。"); }, TaskCreationOptions.AttachedToParent);
task1.Start();
task2.Start();
});
parentTask.Start();
parentTask.Wait();
Console.WriteLine("我是主線程");
-
LongRunning:耗時任務(wù)
當(dāng)要執(zhí)行的線程任務(wù)比較耗時時,建議在創(chuàng)建線程對象時傳入?yún)?shù)TaskCreationOptions.LongRunning,以此來聲明為長時間運行的線程任務(wù)。
默認(rèn)情況下,新建Task線程是從線程池ThreadPool中分配出來的,當(dāng)使用TaskCreationOptions.LongRunning聲明后則是直接新建一個線程。這樣就可以避免耗時任務(wù)一直占用線程池資源的情況。當(dāng)然了,也可以直接使用Thread,效果上是一樣的。
Task task = new Task(() => { ...}, TaskCreationOptions.LongRunning);
task.Start();
八 Task中使用取消令牌
-
介紹說明:
Task中的取消功能使用的是CanclelationTokenSource,即取消令牌源對象,可用于解決多線程任務(wù)中協(xié)作取消和超時取消。
CancellationToken Token:CanclelationTokenSource類的屬性成員,返回CancellationToken對象,可以在開啟或創(chuàng)建線程時作為參數(shù)傳入。
bool IsCancellationRequested:CanclelationTokenSource類的屬性成員,表示當(dāng)前任務(wù)是否已經(jīng)請求取消。Token類中也有此屬性成員,兩者互相關(guān)聯(lián)。
void Cancel():CanclelationTokenSource類的實例方法,取消線程任務(wù),同時將自身以及關(guān)聯(lián)的Token對象中的IsCancellationRequested屬性置為true。
void CancelAfter(int millisecondsDelay):CanclelationTokenSource類的實例方法,用于延遲取消線程任務(wù)。
CancellationTokenRegistration Register(Action callback):Token類的實例方法,用于注冊取消任務(wù)后的回調(diào)任務(wù)。
Dispose (): 當(dāng)不再需要取消令牌時,應(yīng)調(diào)用 CancellationTokenSource 的 Dispose 方法來釋放資源。這是很重要的,特別是在長時間運行的應(yīng)用程序中,以確保不會發(fā)生資源泄露
// Task任務(wù)的取消和判斷
CancellationTokenSource cst = new CancellationTokenSource();
Task task = Task.Run(() => {
while (!cst.IsCancellationRequested)
{
Console.WriteLine("持續(xù)時間:" + DateTime.Now);
}
}, cst.Token);//這里第二個參數(shù)傳入取消令牌
Thread.Sleep(2000);
cst.Cancel(); //兩秒后結(jié)束
// 任務(wù)的延時取消可以用于訪問超時、執(zhí)行超時等情況下的任務(wù)強制終止
CancellationTokenSource cst = new CancellationTokenSource();
Task task = Task.Run(() => {
while (!cst.IsCancellationRequested)
{
Console.WriteLine("持續(xù)時間:" + DateTime.Now);
}
}, cst.Token);//這里第二個參數(shù)傳入取消令牌
cst.CancelAfter(2000); //兩秒后結(jié)束 也是異步進行
// Task任務(wù)取消回調(diào):如果取消任務(wù)后希望做一些處理工作。
// 此時可以使用CancellationToken類的Register()函數(shù)來注冊一個委托(回調(diào)函數(shù)),用于取消線程后調(diào)用。
CancellationTokenSource cst = new CancellationTokenSource();
Task task = Task.Run(() => {
while (!cst.IsCancellationRequested)
{
Console.WriteLine("持續(xù)時間:" + DateTime.Now);
Thread.Sleep(500);
}
}, cst.Token);//這里第二個參數(shù)傳入取消令牌
cst.Token.Register(() => {
Console.WriteLine("開始處理工作......");
Thread.Sleep(2000);
Console.WriteLine("處理工作完成......");
});
Thread.Sleep(2000);
cst.Cancel(); //兩秒后結(jié)束
- 常規(guī)用法:
private Task task;
private CancellationTokenSource cancellationSource;
private void TestTokenSource()
{
// 常規(guī)使用
cancellationSource = new CancellationTokenSource();
CancellationToken token = cancellationSource.Token;
task = Task.Run(() =>
{
while (!token.IsCancellationRequested)
{
Console.WriteLine("持續(xù)工作");
}
}, token);
task.Wait(5000);
cancellationSource?.Cancel();
cancellationSource?.Dispose();
}
或者
private CancellationTokenSource cts123;
private void button4_Click(object sender, EventArgs e)
{
cts123 = new CancellationTokenSource();
CancellationToken token = cts123.Token;
Task.Run(async () =>
{
Debug.WriteLine("開啟循環(huán)");
while (!token.IsCancellationRequested)
{
Debug.WriteLine("任務(wù)進行中...");
await Task.Delay(1000);
//await Task.Delay(1000,token); 如果取下這里會立即報 異常,可外部增加try來捕捉,就不用再等1秒了
}
Debug.WriteLine("結(jié)束循環(huán)");
}, token);
}
private void button5_Click(object sender, EventArgs e)
{
cts123?.Cancel();
cts123?.Dispose();
cts123 = null;
}
-
CancellationTokenSource和CancellationToken區(qū)別:
CancellationTokenSource(CTS): 這是生成取消令牌的主要對象??梢酝ㄟ^調(diào)用它的 Cancel 方法來請求取消一個或多個操作。
CancellationToken(CT): 這是一個輕量級的結(jié)構(gòu),由 CancellationTokenSource 生成,并可以傳遞到多個操作中。操作可以檢查此令牌的狀態(tài)以確定是否應(yīng)該取消其執(zhí)行。
cancellationSource = new CancellationTokenSource();
cancellationSource.Cancel();
cancellationSource.Dispose();
if (cancellationSource.Token.IsCancellationRequested)// 報錯 System.ObjectDisposedException:“CancellationTokenSource 已釋放?!? {
Console.WriteLine("cancle");
}
else
{
Console.WriteLine("not cancle");
}
if (cancellationSource.IsCancellationRequested)
{
Console.WriteLine("cancle");// 打印
}
else
{
Console.WriteLine("not cancle");
}
if (cancellationSource != null)
{
Console.WriteLine("not null");// 打印
}
else
{
Console.WriteLine(" null");
}
九 Task跨線程訪問控件
在使用Winform或WPF編寫程序時,經(jīng)常會遇到跨線程訪問控件的情況,除了使用Invoke和委托等方法外,還可以有以下兩種解決方法。
-
方式一:
直接將TaskScheduler對象做為參數(shù)傳給Start()函數(shù)(使用TaskScheduler.FromCurrentSynchronizationContext()可以獲得TaskScheduler對象),以此來將線程任務(wù)傳送到指定的調(diào)度程序中運行,結(jié)合WPF編程寶典多線程章節(jié)中的內(nèi)容,應(yīng)該是將線程任務(wù)丟給控件元素所在線程的調(diào)度程序中運行。這樣做雖然可以跨線程訪問控件,但是帶來的弊端就是,如果線程任務(wù)耗時,就會讓整個窗體卡住。
Task task = new Task(() =>
{
Thread.Sleep(5000);//模擬耗時處理
txt_Info.Text = "test"; //此為文本控件
});
task.Start(TaskScheduler.FromCurrentSynchronizationContext());
-
方式二:
針對線程耗時的情況,如果直接使用方式一,會導(dǎo)致整個UI界面都卡住,等到控件處理完成才恢復(fù),這樣顯然是不可以的。因此要改變一下用法,利用線程延續(xù),將耗時的任務(wù)與訪問UI控件的任務(wù)分為兩個線程,訪問UI的線程放到延續(xù)的線程中。
txt_Info.Text = "數(shù)據(jù)正在處理中......";
txt_Info.Text = "數(shù)據(jù)正在處理中......";
Task.Run(() =>
{
Thread.Sleep(5000);
}).ContinueWith(t => {
txt_Info.Text = "test";
}, TaskScheduler.FromCurrentSynchronizationContext());
十 Task的異常處理
1.線程外部使用Wait:Task線程的異常處理不能直接將線程對象相關(guān)代碼try-catch來捕獲,那樣是捕獲不到異常的,因為開始異常還沒發(fā)生,主線程已經(jīng)執(zhí)行完畢,需要通過調(diào)用線程對象的wait()函數(shù),通過wait()函數(shù)來進行線程的異常捕獲。此外,線程的異常會聚合到AggregateException異常對象中(AggregateException是專門用來收集線程異常的異常類),需要通過遍歷該異常對象,獲取正確的異常信息。如果捕獲到線程異常之后,還想繼續(xù)往上拋出,就需要調(diào)用AggregateException對象的Handle函數(shù),并返回false。(Handle函數(shù)遍歷了一下AggregateException對象中的異常)
Task task1 = Task.Run(() =>
{
throw new Exception("線程1的異常拋出");
});
Task task2 = Task.Run(() =>
{
throw new Exception("線程2的異常拋出");
});
Task task3 = Task.Run(() =>
{
throw new Exception("線程3的異常拋出");
});
try
{
task1.Wait();
task2.Wait();
task3.Wait();
}
catch (AggregateException ex)
{
foreach (var item in ex.InnerExceptions)
{
Console.WriteLine(item.Message);
}
//如果希望再將異常往外拋出,可以調(diào)用AggregateException的Handle函數(shù)
//ex.Handle(p => false);
}
Console.Read();
2.線程內(nèi)部可直接try-catch
try
{
Task task = Task.Run(() =>
{
try
{
int i = 0;
int j = 10;
int k = j / i; //嘗試除以0,會異常
}
catch (Exception ex)
{
Debug.WriteLine($"線程內(nèi)異常{ex.Message}");
}
});
}
catch (AggregateException aex)
{
foreach (var exception in aex.InnerExceptions)
{
Debug.WriteLine($"線程不等待:異常{exception.Message}");
}
}
十一 Task.Delay(5000).Wait()和await Task.Delay(5000)區(qū)別
- Task.Delay(5000); // 創(chuàng)建一個在指定的毫秒數(shù)后完成的任務(wù),Task.Delay() 是一個異步執(zhí)行的方法。
- Wait() 方法是一個同步方法,它會阻塞調(diào)用它的線程,直到 Task.Delay 返回的任務(wù)完成。
- await關(guān)鍵字用于等待一個Task或Task<T> 的完成,而不會阻塞當(dāng)前線程。你可以在async方法內(nèi)部使用await
十二 token傳遞和不傳遞的區(qū)別
在官方網(wǎng)站中是這樣解釋這個重載方法的:
Run(Action, CancellationToken):將指定的工作排入隊列,以在線程池上運行,并返回表示該工作的 Task 對象。 取消令牌允許取消工作(如果尚未啟動)。
那么我們試一下例子

我們傳遞token后

所以如果不傳遞token,我們就無法控制這個任務(wù)的取消。
在看一個例子

通過打印,可以看到,一旦task開啟后,即使傳遞了token,外面也無法真的取消掉,因此我們傳遞token的同時還必須在內(nèi)部對token進行IsCancellationRequested的判斷。
在C#中,當(dāng)您使用Task.Run并傳遞一個CancellationToken時,這個令牌不僅可以在任務(wù)內(nèi)部用于檢查取消請求,而且Task.Run方法本身也會對這個令牌進行監(jiān)視。這意味著,如果令牌在任務(wù)開始執(zhí)行之前或執(zhí)行過程中被取消,Task.Run返回的Task對象將能夠反映出這一點。然而,如果您不將CancellationToken作為參數(shù)傳遞給Task.Run,那么即使您在任務(wù)內(nèi)部定義了一個CancellationToken變量并檢查它,Task.Run返回的Task對象也不會知道這個令牌的存在或狀態(tài)。換句話說,Task對象本身不會因為這個令牌而被取消或改變其狀態(tài)。
其實,我們把token傳遞之后,任務(wù)本身知道了token的狀態(tài),會在令牌取消后,自己做一些提前退出或釋放資源等等,所以最好還是把token作為第二個參數(shù)進行傳遞。