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

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

//更新 郑林 2018-3-20 15:55:45
//更新备注:可以传递参数,并带返回结果的Task。更容易理解。

//不带参数,不返回结果的Task实例
private void button1_click(object sender, EventArgs e)
{
    //创建Task任务
    Task<int> deliveryTask = new Task<int>(deliveryTaskOperate, deliveryList);

    //call-back-回调方法
    deliveryTask.ContinueWith(deliverySuccessCallBack, System.Threading.CancellationToken.None, TaskContinuationOptions.OnlyOnRanToCompletion, m_syncContextTaskScheduler);
    deliveryTask.ContinueWith(deliveryFailedCallBack, System.Threading.CancellationToken.None, TaskContinuationOptions.NotOnRanToCompletion, m_syncContextTaskScheduler);

    //开始
    deliveryTask.Start();
}        

//Task的具体任务     
 private int deliveryTaskOperate(object passDataObject)
{
    int delivertResult = 0;
    //将object进行转换为所需的类型
    List<DrugDeliveryModel> tempDeliveryData = (List<DrugDeliveryModel>)passDataObject;

    delivertResult = drugDispensingLogic.delivery(tempDeliveryData);
    //返回Task的int类型结果
    return delivertResult;
}

        //成功的回调方法
private void deliverySuccessCallBack(Task<int> status)
{
    //获取Task的返回结果
    int taskResult= status.Result;

}

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

private static int Sum(int n)
{
    int sum=0;
    for(;n>0;n--)
        checked{sum += n;}
    return sum;
}

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

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

在.NET中有两个Task类,一个是不返回参数的,其参数中使用的是Action委托
可返回结果的Task,其参数是使用的Func<Object,TResult>或者是不带参数的Func<TResult>

Task的异常

//更新备注:Task异常抛出的时机说明
//更新:郑林 2018-3-20 16:34:02

Task异常的抛出一般的不同。
一般情况的异常捕获时,是自动捕获的:

try{

}catch{
//异常会自动捕获
}

但是Task的异常不是自动抛出的。当满足条件时才抛出异常。
1、若是使用Task<TResult>,在调用其Wait或Result方法时
2、若使用的Task,则可以调用wait方法
3、查询其Exception属性

所以上面myTask的例子:

        try
                {
                    //创建Task任务
                    Task<int> deliveryTask = new Task<int>(deliveryTaskOperate, deliveryList);

                    //call-back
                    deliveryTask.ContinueWith(deliverySuccessCallBack, System.Threading.CancellationToken.None, TaskContinuationOptions.OnlyOnRanToCompletion, m_syncContextTaskScheduler);
                    deliveryTask.ContinueWith(deliveryFailedCallBack, System.Threading.CancellationToken.None, TaskContinuationOptions.NotOnRanToCompletion, m_syncContextTaskScheduler);

                    //task启动
                    deliveryTask.Start();

            //满足以上面提到的3条条件时,才会抛出异常
                    int deliveryResult = deliveryTask.Result;
                }
                catch (AggregateException ex)
                {
                    StringBuilder sb = new StringBuilder("异常信息:");
                    foreach (var item in ex.InnerExceptions)
                    {
                        sb.Append("异常类型:" + item.GetType().Name + "。异常具体信息:" + item.Message);
                    }
                    MessageBox.Show(sb.ToString());
                }

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

public ReadOnlyCollection<Exception> InnerExceptions { get; }

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

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

(写到这儿,不得不发出感慨!当初设计.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>();
       }
   }
}

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();
}

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

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

而wait方法的代码如下:

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

而SpinWait 是window内核的方法 (这儿有误,Thread下的方法不可能是内核方法,一定是用户模式下的自旋方法),但是通过名称可以得知,其是一个自旋的等待。即代表占用CPU的资源,使得线程被挂起。

个人对join方法的理解,如下图所示:
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;
}

当任务完成后,自动执行下一个任务
通过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
)

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));

当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[1]=Sum(200),TaskCreationOptions.AttachedToParent).start();
    new Task(()=>results[2]=Sum(300),TaskCreationOptions.AttachedToParent).start();

    return results;
);

var ctw = parent.ContinueWith(
    parentTask=> Array.ForEach(Console.WriteLine("结果为:"+parentTask.Result));
);

parent.Start(); 

任务内部/小节
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)
{
    //.....
}

任务工厂
有时候会遇到一种情况,用相同的配置创建多个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);
    }
}

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值