1、简介

为什么MS要推出Task,而不推Thread和ThreadPool,以下是我的见解:

(1)、Thread的Api并不靠谱,甚至MS自己都不推荐,原因,它将整个Thread类都不开放给Windows Sotre程序,且它的Api过于强大,如果在程序中过度使用,维护的成本太高,想想代码中充斥着挂起线程,阻塞线程、后期的应用程序很难维护.

(2)、ThreadPool最大的问题是,所有的辅助线程都是异步的,没有向Thread的Join方法那样去等待一个线程执行完,然后执行回调函数的机制,也就是你无法判断线程什么时候执行完,也没有机制获得线程的返回值,所有MS推出了Task来解决Thread和ThreadPool的问题

当然最主要的是,Thread和Thread好用.因为Task是它们的升级版,升级版当然比较好.

2、Task的缺点

虽然Task以其强大的Api,以及封装,让我们在CLR环境下,能完成高效率的编程,但是它并不是没有缺点的,高效率的背后,肯定带来的性能的损失,这一点很多类似的框架都能说明,比如EF,强大的背后,大量的使用了反射等操作,所以虽然开发效率提升了,但是性能却下降了,这里不想说太多,所以简单的api可能不会产生过多的性能损耗,所以这也是为什么大型互联网项目,更愿意使用原生Ado或者Dapper去做.所以这些在我们的实际开发中,这些都需要我们去权衡.有得必有失.下面来简单的说下Task具体在哪里会产生性能损失:

很直观,直接分析ThreadPool类和Task类的构造:

ThreadPool类

很简洁,没有任何的字段和属性!

Task类,1700行代码,里面有大量的字段和属性,大致如下:

还包括对父任务的引用、任务调度器(TaskScheduler)的引用、对回调方法的引用、对执行上下文(ExecutionContext)的引用、对ManualResetEventSlim信号量的引用、还有CancellationToken取消信号量(我把它理解为信号量)的引用、一个ContinueWithTask的任务集合的引用、还有未抛出异常的Task对象集合的引用等等,这些后面的文章都会介绍.

所以,不分析具体的性能损耗点,但是单单两个类的构造,你就能清楚使用那个类创建线程所产生的性能消耗大.

3、实战

(1)、不带返回值,实现和ThreadPool线程池线程一样的效果

        static void Main(string[] args)
{
var result=Task.Run(() => Calculate("这个参数很六啊"));
Console.WriteLine("主线程有没有在继续执行,look look");
Console.ReadKey();
} static void Calculate(string param)
{
Console.WriteLine("子线程开始执行,带着主线程给它传递的参数呢!参数是:{0}",param);
Thread.Sleep();
Console.WriteLine("子线程执行完了");
}

根据输出,发现主线程并没有等带子线程执行完毕,通过开启一个新线程之后,立刻返回去执行它自己的任务.

(2)、带返回值

        static void Main(string[] args)
{
var result=Task.Run(() => Calculate());
Console.WriteLine(result.Result);
Console.WriteLine("主线程有没有在继续执行,look look");
Console.ReadKey();
} /// <summary>
/// 简单递归计算n+(n-1)+.....+1
/// </summary>
/// <param name="param"></param>
/// <returns></returns>
static int Calculate(int param)
{
if (param == )
return ;
return param + Calculate(param - );
}

无论给Calculate方法传递的参数多小,主线程都等待子线程返回结果后,在继续执行它的任务.所以可以得出结论.调用子线程返回值的Result属性

相等于调用了Wait方法,当然Task确实提供了这个实例方法,但是使用Result属性一样有这个效果.主线程会等待子线程执行完毕在执行它的任务.

(3)、关于Task的小要点

当主线程通过Task开启了一个子线程之后,返回做自己的事情,当它执行到Wait方法,这个时候主线程会阻塞,CPU的执行速度很快,所以它会去判断子线程有没有开始执行,如果没有执行,那么它会自己去做子线程的任务,而不是开启一个新的线程去做.这样就节约了系统资源.这样就不会存在线程阻塞的情况.所有事情都由主线程干完.

