Home>Article>Backend Development> async asynchronous, thread multi-threading in .NET
1. Task
System.Threading.Tasks was introduced in .NET4, and there are too many APIs for the previous threads It is inconvenient to control, and the ThreadPool control capability is too weak. For example, it is not convenient to perform thread continuation, blocking, cancellation, timeout and other functions, so Task abstracts the thread function and uses ThreadPool in the background
1. Start the task
You can use the constructor and Start() method of the TaskFactory class or Task class. The delegate can provide an input parameter with an Object type, so any data can be passed to the task. , and also missed a commonly used 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();
Only the Task class instance mode requires Start() to start the task. Of course, you can RunSynchronously() to execute the task synchronously. The main thread will wait, that is, use the main thread to Execute this task
Task task = new Task(() => { Thread.Sleep(10000); Console.WriteLine($"tid={Thread.CurrentThread.ManagedThreadId},datetime={DateTime.Now}"); }); task.RunSynchronously();
2. Blocking continuation
In Thread we use join to block and wait. It is not convenient to control multiple Threads. In Task, we use the instance method Wait to block a single task or the static methods WaitAll and WaitAny to block multiple tasks
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);//任务都完成才继续
If you do not want to block the main thread, you can execute other tasks after one task or several tasks are completed. Use Task static methods WhenAll and WhenAny. They will return a Task, but this Task does not allow you to control it. It will be automatically completed when the task in WhenAll and WhenAny is completed. Then call the Task's ContinueWith method to complete a task after it is completed. Immediately start another task
Task.WhenAll(task, task2).ContinueWith((t) => { Console.WriteLine($"tid={Thread.CurrentThread.ManagedThreadId},datetime={DateTime.Now}"); }); Task.Factory工厂中也存在类似ContinueWhenAll和ContinueWhenAny
3. Task hierarchy
Not only can you execute another task after the end of one task, you can also start a task within a task. Task, this starts a parent-child hierarchy
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();
If the parent task ends before the child task, the status of the parent task is WaitingForChildrenToComplete. When the child task is also completed, the status of the parent task becomes RanToCompletion. Of course , specify the TaskCreationOptions enumeration parameter when creating a task, which can control the optional behavior of task creation and execution
4. Enumeration parameters
A brief introduction to creating tasks In the TaskCreationOptions enumeration parameter, when creating a task, we can provide the TaskCreationOptions enumeration parameter, a flag used to control the optional behavior of task creation and execution
AttachedToParent: Specifies that the task is attached to the task hierarchy. A certain parent means establishing a parent-child relationship. The parent task must wait for the child task to complete before it can continue to execute. The effect is the same as WaitAll. In the above example, if TaskCreationOptions.AttachedToParent is specified when creating a subtask, the parent task will also wait for the end of the subtask when waiting.
DenyChildAttach: Do not allow the subtask to be attached to the parent task
LongRunning: Specify a long-running task. If you know in advance that the task will take a long time, it is recommended to set this. In this way, the Task scheduler will create Thread threads instead of using ThreadPool threads. Because you occupy the ThreadPool thread for a long time and do not return it, it may open a new thread in the thread pool when necessary, causing scheduling pressure
PreferFairness: Arrange tasks as fairly as possible, which means that they are scheduled earlier Tasks will be more likely to run earlier, and tasks scheduled to run later will be more likely to run later. In fact, the task is placed in the global queue of the thread pool and let the worker threads compete for it. The default is in the local queue.
Another enumeration parameter is the TaskContinuationOptions enumeration parameter in the ContinueWith method. In addition to having several enumeration values with the same functions as above, it also has functions such as controlling the cancellation and continuation of tasks.
LazyCancellation: In case of continuation cancellation, prevents completion of the continuation until the previous task is completed. What does that mean?
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();
In the above example, we attempt to execute task1->task2->task3 sequentially, and then cancel the execution of task2 through CancellationToken. What will be the result? As a result, task1 and task3 will be executed in parallel (task3 will also be executed, and in parallel with task1, which means that the original chain becomes two chains), and then we try to use
LazyCancellation, var task2 = task1.ContinueWith(t => { Console.WriteLine($"task2 id={Thread.CurrentThread.ManagedThreadId},datetime={DateTime.Now}"); },source.Token,TaskContinuationOptions.LazyCancellation,TaskScheduler.Current);
In this way, it will be in task1 After the execution is completed, task2 will judge the source.Token. If it is Cancel, it will not be executed. Then the execution of task3 will ensure the original order.
ExecuteSynchronously: Specifies that the continuation task should be executed synchronously. For example, in the above example, in the continuation If this parameter is specified in task task2, task2 will be executed using the thread that executes task1. This prevents thread switching and allows some access to shared resources. If not specified, it will be random, but the thread of task1 can also be used
NotOnRanToCompletion: The continuation task must be executed in the non-completion state of the previous task
OnlyOnRanToCompletion: The continuation task must be in the completion state of the previous task To execute
NotOnFaulted, OnlyOnCanceled, OnlyOnFaulted, etc.
5. Task cancellation
When using Thread in the previous article, we used a variable isStop mark Whether to cancel the task or not, this method of accessing shared variables will inevitably cause problems. The CancellationTokenSource class is proposed in task to specifically handle task cancellation. For common usage, see the code comments below
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 results
让子线程返回结果,可以将信息写入到线程安全的共享变量中去,或则使用可以返回结果的任务。使用Task的泛型版本Taskb54c2c292509147c0b54128f4eb90887,就可以定义返回结果的任务。Task是继承自Task的,Result获取结果时是要阻塞等待直到任务完成返回结果的,内部判断没有完成则wait。通过TaskStatus属性可获得此任务的状态是启动、运行、异常还是取消等
var task = new Task(() => { 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(int fromInclusive, int toExclusive, Func localInit, Func body, Action localFinally);
像下面这样我们求0…100的和,第三个参数更定一个种子初始值,第四个参数迭代累计,最后聚合
int totalNum = 0; Parallel.For(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 TaskAsyncTest2() { 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教程栏目,欢迎学习!
The above is the detailed content of async asynchronous, thread multi-threading in .NET. For more information, please follow other related articles on the PHP Chinese website!