C#的变迁史04 - C# 4.0 之多线程篇

  在.NET 4.0中,并行计算与多线程得到了一定程度的加强,这主要体现在并行对象Parallel,多线程Task,与PLinq。这里对这些相关的特性一起总结一下。

  使用Thread方式的线程无疑是比较麻烦的,于是在这个版本中有了改善的版本Task。除了运行效率等方面的提升,Task还与并行计算紧紧联系在了一起,这些线程充分的利用了多核的优势,在一定的场合下,大幅的提高了程序的运行效率。屏蔽了运行细节的Task和Parallel方式使得程序员们完全不用编写任何针对多核的程序,只需要使用标准的类库完成任务就可以了,其它的CLR会去处理。

这一篇中先看第一个利器:多线程Task。

  Task类为把线程类进行改良,使之使用起来更简便,更加容易。要想开启一个新的线程执行任务,只要调用Task.Factory.StartNew方法就可以了,执行完这个语句后线程就开始运行了。当然了,使用new初始化一个Task,然后适时调用其Start方法开始运行也是很不错的一个选择。

复制代码

using System;
using System.Threading.Tasks;class Program
{static void Main(string[] args){    var task = Task.Factory.StartNew(() =>{for (int i = 0; i < 100; i++) { Console.Write('B'); }});task.ContinueWith(t =>{Console.WriteLine();Console.WriteLine("sub task {0} done", t.Id);});for (int i = 0; i < 100; i++) { Console.Write('A'); }task.Wait();}
}

复制代码

  注意最后的task.Wait(),调用这个方法是等待子线程执行结束,当需要等待子线程结果的时候,它最有用。

  task对象还有很多有用的方法,从它们的名字就可以知道它们各自的用途了,其中实例方法如上面的ContinueWith方法,它会在task执行完毕后执行其参数指定的行为;静态方法如WaitAny,WaitAll等,它们指定了在执行多个task时主线程的等待条件。

  对于多线程编程来说,启动线程并等待其自然结束是最常见的一种应用,处理也比较简单。相比而言,线程的中途终止和异常的处理要麻烦的多,难以预计的隐藏bug会出现在线程程序中。

线程的终止类型

  线程执行的任务结束以后,线程就正常结束了。这里线程任务结束通常有3种情况:任务正常执行完,任务被取消,任务被异常打断结束。查询Task结束时的状态类型就是调用相关的属性,如下面的例子:

复制代码