(4)、关于简单的死锁问题

一般死锁的产生,都是多线程争用相同的资源导致的.下面就来重现一下.

        private static object lockObj = new object();
static void Main(string[] args)
{
var result=Task.Run(() => Calculate());
lock (lockObj)
{
Console.WriteLine("主线程这个时候争用了lockObj锁,并执行子线程");
Console.WriteLine(result.Result);
}
Console.WriteLine("主线程有没有在继续执行,look look");
Console.ReadKey();
} /// <summary>
/// 简单递归计算n+(n-1)+.....+1
/// </summary>
/// <param name="param"></param>
/// <returns></returns>
static int Calculate(int param)
{
lock (lockObj)
{
Console.WriteLine("子线程这个时候也去争用lockObj锁,发现主线程已经争用了这个锁,那么它等待主线程释放这个锁,但是主线程正等待它执行完!");
Console.WriteLine("好了,这个时候就发生了死锁现象.主线程等子线程执行完,子线程等主线程释放lockObj锁,两个线程在相互等待,死锁了");
}
if (param == )
return ;
return param + Calculate(param - );
}

光标一直在那闪啊闪,好吧,那就都等着吧.谁都执行不下去了.

解决办法很简单.在创建一个新的锁,这个就不代码演示了.

(5)、取消Task创建的子线程

取消Task创建的线程和取消ThreadPool创建的子线程一样,通过CancellationTokenSource类实现,代码如下:

            var cancellationSource = new CancellationTokenSource();
cancellationSource.Cancel();
try {
Task.Run(() => ChildThread(cancellationSource.Token));
}
catch(AggregateException ex)
{
//处理子线程抛出的异常
ex.Handle((x) => x is OperationCanceledException);
}
Console.WriteLine("主线程继续做它的事情");
Console.ReadKey();
} /// <summary>
/// 子线程
/// </summary>
static void ChildThread(CancellationToken token)
{
token.ThrowIfCancellationRequested();
Console.WriteLine("子线程做完了它的事情");
}

(6)、任务完成时启动新的任务 ContinueWith

当使用Task进行多线程任务开发时,不建议使用Wait方法或者Result属性,去阻塞主线程,原因如下:

i、会卡界面

ii、伸缩性好的软件,不会这么做,除非迫不得已

iii、很有可能创建新的线程,浪费资源(如果主线程执行的足够快,它可能自己去完成子线程的任务,而不是创建新的线程)

代码如下:

        static void Main(string[] args)
{
//开启一个子线程进行计算操作
var watch = Stopwatch.StartNew();
Task<int> task=Task.Run(() => ChildThreadOne());
//当子线程一计算完毕之后,开启一个新的线程去执行输出子线程一的结果,这里新的线程不会阻塞
//只有当子线程完成计算输出后,它才会开启,并输出子线程的值
//所以该程序并不会发生线程阻塞的情况
task.ContinueWith(x =>
{
watch.Stop();
Console.WriteLine("输出子线程一的返回值:{0},耗时:{1}", task.Result, watch.ElapsedMilliseconds / );
}); Console.WriteLine("主线程继续做它的事情");
Console.ReadKey();
} /// <summary>
/// 子线程一
/// </summary>
/// <returns></returns>
static int ChildThreadOne()
{
Thread.Sleep();//模拟长时间运算
return ;
}

这里注意两点:

(1)、这里ContinueWith会检测到子线程完成之后,立即启动一个新的线程去显示结果.不会存在子线程还没有完成计算的情况下,输出一个空值,或者发生异常,这一点,CLR能保证.

