打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
《CLR via C#》读书笔记

《CLR via C#》读书笔记-.NET多线程(五)

作者:zlbcdn

现状
使用ThreadPool的QueueUserWorkItem方法完成异步操作会存在两个问题:
1、系统无法知道异步操作是否完成
2、无法获取异步操作完成时的返回值
问题来了,那就需要新的解决方案(忽然想起上《通信原理》时老师讲的话,“遇到问题,解决问题,因此就有了不同的编码方式”,从调幅,到调频,再到码分….,工程领域的主题就是遇到问题,解决问题!跑题了!)
为了解决上面提到的问题,.NET提出了Task的概念
Task
Task的构造方法如下图所示:


其中参数有:
Action:.NET内部委托
Object:传入操作的数值
CancellationToken:协作式取消的Token
TaskCreationOption:创建Task时的创建方式选项
对于第四项,其规定TaskScheduler的行为方式,让TaskScheduler按照规定的方式创建Task。但是,TaskScheduler不一定按照此方式执行。因此这个参数不是很重要。
对于Task还有一个特别的地方:Task所使用的线程并不是来自.NET的线程池,而是新创建的一个Thread
可返回结果的Task类
可以使Task返回结果,这个时候需要用到Task<TResult>类,其就是代表一个可以有返回值的异步操作。
本类的构造函数如下:

其中,Func(TResult)的定义如下:

public delegate TResult Func<out TResult>()
  • 1

具体的例子如下:
假设,操作的代码如下:

