一、任務Task
#System.Threading.Tasks在.NET4引入,前面執行緒的API太多了,控制不方便,ThreadPool控制能力又太弱,比如做線程的延續、阻塞、取消、超時等功能不太方便,所以Task就抽象了線程功能,在後台使用ThreadPool
#1、啟動任務
可以使用TaskFactory類別或Task類別的建構子和Start()方法,委託可以提供帶有一個Object類型的輸入參數,所以可以傳遞給任務任意數據,還漏了一個常用的Task.Run
TaskFactory taskFactory = new TaskFactory(); taskFactory.StartNew(() => { Console.WriteLine($"tid={Thread.CurrentThread.ManagedThreadId},datetime={DateTime.Now}"); }); Task.Factory.StartNew(() => { Console.WriteLine($"tid={Thread.CurrentThread.ManagedThreadId},datetime={DateTime.Now}"); }); Task task = new Task(() => { Console.WriteLine($"tid={Thread.CurrentThread.ManagedThreadId},datetime={DateTime.Now}"); }); task.Start();
只有Task類別實例方式需要Start()去啟動任務,當然可以RunSynchronously()來同步執行任務,主執行緒會等待,就是用主執行緒來執行這個task任務
Task task = new Task(() => { Thread.Sleep(10000); Console.WriteLine($"tid={Thread.CurrentThread.ManagedThreadId},datetime={DateTime.Now}"); }); task.RunSynchronously();
2、阻塞延續
在Thread中我們使用join來阻塞等待,在多個Thread時進行控制就不太方便。在Task中我們使用實例方法Wait阻塞單一任務或靜態方法WaitAll和WaitAny阻塞多個任務
var task = new Task(() => { Thread.Sleep(5*1000); Console.WriteLine($"tid={Thread.CurrentThread.ManagedThreadId},datetime={DateTime.Now}"); }); var task2 = new Task(() => { Thread.Sleep(10 * 1000); Console.WriteLine($"tid={Thread.CurrentThread.ManagedThreadId},datetime={DateTime.Now}"); }); task.Start(); task2.Start(); //task.Wait();//单任务等待 //Task.WaitAny(task, task2);//任何一个任务完成就继续 Task.WaitAll(task, task2);//任务都完成才继续
如果不希望阻塞主線程,實作當一個任務或幾個任務完成後執行別的任務,可以使用Task靜態方法WhenAll和WhenAny,他們將返回一個Task,但這個Task不允許你控制,將會在滿足WhenAll和WhenAny裡任務完成時自動完成,然後調用Task的ContinueWith方法,就可以在一個任務完成後跟上開始另一個任務
Task.WhenAll(task, task2).ContinueWith((t) => { Console.WriteLine($"tid={Thread.CurrentThread.ManagedThreadId},datetime={DateTime.Now}"); }); Task.Factory工厂中也存在类似ContinueWhenAll和ContinueWhenAny
3、任務層次結構
#不僅可以在一個任務結束後執行另一個任務,也可以在一個任務內啟動一個任務,這啟動了一個父子層次結構
var parentTask = new Task(()=> { Console.WriteLine($"parentId={Thread.CurrentThread.ManagedThreadId},datetime={DateTime.Now}"); Thread.Sleep(5*1000); var childTask = new Task(() => { Thread.Sleep(10 * 1000); Console.WriteLine($"childId={Thread.CurrentThread.ManagedThreadId},datetime={DateTime.Now}") }); childTask.Start(); }); parentTask.Start();
如果父任務在子任務之前結束,父任務的狀態為WaitingForChildrenToComplete,當子任務也完成時,父任務的狀態就變為RanToCompletion,當然,在建立任務時指定TaskCreationOptions枚舉參數,可以控制任務的創建和執行的可選行為
4、枚舉參數
#簡單介紹下創建任務中的TaskCreationOptions枚舉參數,創建任務時我們可以提供TaskCreationOptions枚舉參數,用於控制任務的創建和執行的可選行為的標誌
AttachedToParent:指定將任務附加到任務層次結構中的某個父級,意思是建立父子關係,父任務必須等待子任務完成才可以繼續執行。和WaitAll效果一樣。上面範例如果在建立子任務時指定TaskCreationOptions.AttachedToParent,那麼父任務wait時也會等子任務的結束
DenyChildAttach:不讓子任務附加到父任務上
#LongRunning:指定是長時間運行任務,如果事先知道這個任務會耗時比較長,建議設定此項目。這樣,Task調度器會建立Thread線程,而不使用ThreadPool線程。因為你長時間佔用ThreadPool線程不還,那它可能必要時會在線程池中開啟新的線程,造成調度壓力
PreferFairness:盡可能公平的安排任務,這意味著較早安排的任務將更可能較早運行,而較晚安排運行的任務將更可能較晚運行。實際上透過把任務放到執行緒池的全域隊列中,讓工作執行緒去爭搶,預設是在本地隊列中。
另一個枚舉參數是ContinueWith方法中的TaskContinuationOptions枚舉參數,它除了擁有幾個和上面同樣功能的枚舉值外,還擁有控制任務的取消延續等功能
#LazyCancellation:在延續取消的情況下,防止延續的完成直到完成先前的任務。什麼意思呢?
CancellationTokenSource source = new CancellationTokenSource(); source.Cancel(); var task1 = new Task(() => { Console.WriteLine($"task1 id={Thread.CurrentThread.ManagedThreadId},datetime={DateTime.Now}"); }); var task2 = task1.ContinueWith(t => { Console.WriteLine($"task2 id={Thread.CurrentThread.ManagedThreadId},datetime={DateTime.Now}"); },source.Token); var task3 = task2.ContinueWith(t => { Console.WriteLine($"task3 id={Thread.CurrentThread.ManagedThreadId},datetime={DateTime.Now}"); }); task1.Start();
上面例子我們企圖task1->task2->task3順序執行,然後透過CancellationToken來取消task2的執行。結果會是怎樣呢?結果task1和task3會並行執行(task3也是會執行的,而且是和task1並行,等於原來的一條鏈變成了兩條鏈),然後我們嘗試使用
LazyCancellation, var task2 = task1.ContinueWith(t => { Console.WriteLine($"task2 id={Thread.CurrentThread.ManagedThreadId},datetime={DateTime.Now}"); },source.Token,TaskContinuationOptions.LazyCancellation,TaskScheduler.Current);
這樣,將會在task1執行完成後,task2才去判斷source.Token,為Cancel就不執行,接下來執行task3就保證了原來的順序
ExecuteSynchronously:指定應同步執行延續任務,比如上例中,在延續任務task2中指定此參數,則task2會使用執行task1的執行緒來執行,這樣防止執行緒切換,可以做些共有資源的存取。不指定的話就隨機,但也能也用到task1的執行緒
NotOnRanToCompletion:延續任務必須在前面任務非完成狀態下執行
OnlyOnRanToCompletion:延續任務必須在前面任務完成狀態才能執行
NotOnFaulted,OnlyOnCanceled,OnlyOnFaulted等等
5、任務取消
在上篇使用Thread時,我們使用一個變數isStop標記是否取消任務,這種存取共享變數的方式難免會出問題。 task中提出CancellationTokenSource類別專門處理任務取消,常見用法看下面程式碼註解
CancellationTokenSource source = new CancellationTokenSource();//构造函数中也可指定延迟取消 //注册一个取消时调用的委托 source.Token.Register(() => { Console.WriteLine("当前source已经取消,可以在这里做一些其他事情(比如资源清理)..."); }); var task1 = new Task(() => { while (!source.IsCancellationRequested) { Console.WriteLine($"task1 id={Thread.CurrentThread.ManagedThreadId},datetime={DateTime.Now}"); } },source.Token); task1.Start(); //source.Cancel();//取消 source.CancelAfter(1000);//延时取消
6、任務結果
让子线程返回结果,可以将信息写入到线程安全的共享变量中去,或则使用可以返回结果的任务。使用Task的泛型版本Taskb54c2c292509147c0b54128f4eb90887,就可以定义返回结果的任务。Task是继承自Task的,Result获取结果时是要阻塞等待直到任务完成返回结果的,内部判断没有完成则wait。通过TaskStatus属性可获得此任务的状态是启动、运行、异常还是取消等
var task = new Task<string>(() => { return "hello ketty"; }); task.Start(); string result = task.Result;
7、异常
可以使用AggregateException来接受任务中的异常信息,这是一个聚合异常继承自Exception,可以遍历获取包含的所有异常,以及进行异常处理,决定是否继续往上抛异常等
var task = Task.Factory.StartNew(() => { var childTask1 = Task.Factory.StartNew(() => { throw new Exception("childTask1异常..."); },TaskCreationOptions.AttachedToParent); var childTask12= Task.Factory.StartNew(() => { throw new Exception("childTask2异常..."); }, TaskCreationOptions.AttachedToParent); }); try { try { task.Wait(); } catch (AggregateException ex) { foreach (var item in ex.InnerExceptions) { Console.WriteLine($"message{item.InnerException.Message}"); } ex.Handle(x => { if (x.InnerException.Message == "childTask1异常...") { return true;//异常被处理,不继续往上抛了 } return false; }); } } catch (Exception ex) { throw; }
二、并行Parallel
1、Parallel.For()、Parallel.ForEach()
在.NET4中,另一个新增的抽象的线程时Parallel类。这个类定义了并行的for和foreach的静态方法。Parallel.For()和Parallel.ForEach()方法多次调用一个方法,而Parallel.Invoke()方法允许同时调用不同的方法。首先Parallel是会阻塞主线程的,它将让主线程也参与到任务中
Parallel.For()类似于for允许语句,并行迭代同一个方法,迭代顺序没有保证的
ParallelLoopResult result = Parallel.For(0, 10, i => { Console.WriteLine($"{i} task:{Task.CurrentId} thread:{Thread.CurrentThread.ManagedThreadId}"); }); Console.WriteLine(result.IsCompleted);
也可以提前中断Parallel.For()方法。For()方法的一个重载版本接受Action0b577780dcee67a906216f9ca0bee521类型参数。一般不使用,像下面这样,本想大于5就停止,但实际也可能有大于5的任务已经在跑了。可以通过ParallelOptions传入允许最大线程数以及取消Token等
ParallelLoopResult result = Parallel.For(0, 10, new ParallelOptions() { MaxDegreeOfParallelism = 8 },(i,loop) => { Console.WriteLine($"{i} task:{Task.CurrentId} thread:{Thread.CurrentThread.ManagedThreadId}"); if (i > 5) { loop.Break(); } });
2、Parallel.For484ebffe0129924a37882ea651634562
For还有一个高级泛型版本,相当于并行的聚合计算
ParallelLoopResult For<TLocal>(int fromInclusive, int toExclusive, Func<TLocal> localInit, Func<int, ParallelLoopState, TLocal, TLocal> body, Action<TLocal> localFinally);
像下面这样我们求0…100的和,第三个参数更定一个种子初始值,第四个参数迭代累计,最后聚合
int totalNum = 0; Parallel.For<int>(0, 100, () => { return 0; }, (current, loop, total) => { total += current; return total; }, (total) => { Interlocked.Add(ref totalNum, total); });
上面For用来处理数组数据,ForEach()方法用来处理非数组的数据任务,比如字典数据继承自IEnumerable的集合等
3、Parallel.Invoke()
Parallel.Invoke()则可以并行调用不同的方法,参数传递一个Action的委托数组
Parallel.Invoke(() => { Console.WriteLine($"方法1 thread:{Thread.CurrentThread.ManagedThreadId}"); } , () => { Console.WriteLine($"方法2 thread:{Thread.CurrentThread.ManagedThreadId}"); } , () => { Console.WriteLine($"方法3 thread:{Thread.CurrentThread.ManagedThreadId}"); });
4、PLinq
Plinq,为了能够达到最大的灵活度,linq有了并行版本。使用也很简单,只需要将原始集合AsParallel就转换为支持并行化的查询。也可以AsOrdered来顺序执行,取消Token,强制并行等
var nums = Enumerable.Range(0, 100); var query = from n in nums.AsParallel() select new { thread=$"tid={Thread.CurrentThread.ManagedThreadId},datetime={DateTime.Now}" };
三、异步等待AsyncAwait
异步编程模型,可能还需要大篇幅来学习,这里先介绍下基本用法,内在本质需要用ILSpy反编译来看,以后可能要分专题总结。文末先给几个参考资料,有兴趣自己阔以先琢磨琢磨鸭
1、简单使用
这是.NET4.5开始提供的一对语法糖,使得可以较简便的使用异步编程。async用在方法定义前面,await只能写在带有async标记的方法中,任何方法都可以增加async,一般成对出现,只有async没有意义,只有await会报错,请先看下面的示例
private static async void AsyncTest() { //主线程执行 Console.WriteLine($"before await ThreadId={Thread.CurrentThread.ManagedThreadId}"); TaskFactory taskFactory = new TaskFactory(); Task task = taskFactory.StartNew(() => { Thread.Sleep(3000); Console.WriteLine($"task ThreadId={Thread.CurrentThread.ManagedThreadId}"); }); await task;//主线程到这里就返回了,执行主线程任务 //子线程执行,其实是封装成委托,在task之后成为回调(编译器功能 状态机实现) 后面相当于task.ContinueWith() //这个回调的线程是不确定的:可能是主线程 可能是子线程 也可能是其他线程,在winform中是主线程 Console.WriteLine($"after await ThreadId={Thread.CurrentThread.ManagedThreadId}"); }
一般使用async都会让方法返回一个Task的,像下面这样复杂一点的
private static async Task<string> AsyncTest2() { Console.WriteLine($"before await ThreadId={Thread.CurrentThread.ManagedThreadId}"); TaskFactory taskFactory = new TaskFactory(); string x = await taskFactory.StartNew(() => { Thread.Sleep(3000); Console.WriteLine($"task ThreadId={Thread.CurrentThread.ManagedThreadId}"); return "task over"; }); Console.WriteLine($"after await ThreadId={Thread.CurrentThread.ManagedThreadId}"); return x; }
通过var reslult = AsyncTest2().Result;调用即可。但注意如果调用Wait或Result的代码位于UI线程,Task的实际执行在其他线程,其需要返回UI线程则会造成死锁,所以应该Async all the way
2、优雅
从上面简单示例中可以看出异步编程的执行逻辑:主线程A逻辑->异步任务线程B逻辑->主线程C逻辑。
异步方法的返回类型只能是void、Task、Task。示例中异步方法的返回值类型是Task,通常void也不推荐使用,没有返回值直接用Task就是
上一篇也大概了解到如果我们要在任务中更新UI,需要调用Invoke通知UI线程来更新,代码看起来像下面这样,在一个任务后去更新UI
private void button1_Click(object sender, EventArgs e) { var ResultTask = Task.Run(() => { Thread.Sleep(5000); return "任务完成"; }); ResultTask.ContinueWith((r)=> { textBox1.Invoke(() => { textBox1.Text = r.Result; }); }); }
如果使用async/await会看起来像这样,是不是优雅了许多。以看似同步编程的方式实现异步
private async void button1_Click(object sender, EventArgs e) { var t = Task.Run(() => { Thread.Sleep(5000); return "任务完成"; }); textBox1.Text = await t; }
3、最后
在.NET 4.5中引入的Async和Await两个新的关键字后,用户能以一种简洁直观的方式实现异步编程。甚至都不需要改变代码的逻辑结构,就能将原来的同步函数改造为异步函数。
在内部实现上,Async和Await这两个关键字由编译器转换为状态机,通过System.Threading.Tasks中的并行类实现代码的异步执行。
本文来自 C#.Net教程 栏目,欢迎学习!
以上是.NET中async非同步、thread多執行緒的詳細內容。更多資訊請關注PHP中文網其他相關文章!