(2)、这里ContinueWith会返回一个Task对象示例,所以可以调用Wait方法,或者Result属性,单一般不建议这么做,还是那句话会阻塞线程.一般都忽略这个Task实例,所以需要谨慎使用.

        static void Main(string[] args)
{
Task<int> task = Task.Run(() => ChildThreadOne());
var t1=task.ContinueWith((x) => ChildOneContinueOne(task.Result));
var t2=task.ContinueWith((x) => ChildOneContinueTwo(task.Result));
t1.ContinueWith(x => { Console.WriteLine("输出子线程一的计算结果加10后的结果值:{0}", t1.Result); });
t2.ContinueWith(x => { Console.WriteLine("输出子线程一的计算结果乘10后的结果值:{0}", t2.Result); });
Console.WriteLine("主线程继续做它的事情");
Console.ReadKey();
} /// <summary>
/// 子线程一
/// </summary>
/// <returns></returns>
static int ChildThreadOne()
{
Thread.Sleep();//模拟长时间运算
return ;
} /// <summary>
/// 在子线程一完成计算后,开启一个新的线程对子线程一的结果进行+66操作
/// </summary>
/// <param name="childOneResult"></param>
/// <returns></returns>
static int ChildOneContinueOne(int childOneResult)
{
Console.WriteLine("ChildOneContinueOne线程拿到的子线程一的结果值为{0}", childOneResult);
Thread.Sleep();//模拟长时间计算任务
return + childOneResult;
} /// <summary>
/// 在子线程一完成计算后,开启一个新的线程对子线程一的结果进行乘66操作
/// </summary>
/// <param name="childOneResult"></param>
/// <returns></returns>
static int ChildOneContinueTwo(int childOneResult)
{
Console.WriteLine("ChildOneContinueTwo线程拿到的子线程一的结果值为{0}", childOneResult);
Thread.Sleep();//模拟长时间计算任务
return * childOneResult;
}

用ContinueWith做了一件有趣的事情,大致思路是我们在开发过程中会遇到,到我们拿到一个线程的返回值后,立即开启两个新的线程去做两个方向的任务,如下图:

这在开发中经常使用,整个过程没有任务阻塞线程.暂时没有发现多线程争用问题.

原理浅析:

Task对象实例包含一个ContinueWith任务的一个集合,所以可以使用Task对象多次调用ContinueWith方法(就像上面的代码一样),所有的线程都会进入线程池的队列中,当Task任务执行完毕,线程池回依次调用它们.

(2)、使用ContinueWith中产生的特殊情况

当子线程发生异常、取消、或者超时时,这个时候就要告诉线程池如何处理唤起线程,而不是无视,子线程的异常,所以MS给ContinueWith提供了一个TaskContinuationOptions枚举,来处理这个问题.下面介绍几个常用的.

TaskContinuationOptions.OnlyOnRanToCompletion 主要当前面的任务,完美的完成任务,才能执行延续任务.

 class Program
{
static void Main(string[] args)
{
Task<int> task = Task.Run(() => ChildThreadOne());
task.ContinueWith(t => Console.WriteLine("子线程一的延续任务,只有在子线程一完美的完成的任务的情况下,才会执行"), TaskContinuationOptions.OnlyOnRanToCompletion);
Console.WriteLine("主线程继续执行它的操作");
Console.ReadKey();//必须加这行代码,因为Task时线程池线程,属于后台线程
} /// <summary>
/// 子线程一
/// </summary>
static int ChildThreadOne()
{
Thread.Sleep();//模拟执行长时间计算任务
Console.WriteLine("子线程一完成了计算任务,返回值6");
return ;
}
}

这里,看着,让子线程一抛出异常,看看延续任务会不会继续执行.

        static void Main(string[] args)
{
Task<int> task = Task.Run(() => ChildThreadOne());
task.ContinueWith(t => Console.WriteLine("子线程一的延续任务,只有在子线程一完美的完成的任务的情况下,才会执行"), TaskContinuationOptions.OnlyOnRanToCompletion);
Console.WriteLine("主线程继续执行它的操作");
Console.ReadKey();//必须加这行代码,因为Task时线程池线程,属于后台线程
} /// <summary>
/// 子线程一
/// </summary>
static int ChildThreadOne()
{
Thread.Sleep();//模拟执行长时间计算任务
Console.WriteLine("子线程一完成了计算任务,返回值6");
throw new Exception("模拟抛出异常");
}