private static int Sum(int n){    int sum=0;    for(;n>0;n--)        checked{sum += n;}    return sum;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

若程序希望获取操作的返回值,则应该如此使用:

Task<int> t = new Task<int>(n=>Sum((int)n),1000000);t.start();t.wait(); //可以写这句,也可以不写Console.WriteLine(t.Result);
  • 1
  • 2
  • 3
  • 4

在.NET中有两个Task类,一个是不返回参数的,其参数中使用的是Action委托
可返回结果的Task,其参数是使用的Func<Object,TResult>或者是不带参数的Func<TResult>
Task的异常
在Task的异步操作中可能会出现异常,若是使用Task<TResult>,在调用其Wait或Result方法时,异常就会抛出。若使用的Task,则可以调用wait方法,异常就会抛出。另外,不管是Task<TResult>还是Task,只要查询其Exception属性,异常也会抛出。
Task的异常会被存储在一个异常集合中,其名称为AggregateException。其内部有一个InnerExceptions 的属性,其定义如下:

public ReadOnlyCollection<Exception> InnerExceptions { get; }
  • 1

通过定义可以看到其返回结果是一个异常的集合类。因此,可以通过for循环,完成对每一个异常的处理
AggregateException还有一个Handle方法,该方法的定义如下:

public void Handle(Func<Exception,?bool> predicate)
  • 1

(写到这儿,不得不发出感慨!当初设计.NET的这帮人真他妈厉害!)
这个方法的作用就是为AggregateException内包含的每一个异常都调用一个回调方法
举例说明:

using System;using System.IO;using System.Threading.Tasks;public class Example{    public static void Main()    {        // This should throw an UnauthorizedAccessException.       try {           var files = GetAllFiles(@"C:\");           if (files != null)              foreach (var file in files)                 Console.WriteLine(file);        }        catch (AggregateException ae) {           //这儿就是一个遍历InnerExceptions循环,将内部所有的异常全部遍历,然后进行相应处理           //下面的代码比较简单,可通过判断是个什么异常,从而进行下一步的相关操作           foreach (var ex in ae.InnerExceptions)               //例如:               //if(ex is UnauthorizedAccessException)               //     doSomeThing();                   Console.WriteLine("{0}: {1}", ex.GetType().Name, ex.Message);        }        Console.WriteLine();        // This should throw an ArgumentException.        try {           foreach (var s in GetAllFiles(""))              Console.WriteLine(s);        }        catch (AggregateException ae) {           foreach (var ex in ae.InnerExceptions)               Console.WriteLine("{0}: {1}", ex.GetType().Name, ex.Message);        }    }    static string[] GetAllFiles(string path)    {       var task1 = Task.Run( () => Directory.GetFiles(path, "*.txt",                                                      SearchOption.AllDirectories));       try {          return task1.Result;       }       catch (AggregateException ae) {          //Handle是传入一个异常参数,返回一个bool的结果,若处理则返回true,否则返回false          //Handle方法应该是一个遍历方法,即通过InnerExceptions属性,为每一个异常添加这个回调方法          ae.Handle( x => { // Handle an UnauthorizedAccessException                            if (x is UnauthorizedAccessException) {                                Console.WriteLine("You do not have permission to access all folders in this path.");                                Console.WriteLine("See your network administrator or try another path.");                            }                            return x is UnauthorizedAccessException;                          });          return Array.Empty<String>();       }   }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59

Task的方法关于阻塞
Task类中有几个Wait方法:Wait、WaitAny、WaitAll。具体可参考MSDN的方法说明:具体的链接在这儿,这儿说明的是:
不管是哪个方法,调用这三者方法的任何一个,都会造成调用线程被阻塞,即等待task完成相关的操作。
这儿就有一个区别,之前join方法与wait方法同样都是让线程等待,但是内部如何实现及两者的区别
.NET中join与wait方法的区别
在.NET中join方法的源码如下:(查看.NET的源码的网址在这儿:查看.NET的源码网址

//Join方法的源代码[SecuritySafeCritical, HostProtection(SecurityAction.LinkDemand, Synchronization=true, ExternalThreading=true)]public void Join(){    this.JoinInternal();}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

JoinInternal的源代码未知,只知是window内核的方法

[MethodImpl(MethodImplOptions.InternalCall), SecurityCritical, HostProtection(SecurityAction.LinkDemand, Synchronization=true, ExternalThreading=true)]private extern void JoinInternal();
  • 1
  • 2

而wait方法的代码如下:

//其他的无关代码,关键的一行如下:Thread.SpinWait(Environment.ProcessorCount * (((int) 4) << i));
  • 1
  • 2

而SpinWait是也是window内核的方法,但是通过名称可以得知,其是一个自旋的等待。即代表占用CPU的资源,使得线程被挂起。
个人对join方法的理解,如下图所示:


如上图所示,当程序启动后,主线程会执行,如图中1所示,声明一个task后,task进行异步相关操作,如图2所示。当主线程到达t1.join()时,主线程机会挂起(即主线程不释放资源或同步锁,进入等待状态,此处不确定是用户模式等待还是内核模式等待)(如图3),然后task线程执行需要完成的操作(如图4),直到完成(如图5)。然后主线程才会执行剩余的内容(如图6)所示。
个人对wait方法的理解。
暂未找到.NET中关于wait方法的详细说明,因此有两个疑问:
1、调用线程(the calling thread)在阻塞时,是否释放同步锁[此处个人认为是不释放同步锁]
2、wait方法与join方法的差异
另外,wait方法很有意思,当调用线程遇到t.Wait()语句时, 调用线程并不是就傻傻的被阻塞了,而是先去看看当前线程是否开始执行,若当前线程尚未开始执行操作,则调用线程就会将当前线程将要执行的操作加载到调用线程中执行;若当前线程的操作已经开始执行了,那没办法,调用线程只能被阻塞。
MSDN上有一篇文章wait方法执行逻辑进行了详细说明,具体的网址在这儿
取消任务
取消任务是指使用协作式取消方式取消task。这个可以根据task的构造器,传入一个token。举例如下:

//构造一个取消操作对象private CancellationTokenSource cts = new CancellationSource();public static void Main(string[] args){    //定义一个具有返回结果类型的Task<TResult>    Task<int> t=new Task<int>(tempMethod,t.Token);    t.start();    //取消操作    cts.Cancel();    try    {        Console.WriteLine("返执行结果是:"+t.Result);    }    catch(AggregateException ex) //只要是使用了Task,则catch捕获的异常就应该使用AggregatException,而不是简单的使用Exception    {        ex.Handle(e=>e is OperationCanceledException);        Console.WriteLine("Sum操作已经完成")    }}//要符合Func委托private static int tempMethod(){    int resultInt=Sum(cts.Token,10000);}private static int Sum(CancellationToken ct,int n){    int sum=0;    for(;n>0;n--)    {        //若请求操作取消操作,则抛出OperationCanceledException        ct.ThrowIfCancellationRequest();        checked{sum+=n;}    }    return sum;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

当任务完成后,自动执行下一个任务
通过wait方法或者是task.Result属性获取操作最后的结果,这个是存在问题的。因为这样做会阻塞线程(Result属性的内部就是调用了wait方法)。因此为了避免因阻塞而导致的性能问题,.NET提供了一种回调机制,当线程完成操作时,就会调用callback方法。
特别说明,调用callback的线程是一个新的线程。
而实现callback的就是ContinueWith方法,ContinueWith方法的定义如下,更多的定义参见:MSDN中方法的定义

public Task ContinueWith(    Action<Task, object> continuationAction,    object state,    CancellationToken cancellationToken,    TaskContinuationOptions continuationOptions,    TaskScheduler scheduler)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

2、一个Task对象内部包含了一个ContinueWith的对象集合。即一个task对象可以声明多个继续的操作。例如:

    Task t = new Task(Action());    t.ContinueWith(task=>Console.WriteLine("操作1:Task的结果是"+t.Result));    t.ContinueWith(task=>Console.WriteLine("操作2:Task的结果是"+t.Result));    t.ContinueWith(task=>Console.WriteLine("操作3:Task的结果是"+t.Result));
  • 1
  • 2
  • 3
  • 4

当Task的操作完成后,线程池中会启用3个线程处理相应的回调方法。
3、ContinueWith方法中有一参数:TaskContinuationOptions,是指存在某些情况的下才调用,具体参见参数。一般而言,continuewith是通过创建一个独立的task完成回调方法的调用,但是这个选项中也可以有一个AttachedToParent选项,使其成为一个Task的子任务
创建子任务
因为task的构造器中只是定义了委托的定义,但没有规定符合委托的方法中是否可以创建task。因此可以在task内部创建子任务,但是在创建子任务的时候,需要明确子任务的创建方式,若是正常创建,则新任务将是独立的task。想成为task的子任务,可以在task的构造器中使用TaskCreationOptions.AttachedToParent属性。这样就相当于子任务与父任务绑定在一起,只有当所有的任务完成时,父任务才认为是完成了。举例如下:

//在定义的时候同步创建了3个子线程,并且用了TaskCreationOptions.AttachedToParent属性Task<int[]> parent = new Task<int[]>(    int[] results=new int[3];    new Task(()=>results[0]=Sum(100),TaskCreationOptions.AttachedToParent).start();    new Task(()=>results[0]=Sum(200),TaskCreationOptions.AttachedToParent).start();    new Task(()=>results[0]=Sum(300),TaskCreationOptions.AttachedToParent).start();    return results;);var ctw = parent.ContinueWith(    parentTask=> Array.ForEach(Console.WriteLine("结果为:"+parentTask.Result)););parent.Start(); 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

任务内部/小节
1、Task所使用的线程默认是创建新线程,而不是使用线程池线程。因此,其资源的占用和消耗要大于ThreadPool.QueueUserWorkItem
2、Task实现了IDispose接口,因此在用完task时要调用dispose,以释放资源。而不要使用GC回收
3、每个Task有唯一ID,可通过CurrentID获取,而CurrentID是一个可控类型的Int32
4、Task在生命周期中会有几个状态:
4.1 Createed //任务显示的创建完。可使用start方法,手动开始这个任务
4.2 WaitingForActivation //任务隐式创建。会自动开始。例如,通过continuewith开始的任务
4.3 WaitingToRun //已经进入调度,尚未开始
4.4 Runing
4.5 WaitingForChildrenToComplete
4.6 task最终的结果为:
4.6.1 RanToCompletion
4.6.2 Cancelled
4.6.3 Faulted
5、可以通过task的Status属性获取task的状态。同时Task提供了几个属性:
IsCanceled、IsCompleted、IsFaulted判断task的状态。但是有一个特殊情况:当Task的状态为RanToCompletion、Cancelled、Faulted中的任意状态时,调用Task的IsCompleted属性,其都将返回true。因此判断一个task是正常的完成的方式是:

if(task.Status==TaskStatus.RanToCompletion){    //.....}
  • 1
  • 2
  • 3
  • 4

任务工厂
有时候会遇到一种情况,用相同的配置创建多个Task。一种方法是挨个创建task;而另外一个方法就是使用任务工厂。(个人认为任务工厂使用的机会不会很多)
任务工厂同Task,也有两种方式,TaskFactory、TaskFactory<TResult>
可以通过TaskFactory的构造器创建TaskFactory实例,也可以使用Task类的TaskFactory属性进行创建,一般情况是使用后者。
TaskFactory具体的使用方法,可以参考MSDN的资料:TaskFactory
任务调度器
1、.NET中有多个类型的任务调度器,其中线程池使用的是“线程池线程任务调度器”,而GUI(WinForm、WPF、SilverLight)使用的则是“同步上下文任务调度器”,而通过不同的任务调度器所创建的task,其不允许相互操作。因此使用“线程池线程任务调度器”所创建的线程,不能操作界面(改变标题之类的操作),否则就会爆出InvalidOperationException
但是若希望线程池中的操作可以操作GUI上的元素,则其需通过TaskScheduler.FromCurrentSynchronizationContext()方法获得“同步上下文任务调度器”,在创建task时,将其作为参数传入。书中的例子很好,如下:

internal sealed class MyFrom:Form{    //Form的构造器,初始化标题等内容    public MyFrom(){        this.text="同步上下文任务调度器demo";        visible=true;        width=400;        height=400;    }    //通过TaskScheduler获得当前Form的“同步上下文任务调度器”    private readonly TaskScheduler m_syncContextTaskScheduler=        TaskScheduler.FromCurrentSynchronizationContext();    private CancellationTokeSource cts;    //重写鼠标的点击方法    protected override void OnMouseClick(MouseEventArgs e){        if(cts!=null){            cts.cancel();            cts=null;        }else{            text="操作开始";            cts = new CancellationTokeSource();            //下面的这个task使用线程池的任务调度器,也就是默认的任务调度器              var task = new task(()=>Sum(cts.Token,2000),cts.Token);            task.ContinueWith(                t=>Text="结果为"+t.Result,                CancellationToken.None,                TaskContinuationOptions.OnlyOnRanToCompletion,                syncContextTaskScheduler            );            task.ContinueWith(                t=>Text="操作被取消",                CancellationToken.None,                TaskContinuationOptions.OnlyOnCanceled,                syncContextTaskScheduler            );            task.ContinueWith(                t=>Text="操作失败",                CancellationToken.None,                TaskContinuationOptions.OnlyOnFaulted,                syncContextTaskScheduler            );        }//else结束        base.OnMouseClick(e);    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52

代码简单易懂,有一点很有意思。就是传入的参数中有:CancellationToken.None,这个很有意思。其本质的想法就是,我创建的这个操作,不想被外界的cancel方法所取消,但是方法中还必须有一个这个参数,因此就使用CancellationToken.None。这个CancellationToken.None会返回一个CancellationToken,因其与任何的CancellationTokenSource没有任何关系,因此操作也就不能被取消了
26.4读书思考
1、task还是挺耗费资源的,但是又比Thread、ThreadPool等好用,没办法只能使用它
2、在26.4遇到一个问题,join与wait方法区别,有待解答!

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
[深入学习C#]C#实现多线程的方式:Task
委托与线程的见解(下)——线程
C#中创建线程的四种方式
C#之Task.ContinueWith使用实例
C#线程系列讲座(1):BeginInvoke和EndInvoke方法
C# 多线程(菜鸟教程及爱整理)
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服