static void Main(string[] args)
{Task t = new Task(() =>{Console.WriteLine("任务开始工作……");//模拟工作过程Thread.Sleep(5000);});t.Start();t.ContinueWith((task) =>{Console.WriteLine("任务完成,完成时候的状态为:");Console.WriteLine("IsCanceled={0}\tIsCompleted={1}\tIsFaulted={2}", task.IsCanceled, task.IsCompleted, task.IsFaulted);});Console.ReadKey();
}

复制代码

  Task对象的三个属性IsCompleted,IsCanceled,IsFaulted就是查询线程的执行情况,不过需要注意,只要线程结束了,不管是以什么方式,IsCompleted始终返回true。IsCanceled代表程序被主动取消了,IsFaulted代表线程出现异常被动结束,这两个值都为false,代表线程是正常执行完结束的。

  正常结束的情况比较简单,这里就不多说了,这里看一下子线程任务的取消问题。这在编程中是很常见的一个需求,启动了一个线程以后,发现某些条件具备了,就不需要线程继续运行了,这个时候就需要取消线程任务。

主动取消/中止线程的标准做法

  在以前的版本中,我基本上是都通Thread的Abort方法强行的中止线程。在C# 4.0中,标准的取消一个线程任务的做法是使用协作式取消(Cooperative Cancellation)。协作式取消的机制是,如果线程需要被停止,那么线程自身就得负责开放给调用者这样的接口:Cancled,然后线程在工作的同时,不断以某种频率检测Cancled标识(通常是把任务主体包装到循环中),若检测到Cancled,线程自己负责退出。

下面是一个最基础的协作式取消的样例:

复制代码

// 设定取消标识
CancellationTokenSource cts = new CancellationTokenSource();
Thread t = new Thread(() =>{while (true){// 检查取消标识if (cts.Token.IsCancellationRequested){Console.WriteLine("线程被终止!");break;}Console.WriteLine(DateTime.Now.ToString());Thread.Sleep(1000);}});t.Start();
Console.ReadLine();
// 主线程申请取消
cts.Cancel();

复制代码

  调用者使用CancellationTokenSource的Cancle方法通知工作线程退出。工作线程则以一定的的频率一边工作,一边检查是否有外界传入进来的Cancel信号。若有这样的信号,则负责退出。可以看到,在正确停止线程的机制中,真正起到主要作用的是线程本身,它负责检测相关信号并退出。

  协作式取消中的关键类型是CancellationTokenSource。它有一个关键属性Token,Token是一个名为CancellationToken的值类型。CancellationToken继而进一步提供了布尔值的属性IsCancellationRequested作为需要取消工作的标识。CancellationToken还有一个方法尤其值得注意,那就是Register方法。它负责传递一个Action委托,在线程停止的时候被回调,使用方法如:

cts.Token.Register(() =>
{Console.WriteLine("工作线程被终止了。");
});

  而且Task对象对CancellationTokenSource对象是天生支持的,在构造Task对象的时候就可以传进去CancellationTokenSource的实例。看一个网上的小例子:

复制代码

static void Main(string[] args)
{CancellationTokenSource cts = new CancellationTokenSource();Task<int> t = new Task<int>(() => Add(cts.Token), cts.Token);t.Start();t.ContinueWith(TaskEnded);//等待按下任意一个键取消任务Console.ReadKey();cts.Cancel();Console.ReadKey();
}static void TaskEnded(Task<int> task)
{Console.WriteLine("任务完成,完成时候的状态为:");Console.WriteLine("IsCanceled={0}\tIsCompleted={1}\tIsFaulted={2}", task.IsCanceled, task.IsCompleted, task.IsFaulted);Console.WriteLine("任务的返回值为:{0}", task.Result);
}static int Add(CancellationToken ct)
{Console.WriteLine("任务开始……");int result = 0;while (!ct.IsCancellationRequested){result++;Thread.Sleep(1000);}return result;
}

复制代码

  不过需要注意,像上面这么写,使用ct.IsCancellationRequested判断一下,如果取消信号被设定了,则退出任务,这种情况CLR会认为是成功结束的,这切实反应了程序员的期望,IsCanceled会返回false。如果想出现IsCanceled为true的情况,那么程序就要改写成:

复制代码

static void Main(string[] args)
{CancellationTokenSource cts = new CancellationTokenSource();Task<int> t = new Task<int>(() => AddCancleByThrow(cts.Token), cts.Token);t.Start();t.ContinueWith(TaskEndedByCatch);//等待按下任意一个键取消任务Console.ReadKey();cts.Cancel();Console.ReadKey();
}static void TaskEndedByCatch(Task<int> task)
{Console.WriteLine("任务完成,完成时候的状态为:");Console.WriteLine("IsCanceled={0}\tIsCompleted={1}\tIsFaulted={2}", task.IsCanceled, task.IsCompleted, task.IsFaulted);try{Console.WriteLine("任务的返回值为:{0}", task.Result);}catch (AggregateException e){e.Handle((err) => err is OperationCanceledException);}
}static int AddCancleByThrow(CancellationToken ct)
{Console.WriteLine("任务开始……");int result = 0;while (true){ct.ThrowIfCancellationRequested();result++;Thread.Sleep(1000);}return result;
}

复制代码

  在任务结束求值的方法TaskEndedByCatch中,如果任务是通过ThrowIfCancellationRequested方法结束的,对任务求结果值将会抛出异常OperationCanceledException,而不是得到抛出异常前的结果值。这意味着任务是通过异常的方式被取消掉的,所以可以注意到上面代码的输出中,状态IsCancled为true。同时你会发现IsFaulted状态却还是等于false。这是因为ThrowIfCancellationRequested是协作式取消方式类型CancellationTokenSource的一个方法,CLR进行了特殊的处理。CLR知道这一行程序开发者有意为之的代码,所以不把它看作是一个异常(它被理解为取消)。要得到IsFaulted等于true的状态,自己手动在一个地方抛出一个异常试试就可以了。

  此外,CancellationTokenSource就是可以被多个Task共享的,这样可以取消一组任务。取消一组任务最简单的就是使用任务工厂。任务工厂支持多个任务之间共享相同的状态,如取消类型。通过使用任务工厂,可以同时取消一组任务:

复制代码

static void Main(string[] args)
{CancellationTokenSource cts = new CancellationTokenSource();//等待按下任意一个键取消任务TaskFactory taskFactory = new TaskFactory();Task[] tasks = new Task[]{taskFactory.StartNew(() => Add(cts.Token)),taskFactory.StartNew(() => Add(cts.Token)),taskFactory.StartNew(() => Add(cts.Token))};//CancellationToken.None指示TasksEnded不能被取消taskFactory.ContinueWhenAll(tasks, TasksEnded, CancellationToken.None);Console.ReadKey();cts.Cancel();Console.ReadKey();
}static void TasksEnded(Task[] tasks)
{Console.WriteLine("所有任务已完成!");
}

复制代码

  好了,看完线程的正常取消,再来看一下线程的异常问题,这个在上面也简单说了一下,这是一种程序员不期望的线程结束的方式。

线程的异常处理

  先看下面的例子:

Task.Factory.StartNew(() =>
{throw new Exception();
});

   运行这段程序,你会发现根本没有异常抛出,线程中的异常会被线程忽略掉,这个是我们不需要的,我们需要知道异常发生了,并进行相应的处理。

  跟踪这种问题,通常记日志是一种常用方法。此外通过技术手段去捕获这些异常时另一种方式,这是这里讨论的重点。

  Task线程中未捕获的异常会在垃圾回收时终结器执行线程中被抛出。我们可以通过GC.Collect来强制垃圾回收从而引发终结器处理线程,此时Task的未捕获异常会被抛出。例如:

复制代码

//在Task中抛出异常
Task.Factory.StartNew(() =>
{throw new Exception();
});
//确保任务完成
Thread.Sleep(100);
//强制垃圾回收
GC.Collect();
//等待终结器处理
GC.WaitForPendingFinalizers();

复制代码

  好了,异常抛出,程序崩溃了。不过这个行为在.NET 4.5中又有所改变,直接运行这个程序并不会抛出异常,而在App.config中添加如下配置以后,异常才会抛出:

<configuration><runtime><ThrowUnobservedTaskExceptions enabled="true"/></runtime>
</configuration>

  抛出异常,程序崩溃并不是程序员想要的行为,我们期望的是可以捕获异常并处理之。要达到这个目的,针对Task对象,我们可以采用的手段有这么几个:调用Task.Wait/WaitAll,或者引用Task<T>.Result属性(这个在上面的例子中已经使用了),或者最简单的引用Task.Exception属性来捕获Task的异常。

  例如通过Task.Wait手动捕获AggregateException:

复制代码

try
{Task.WaitAll(Task.Factory.StartNew(() =>{throw new Exception();}));
}
catch (AggregateException)
{// 处理异常//...... 
}

复制代码

  这样我们就捕获到了异常并可以处理它了。

  当然最简单的就是直接引用一下Task.Exception属性:

复制代码

Task.Factory.StartNew(() =>
{throw new Exception();
}).ContinueWith(t => { var exp = t.Exception;// 处理异常//...... 
});

复制代码

  同样的,我们捕获了异常,并且处理掉异常就可以了,像上面例子中的处理方式就是忽略线程异常,没做任何处理。

  另外,可以通过TaskContinuationOptions.OnlyOnFaulted来使得只有在发生异常时才去执行ContinueWith中指定的行为,代码如下:

Task.Factory.StartNew(() =>
{throw new Exception();
}).ContinueWith(t => { var exp = t.Exception; }, TaskContinuationOptions.OnlyOnFaulted);

  最后需要说明的是TaskScheduler.UnobservedTaskException事件,该事件是所有未捕获被抛出前的最后可以将其捕获的方法。通过UnobservedTaskExceptionEventArgs.SetObserved方法来将异常标记为已捕获。

复制代码

TaskScheduler.UnobservedTaskException += (s, e) =>
{//设置所有未捕获异常被捕获e.SetObserved();
};Task.Factory.StartNew(() =>
{throw new Exception();
});

复制代码

   好了,Task的有关问题就总结到这里了,下面将总结一下并行计算方面的知识,它们与Task对象之间其实存在着千丝万缕的联系。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/438136.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

REVERSE-PRACTICE-BUUCTF-29

REVERSE-PRACTICE-BUUCTF-29[FlareOn1]Shellolololol[CFI-CTF 2018]powerPacked[INSHack2018]Tricky-Part2[CFI-CTF 2018]Automated Reversing[FlareOn1]Shellolololol exe程序&#xff0c;直接不能运行&#xff0c;无壳&#xff0c;ida分析 简单F8单步调试发现&#xff0c;在…

C#的变迁史05 - C# 4.0篇

C# 4.0 (.NET 4.0, VS2010) 第四代C#借鉴了动态语言的特性&#xff0c;搞出了动态语言运行时&#xff0c;真的是全面向“高大上”靠齐啊。 1. DLR动态语言运行时 C#作为静态语言&#xff0c;它需要编译以后运行&#xff0c;在编译的过程中&#xff0c;编译器要检查语法的正确性…

REVERSE-PRACTICE-BUUCTF-30

REVERSE-PRACTICE-BUUCTF-30[RCTF2019]DontEatMe[b01lers2020]little_engine[NPUCTF2020]你好sao啊[MRCTF2020]Shit[RCTF2019]DontEatMe exe程序&#xff0c;运行后输入&#xff0c;无壳&#xff0c;用ida分析 交叉引用字符串来到sub_401260函数&#xff0c;读取输入&#xff…

C#的变迁史06 - C# 4.0 之并行处理篇

前面看完了Task对象&#xff0c;这里再看一下另一个息息相关的对象Parallel。 Parallel对象 Parallel对象封装了能够利用多核并行执行的多线程操作&#xff0c;其内部使用Task来分装多线程的任务并试图将它们分配到不同的内核中并行执行。请注意“试图”这个词&#xff0c;Par…

REVERSE-PRACTICE-BUUCTF-31

REVERSE-PRACTICE-BUUCTF-31[羊城杯 2020]login[羊城杯 2020]Bytecode[羊城杯 2020]babyre[ACTF新生赛2020]fungame[羊城杯 2020]login exe程序&#xff0c;运行后输入&#xff0c;无壳&#xff0c;ida分析 没找到主要逻辑&#xff0c;在字符串窗口看到一些“py”的字样&#…

C#的变迁史07 - C# 4.0 之线程安全集合篇

作为多线程和并行计算不得不考虑的问题就是临界资源的访问问题&#xff0c;解决临界资源的访问通常是加锁或者是使用信号量&#xff0c;这个大家应该很熟悉了。 而集合作为一种重要的临界资源&#xff0c;通用性更广&#xff0c;为了让大家更安全的使用它们&#xff0c;微软为我…

PWN-PRACTICE-BUUCTF-1

PWN-PRACTICE-BUUCTF-1test_your_ncripwarmup_csaw_2016ciscn_2019_n_1test_your_nc 附件的main函数直接system("/bin/sh")&#xff0c;nc直接连即可cat flag rip main函数中&#xff0c;gets函数读取一行会造成栈溢出 构造payload覆盖rip&#xff0c;使得return…

C#的变迁史08 - C# 5.0 之并行编程总结篇

C# 5.0 搭载于.NET 4.5和VS2012之上。 同步操作既简单又方便&#xff0c;我们平时都用它。但是对于某些情况&#xff0c;使用同步代码会严重影响程序的可响应性&#xff0c;通常来说就是影响程序性能。这些情况下&#xff0c;我们通常是采用异步编程来完成功能&#xff0c;这在…

REVERSE-PRACTICE-CTFSHOW-1

REVERSE-PRACTICE-CTFSHOW-1逆向签到题re2逆向4逆向5逆向签到题 ida打开即可得到明文flag re2 附件是一个加密过的flag文本和勒索病毒exe 运行程序&#xff0c;输入1&#xff0c;回车&#xff0c;直接退出&#xff0c;ida分析 选项1的逻辑为&#xff0c;打开flag.txt和enfl…

C#的变迁史09 - C# 5.0 之调用信息增强篇

Caller Information CallerInformation是一个简单的新特性&#xff0c;包括三个新引入的Attribute&#xff0c;使用它们可以用来获取方法调用者的信息&#xff0c; 这三个Attribute在System.Runtime.CompilerServices命名空间下&#xff0c;分别叫做CallerMemberNameAttribute&…

REVERSE-PRACTICE-CTFSHOW-2

REVERSE-PRACTICE-CTFSHOW-2re3红包题 武穆遗书数学不及格flag白给re3 main函数&#xff0c;分析可知&#xff0c;将输入的字符串按十六进制转成数字&#xff0c;写到v5&#xff0c;赋给v17[6] 当i等于6时&#xff0c;v16会加上输入的值&#xff0c;然后进入循环&#xff0c;最…

C#的变迁史10 - C# 5.0 之其他增强篇

1. 内置zip压缩与解压   Zip是最为常用的文件压缩格式之一&#xff0c;也被几乎所有操作系统支持。在之前&#xff0c;使用程序去进行zip压缩和解压要靠第三方组件去支持&#xff0c;这一点在.NET4.5中已有所改观&#xff0c;Zip压缩和解压功能已经内置于框架本身。这个功能使…

REVERSE-PRACTICE-CTFSHOW-3

REVERSE-PRACTICE-CTFSHOW-3签退神光签到baby_gay签退 .pyc文件&#xff0c;uncompyle6反编译&#xff0c;得到python源码&#xff0c;分析写在源码注释中 先变表base64&#xff0c;再凯撒加密&#xff0c;向后移动2位 import string c_charset string.ascii_uppercase str…

REVERSE-PRACTICE-CTFSHOW-4

REVERSE-PRACTICE-CTFSHOW-4encodeEasyBJD hamburger competitionJustREencode elf文件&#xff0c;upx脱壳&#xff0c;ida分析 交叉引用字符串"Please input your flag:"&#xff0c;来到sub_804887C函数 输入经过三次变换&#xff0c;先是变表base64&#xff0c;…

CSS 基础框盒模型介绍

当对一个文档进行布局&#xff08;lay out&#xff09;的时候&#xff0c;浏览器的渲染引擎会根据标准之一的 CSS 基础框盒模型&#xff08;CSS basic box model&#xff09;&#xff0c;将所有元素表示为一个个矩形的盒子&#xff08;box&#xff09;。CSS 决定这些盒子的大小…

REVERSE-PRACTICE-CTFSHOW-5

REVERSE-PRACTICE-CTFSHOW-5re2_归心Mud[吃鸡杯]ezmore[吃鸡杯]有手就行re2_归心 exe程序&#xff0c;运行后要求输入flag&#xff0c;ida分析 函数窗没找到主逻辑函数&#xff0c;shiftF12看字符串窗口 发现有java/lang/String&#xff0c;com/exe4j/runtime/WinLauncher等字…

PWN-PRACTICE-BUUCTF-2

PWN-PRACTICE-BUUCTF-2pwn1_sctf_2016jarvisoj_level0ciscn_2019_c_1[第五空间2019 决赛]PWN5pwn1_sctf_2016 main函数中执行vuln函数 fgets限制了输入的长度&#xff0c;不足以构成栈溢出 通过将输入中的字符"I"替换成"you"&#xff0c;增加长度&#xf…

PWN-PRACTICE-BUUCTF-3

PWN-PRACTICE-BUUCTF-3[OGeek2019]babyropciscn_2019_n_8get_started_3dsctf_2016jarvisoj_level2[OGeek2019]babyrop 简单的ret2libc&#xff0c;构造rop main函数中读取一个随机数到buf中&#xff0c;传入sub_804871F 用"\x00"来绕过strlen和strncmp&#xff0c;b…

c#中常用集合类和集合接口之接口系列【转】

常用集合接口系列&#xff1a;http://www.cnblogs.com/fengxiaojiu/p/7997704.html 常用集合类系列&#xff1a;http://www.cnblogs.com/fengxiaojiu/p/7997541.html 大多数集合都在System.Collections&#xff0c;System.Collections.Generic两个命名空间。其中System.Colle…

PWN-PRACTICE-BUUCTF-4

PWN-PRACTICE-BUUCTF-4ciscn_2019_en_2bjdctf_2020_babystacknot_the_same_3dsctf_2016[HarekazeCTF2019]baby_ropciscn_2019_en_2 这题和ciscn_2019_c_1一模一样 栈溢出ret2libc&#xff0c;encrypt函数里的异或运算不用管 from pwn import * context.log_level"debug&…