因为子线程一抛出了异常,所以延续任务没有执行.这里取消线程,也不会执行延续任务,因为MS为了区分Task的任务完成和任务取消,选择让取消的任务抛出OperationCanceledException异常,所以和抛出简单一样,延续任务并不会执行.超时同理.

TaskContinuationOptions.OnlyOnFaulted 当前面的任务抛出未处理的异常是,执行延续任务.

        static void Main(string[] args)
{
CancellationTokenSource source = new CancellationTokenSource();
Task<int> task = Task.Run(() => ChildThreadOne(source.Token));
task.ContinueWith(t =>
{
Console.WriteLine("子线程一的延续任务,只有在子线程一抛出了未处理的异常,才会执行,这里尝试处理抛出的异常");
//一般记日志,Logger.Error("");
task.Exception.Handle(x =>
{
Console.WriteLine("最好在这里就处理掉异常,以免让外部try canth捕获到,并处理产生的性能损失");
if (x is OperationCanceledException)
{
Console.WriteLine("子线程一抛出了取消异常,异常信息为{0}", x.Message);
}
else {
Console.WriteLine("子线程一抛出了一般异常,异常信息为{0}", x.Message);
} return true;//返回true,告诉CLR异常已被处理,这样外部try catch就捕获不到了.
});
}, TaskContinuationOptions.OnlyOnFaulted);
Console.WriteLine("主线程继续执行它的操作");
source.Cancel();
Console.ReadKey();//必须加这行代码,因为Task时线程池线程,属于后台线程
} /// <summary>
/// 子线程一
/// </summary>
static int ChildThreadOne(CancellationToken cancellation)
{
Thread.Sleep();//模拟执行长时间计算任务
Console.WriteLine("子线程一完成了计算任务,返回值6");
cancellation.ThrowIfCancellationRequested();//抛出取消异常
return ;
}

这里建议对TaskContinueWith做一个封装,让它能处理不同的异常,并且这样异常,能在内部就被全部处理掉,而不需要在外部进行try catch处理,并且有一个友好的异常记录和输出.这里我就不做了,有需要的可以联系我.

ok,这里就介绍TaskContinuationOptions常用的两个值,其余的用法都差不多,可以看Ms的提供的备注,或者参看MSDN,这里就不全介绍了.

C# 多线程五之Task(任务)一的更多相关文章

  1. C# 多线程六之Task(任务)三之任务工厂

    1.知识回顾,简要概述 前面两篇关于Task的随笔,C# 多线程五之Task(任务)一 和 C# 多线程六之Task(任务)二,介绍了关于Task的一些基本的用法,以及一些使用的要点,如果都看懂了,本 ...

  2. 重新想象 Windows 8 Store Apps (43) - 多线程之任务: Task 基础, 多任务并行执行, 并行运算(Parallel)

    [源码下载] 重新想象 Windows 8 Store Apps (43) - 多线程之任务: Task 基础, 多任务并行执行, 并行运算(Parallel) 作者:webabcd 介绍重新想象 W ...

  3. C#多线程实现方法——Task/Task.Factary

    原文:C#多线程实现方法--Task/Task.Factary Task 使用 Task以及Task.Factory都是在.Net 4引用的.Task跟Thread很类似,通过下面例子可以看到. st ...

  4. 多线程(五) java的线程锁

    在多线程中,每个线程的执行顺序,是无法预测不可控制的,那么在对数据进行读写的时候便存在由于读写顺序多乱而造成数据混乱错误的可能性.那么如何控制,每个线程对于数据的读写顺序呢?这里就涉及到线程锁. 什么 ...

  5. .net 多线程 Thread ThreadPool Task

    先准备一个耗时方法 /// <summary>/// 耗时方法/// </summary>/// <param name="name">< ...

  6. c#中@标志的作用 C#通过序列化实现深表复制 细说并发编程-TPL 大数据量下DataTable To List效率对比 【转载】C#工具类:实现文件操作File的工具类 异步多线程 Async .net 多线程 Thread ThreadPool Task .Net 反射学习

    c#中@标志的作用   参考微软官方文档-特殊字符@,地址 https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/toke ...

  7. 转载 .Net多线程编程—任务Task https://www.cnblogs.com/hdwgxz/p/6258014.html

    .Net多线程编程—任务Task   1 System.Threading.Tasks.Task简介 一个Task表示一个异步操作,Task的创建和执行是独立的. 只读属性: 返回值 名称 说明 ob ...

  8. {python--GIL锁}一 介绍 二 GIL介绍 三 GIL与Lock 四 GIL与多线程 五 多线程性能测试

    python--GIL锁 GIL锁 本节目录 一 介绍 二 GIL介绍 三 GIL与Lock 四 GIL与多线程 五 多线程性能测试 一 背景知识 ''' 定义: In CPython, the gl ...

  9. 异步多线程 Thread ThreadPool Task

    一.线程 Thread ThreadPool 线程是Windows任务调度的最小单位,线程是程序中的一个执行流,每个线程都有自己的专有寄存器(栈指针.程序计数器等),但代码区是共享的,即不同的线程可以 ...

随机推荐

  1. DC画线

    CClientDC hdc(this);//获取DC CPen pen(PS_SOLID,4,RGB(255,0,0));//创建一支红笔 CPen * pOldPen=hdc.SelectObjec ...

  2. Ng第八课:神经网络表述(Neural Networks: Representation)

    8.1  非线性假设 8.2  神经元和大脑 8.3  模型表示 1 8.4  模型表示 2 8.5  特征和直观理解 1 8.6  样本和直观理解 II 8.7  多类分类 8.1  非线性假设 无 ...

  3. python基础回顾

    1.第二个缺点就是代码不能加密.如果要发布你的Python程序,实际上就是发布源代码,这一点跟C语言不同,C语言不用发布源代码,只需要把编译后的机器码(也就是你在Windows 上常见的xxx.exe ...

  4. bootstrap4相关文档

    本节课我们主要学习一下Bootstrap的两个个组件功能:输入框组件和导航导航条组件. 一.输入框组件 文本输入框就是可以在<input>元素前后加上文字或按钮,可以实现对表单控件的扩 展 ...

  5. 分频器的verilog设计

    笔者最近由于实验室老师的任务安排重新又看了一下分频器的verilog实现,现总结如下,待以后查看之用(重点是查看计数器计到哪个值clk_out进行状态翻转) 1.偶数分频占空比为50% 其实质还是一个 ...

  6. Swift3 使用系统UIAlertView方法做吐司效果

    /** *显示弹出信息 */ class func showAlertMessage(_ str:String,showtime Num:Double){ let alert = UIAlertVie ...

  7. 关于Java-枚举的总结

    枚举 枚举的定义 枚举也是JDK5.0的新特性. JDK5.0加入了一个全新类型的“类”——枚举类型. 为此引入了一个新的关键字enum. 可以这样来定义一个枚举类型: public enum Col ...

  8. 使用UTL_HTTP时遭遇ORA-29273

    http://blog.itpub.net/8520577/viewspace-1295182/ 项目中需要使用utl_http访问webserivce.使用utl_http时报错ORA-29273. ...

  9. ?js调用PHP里的变量,怎么弄?

    js调用PHP里的变量,怎么弄 网上给的例子都是js文件里一开始先给这个变量一个值,要是那样有啥意思啊,我要的就是可以变化的. hychyc_2008 | 浏览 2741 次  2013-04-18 ...

  10. 加密算法(DES,AES,RSA,MD5,SHA1,Base64)比较和项目应用(转载)

    加密技术通常分为两大类:"对称式"和"非对称式". 对称性加密算法:对称式加密就是加密和解密使用同一个密钥.信息接收双方都需事先知道密匙和加解密算法且其密匙是相 ...