C#多线程全览
并行
1 池
资源管理:重复利用
控制并发的个数
1.1 内存池
分配方式一:预先分配的数组
分配方式二:链表结构
1.2 数据库连接池
1.3 线程池
https://www.cnblogs.com/chenbaoshun/p/10566124.html
2 线程
进程(Process):是系统中的一个基本概念。 一个正在运行的应用程序在操作系统中被视为一个进程,包含着一个运行程序所需要的资源,进程可以包括一个或多个线程 。进程之间是相对独立的,一个进程无法访问另一个进程的数据(除非利用分布式计算方式),一个进程运行的失败也不会影响其他进程的运行,Windows系统就是利用进程把工作划分为多个独立的区域的。进程可以理解为一个程序的基本边界。
线程(Thread):是 进程中的基本执行单元,是操作系统分配CPU时间的基本单位 ,在进程入口执行的第一个线程被视为这个进程的 主线程 。
多线程能实现的基础:
1、CPU运行速度太快,硬件处理速度跟不上,所以操作系统进行分时间片管理。这样,宏观角度来说是多线程并发 ,看起来是同一时刻执行了不同的操作。但是从微观角度来讲,同一时刻只能有一个线程在处理。
2、目前电脑都是多核多CPU的,一个CPU在同一时刻只能运行一个线程,但是 多个CPU在同一时刻就可以运行多个线程 。
多线程的优点:
可以同时完成多个任务;可以让占用大量处理时间的任务或当前没有进行处理的任务定期将处理时间让给别的任务;可以随时停止任务;可以设置每个任务的优先级以优化程序性能。
多线程的缺点:
1、 内存占用 线程也是程序,所以线程需要占用内存,线程越多,占用内存也越多(每个线程都需要开辟堆栈空间,多线程时有时需要切换时间片)。
2、 管理协调 多线程需要协调和管理,所以需要占用CPU时间以便跟踪线程,线程太多会导致控制太复杂。
3、 资源共享 线程之间对共享资源的访问会相互影响,必须解决争用共享资源的问题。
2.1 线程的使用
2.1.1 无参数调用
ThreadTest test = new ThreadTest();
6 //无参调用实例方法
7 Thread thread1 = new Thread(test.Func2);
8 thread1.Start();
2.1.2 有参数调用
//有参调用实例方法,ParameterizedThreadStart是一个委托,input为object,返回值为void。
Thread thread1 = new Thread(new ParameterizedThreadStart(test.Func1));
thread1.Start("有参的实例方法");
2.2 常用的属性和方法
|
属性名称 |
说明 |
|
CurrentThread |
获取当前正在运行的线程。 |
|
ExecutionContext |
获取一个 ExecutionContext 对象,该对象包含有关当前线程的各种上下文的信息。 |
|
IsBackground |
bool,指示某个线程是否为后台线程。 |
|
IsThreadPoolThread |
bool,指示线程是否属于托管线程池。 |
|
ManagedThreadId |
int,获取当前托管线程的唯一标识符。 |
|
Name |
string,获取或设置线程的名称。 |
|
Priority |
获取或设置一个值,该值指示线程的调度优先级 。 Lowest<BelowNormal<Normal<AboveNormal<Highest |
|
ThreadState |
获取一个值,该值包含当前线程的状态。 Unstarted、Sleeping、Running 等 |
|
方法名称 |
说明 |
|
GetDomain() |
返回当前线程正在其中运行的当前域。 |
|
GetDomainId() |
返回当前线程正在其中运行的当前域Id。 |
|
Start() |
执行本线程。(不一定立即执行,只是标记为可以执行) |
|
Suspend() |
挂起当前线程,如果当前线程已属于挂起状态则此不起作用 |
|
Resume() |
继续运行已挂起的线程。 |
|
Interrupt() |
中断处于 WaitSleepJoin 线程状态的线程。 |
|
Abort() |
终结线程 |
|
Join() |
阻塞调用线程,直到某个线程终止。 |
|
Sleep() |
把正在运行的线程挂起一段时间。 |
2.3 线程同步(资源竞争)
所谓同步: 是指在某一时刻只有一个线程可以访问变量 。
c#语言的关键字Lock,它可以把一段代码定义为互斥段,互斥段在一个时刻内只允许一个线程进入执行,实际上是Monitor.Enter(obj),Monitor.Exit(obj)的语法糖。在c#中,lock的用法如下:
lock (obj) { dosomething... }
obj代表你希望锁定的对象,注意一下几点:
1. lock不能锁定空值 ,因为Null是不需要被释放的。 2. 不能锁定string类型 ,虽然它也是引用类型的。因为字符串类型被CLR“暂留”,这意味着整个程序中任何给定字符串都只有一个实例,具有相同内容的字符串上放置了锁,就将锁定应用程序中该字符串的所有实例。 3. 值类型不能被lock ,每次装箱后的对象都不一样 ,锁定时会报错 4 避免锁定public类型 如果该实例可以被公开访问,则 lock(this) 可能会有问题,因为不受控制的代码也可能会锁定该对象。
推荐使用 private static readonly类型的对象,readonly是为了避免lock的代码块中修改对象,造成对象改变后锁失效。
2.4 跨线程访问
C#不允许跨线程访问Windows窗体的控件
解决:线程是资源共享的。可以引用到UI线程的对象。
在新的线程中,UI对象使用dispatch的invoke调用函数,函数刷新UI对象。
2.5 同步和异步
control.invoke(参数delegate)方法:在拥有此控件的基础窗口句柄的线程上执行指定的委托。
control.begininvoke(参数delegate)方法:在创建控件的基础句柄所在线程上异步执行指定委托。
根据这两个概念我们大致理解invoke表是同步、begininvoke表示异步
Invoke:等待invoke提交到的线程执行完后,当前线程才继续执行。
BeginInvoke:不等待invoke提交到的线程执行完,当前线程invoke后,就直接往下执行。
3 并行
3.1 什么是并行
并行是指两个或者多个事件在同一时刻发生。
在程序运行中,并行指多个CPU核心同时执行不同的任务;对于单核心CPU,严格来说是没有程序并行的。并行是为了提高任务执行效率,更快的获取结果。
3.1.1 与并发的区别:
并发是指两个或者多个事件在同一时段发生。
相对于并行,并发强调的是同一时段,是宏观上的同时发生。实际上,同一时刻只有一个任务在被执行,多个任务是分时地交替执行的。并发是为了更合理地分配资源。
3.2 如何实现并行
并行编程中我们只关注应用层面的并行,CPU的指令并行技术(指令流水等)不在我们的考虑范围。
从并行的意义来看,并行编程的目的无非是让多个CPU核心同时执行不同业务逻辑,获取优良的性能。但是,要怎样实现并行呢?实现并行,我们要借助进程和线程。
为了更好地管理计算机中运行的程序,计算机操作系统引入进程:
狭义定义:进程是正在运行的程序的实例(an instance of a computer program that is being executed)。
广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。
——百度百科
由于进程拥有计算机资源,在创建、切换和撤销的过程中开销较大,这就限制了进程的并发程度;多核CPU的日渐普及的环境下,为提高并行粒度和并行计算的效率,引入了一种轻型的进程——线程:
线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。
——百度百科
线程包含于进程,同一进程的线程共享该进程的资源。线程出现后,线程取代进程作为操作系统调度和分派的基本单位,极大地减少了进程切换带来的性能损失,使得更细粒度和更高性能的并行得以实现。
3.2.1 进程的调度
一台计算机会运行很多程序,这些程序进程的数量多会大于CPU的核心数量。每个CPU核心同一时间只能执行一个进程,那操作系统是如何管理这些进程的呢?
当启动一个程序的实例时,操作系统将创建一个进程用来调度该程序实例。一个进程主要包含以下的信息:
- 进程控制块PCB,用于操作系统控制该程序实例
- o进程标识信息,如PID、名称等
- o现场信息,存放进程运行时处理器现场信息
- o控制信息,存放操作系统用于管理和调度进程的信息
- 专有的虚拟地址空间
- 句柄列表
- 程序实例的代码和数据,被映射到进程私有虚拟地址空间
- 程序状态字信息
进程的状态模型,如下图:
操作系统按照进程状态进行程序调度。
- 启动程序时,操作系统创建进程,此时进程为
新建态- o运行资源充足时,操作系统提交进程到
就绪状态,等待CPU选择或者抢占CPU执行 - o运行资源不足,如主存不够,操作系统会挂起进程,进程状态改为
就绪挂起,等待操作系统的恢复
- o运行资源充足时,操作系统提交进程到
- 就绪状态的进程
- oCPU空闲时,会选择执行就绪状态的进程,被选中的进程进入
运行状态 - o进程优先级高时,将抢占当前正在执行进程的CPU资源,自身进入运行状态
- o操作系统会根据当前的可用资源,把就绪状态的进程挂起
- oCPU空闲时,会选择执行就绪状态的进程,被选中的进程进入
- 就绪挂起的进程
- o当前没有就绪的进程,或者就绪挂起的某个进程具有较高的优先级,操作系统会将就绪挂起的进程恢复到就绪状态
- 运行状态的进程
- o进程自然结束、被强制终结或者出现无法解决的异常,将进入
终止状态,终止的线程不再参与进程调度 - o进程到达运行的时间片或者出现优先级高的进程抢占了CPU,进程会回到就绪状态等待调度
- o进程等待资源、I/O或者信号时,会进入
阻塞状态 - o优先级较高的进程抢占CPU,而此时系统资源不足,则正在运行的线程会被转入就绪挂起状态
- o进程自然结束、被强制终结或者出现无法解决的异常,将进入
- 阻塞状态的进程
- o进程阻塞的条件被满足,如等待的资源到位、I/O完成或收到信号,会进入就绪状态
- o进程在等待资源、I/O或者信号时,若系统检测到运行资源不足,会将阻塞的进程挂起进入
阻塞挂起状态
- 阻塞挂起的进程
- o当被挂起的进程具有较高优先级,同时由于其他进程的退出使资源充裕,进程会被转为阻塞状态
- o挂起的阻塞进程得到资源、I/O完成或者收到信号后,被转入就绪挂起状态
上述便是进程的调度过程,其中挂起的进程不占有任何资源。进程的调度很大程度是依赖于运行资源的;进程的优先级也是影响进程调度的重要因素;此外进程的调度还会涉及进程间的通信和同步问题,这里不做展开。
实际上,相对于进程,在并行编程中我们更关心线程,因为线程才是系统调度的基本单位。
3.2.2 线程的调度
在Windows系统中,每个进程至少有一个线程,每个线程都包含下面的内容:
- 线程内核对象,包含线程上下文(包含CPU寄存器信息的内存块)
- 线程环境块,包含线程的异常处理链首、本地存储数据等
- 用户模式栈,存储传给方法的局部变量和实参
- 内核模式栈,线程调用操作系统内核函数时,所传实参从用户模式栈复制到内核模式栈
- DLL线程连接和分离,线程创建和销毁时,所依赖的DLL需要收到通知才能执行相关资源的初始化和清理
从线程所含内容,我们可以知道线程的创建和销毁是有着时间和空间开销的,虽然这些开销相较于进程来说小了很多,但仍是影响程序效率的重要因素。特别是在并行处理的时候,线程的频繁创建和销毁将对并行性能产生极为严重的影响。
系统同一时间只给一个CPU核心分配一个线程,CPU执行该线程达一个时间片后,系统会给该CPU核心分配另一个线程。系统分配线程至CPU核心的过程就是线程的上下文切换过程,此间,系统将执行3个动作:
- 把CPU寄存器的值保存到正在运行的线程上下文中
- 从现有线程集合中选取一个线程准备分配
- 把选中线程上下文中保存的CPU寄存器值加载到CPU寄存器中
线程上下文切换会对程序性能带来很严重的影响,特别是切换到一个新进程的新线程时,很可能需要从RAM中加载代码和数据,大家知道RAM相对于CPU高速缓存太慢了。
线程的创建、切换及销毁都是有着不可忽视的开销,在追求高性能的程序中,我们应尽量少地线程,最优性能的线程数是机器CPU的核心数。当然,性能只是程序的一个方面,响应性和可靠性也是要关注的重点。
4 线程池
线程的创建和销毁要耗费很多时间,而且过多的线程不仅会浪费内存空间,还会导致线程上下文切换频繁,影响程序性能。为改善这些问题,.NET运行时(CLR)会为每个进程开辟一个全局唯一的线程池来管理其线程。
线程池内部维护一个操作请求队列,程序执行异步操作时,添加目标操作到线程池的请求队列;线程池代码提取记录项并派发给线程池中的一个线程;如果线程池中没有可用线程,就创建一个新线程,创建的新线程不会随任务的完成而销毁,这样就可以避免线程的频繁创建和销毁。如果线程池中大量线程长时间无所事事,空闲线程会进行自我终结以释放资源。
线程池通过保持进程中线程的少量和高效来优化程序的性能。
C#中线程池是一个静态类,维护两种线程,工作线程和异步IO线程,这些线程都是后台线程。线程池不会影响进程的正常退出。
4.1 线程池的使用
线程池提供两个静态方法SetMaxThreads和SetMinThreads让我们设置线程池的最大线程数和最小线程数。最大线程数指的是,该线程池能够创建的最大线程数,当线程数达到设定值且忙碌,异步任务将进入请求队列,直到有线程空闲才会执行;最小线程数指的是,线程池优先尝试以设置数量的线程处理请求,当请求数达到一定量(未做深入研究)时,才会创建新的线程。
下面的例子展示了线程池的特性及常见使用方式。
classProgram
{
static void Main(string[] args)
{
//RunThreadPoolDemo();
RunCancellableWork();
Console.ReadKey();
}
static void RunThreadPoolDemo()
{
ThreadPoolDemo.ThreadPoolDemo.ShowThreadPoolInfo();
ThreadPool.SetMaxThreads(100,100);// 默认(1023,1000)(8核心CPU)
ThreadPool.SetMinThreads(8,8);// 默认是CPU核心数
ThreadPoolDemo.ThreadPoolDemo.ShowThreadPoolInfo();
ThreadPoolDemo.ThreadPoolDemo.MakeThreadPoolDoSomeWork(100);//计算限制任务
ThreadPoolDemo.ThreadPoolDemo.MakeThreadPoolDoSomeIOWork();//IO限制任务
}
static void RunCancellableWork()
{
Console.WriteLine($"{DateTime.Now}=> Thread-[{Thread.CurrentThread.ManagedThreadId}] started a work");
Console.WriteLine("Press 'Esc' to cancel the work.");
Console.WriteLine();
ThreadPoolDemo.ThreadPoolDemo.DoSomeWorkWithCancellation();
if(Console.ReadKey(true).Key == ConsoleKey.Escape)
{// 发送取消通知
ThreadPoolDemo.ThreadPoolDemo.CTSource.Cancel();
}
}
}
publicclassThreadPoolDemo
{
/// <summary>
/// 显示线程池信息
/// </summary>
public static void ShowThreadPoolInfo()
{
intworkThreads, completionPortThreads;
//当前线程池可用的工作线程数量和异步IO线程数量
ThreadPool.GetAvailableThreads(outworkThreads,outcompletionPortThreads);
Console.WriteLine($"GetAvailableThreads => workThreads:{workThreads};completionPortThreads:{completionPortThreads}");
//线程池最大可用的工作线程数量和异步IO线程数量
ThreadPool.GetMaxThreads(outworkThreads,outcompletionPortThreads);
Console.WriteLine($"GetMaxThreads => workThreads:{workThreads};completionPortThreads:{completionPortThreads}");
//出现新的请求,判断是否需要创建新线程的依据
ThreadPool.GetMinThreads(outworkThreads,outcompletionPortThreads);
Console.WriteLine($"GetMinThreads => workThreads:{workThreads};completionPortThreads:{completionPortThreads}");
Console.WriteLine();
}
/// <summary>
/// 让线程池做些事情
/// </summary>
public static void MakeThreadPoolDoSomeWork(int workCount = 10)
{
for(inti =0; i < workCount; i++)
{
intindex = i;
// 将方法排队进入线程池工作项队列,当线程池有空闲线程时执行方法
ThreadPool.QueueUserWorkItem(s =>
{
Thread.Sleep(100);//模拟工作时长
Debug.Print($"{DateTime.Now}=> Thread-[{Thread.CurrentThread.ManagedThreadId}] is running. [{index}]");
ShowAvailableThreads("WorkerThread");
});
}
}
/// <summary>
/// 让线程池做些IO工作
/// </summary>
public static void MakeThreadPoolDoSomeIOWork()
{
//随便找一些可以访问的网址
IList<string> uriList =newList<string>()
{
"http://news.baidu.com/",
"https://www.hao123.com/",
"https://map.baidu.com/",
"https://tieba.baidu.com/",
"https://wenku.baidu.com/",
"http://fanyi-pro.baidu.com",
"http://bit.baidu.com/",
"http://xueshu.baidu.com/",
"http://www.cnki.net/",
"http://www.wanfangdata.com.cn",
};
foreach(stringuriinuriList)
{
WebRequest request = WebRequest.Create(uri);
request.BeginGetResponse(ac =>
{// 异步请求网址,将会利用线程池中异步IO线程
try
{
WebResponse response = request.EndGetResponse(ac);
ShowAvailableThreads("IOThread");
Debug.Print($"{DateTime.Now}=> Thread-[{Thread.CurrentThread.ManagedThreadId}] is running. [{response.ContentLength}]");
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
}
}, request);
}
}
/// <summary>
/// 打印线程池当前可用线程数
/// </summary>
private static void ShowAvailableThreads(string sourceTag = null)
{
intworkThreads, completionPortThreads;
ThreadPool.GetAvailableThreads(outworkThreads,outcompletionPortThreads);
Console.WriteLine($"{sourceTag} GetAvailableThreads => workThreads:{workThreads};completionPortThreads:{completionPortThreads}");
Console.WriteLine();
}
/// <summary>
/// 取消通知者
/// </summary>
publicstaticCancellationTokenSource CTSource {get;set; } =newCancellationTokenSource();
/// <summary>
/// 执行可取消的任务
/// </summary>
public static void DoSomeWorkWithCancellation()
{
ThreadPool.QueueUserWorkItem(t =>
{
Console.WriteLine($"{DateTime.Now}=> Thread-[{Thread.CurrentThread.ManagedThreadId}] begun running. [0 - 9999]");
for(inti =0; i <10000; i++)
{
if(CTSource.Token.IsCancellationRequested)
{
Console.WriteLine($"{DateTime.Now}=> Thread-[{Thread.CurrentThread.ManagedThreadId}] recived the cancel token. [{i}]");
break;
}
Thread.Sleep(100);// 模拟工作时长
}
Console.WriteLine($"{DateTime.Now}=> Thread-[{Thread.CurrentThread.ManagedThreadId}] was cancelled.");
});
}
}
4.2 线程池的调度
前面提到,线程池内部维护者一个工作项队列,这个队列指的是线程池全局队列。实际上,除了全局队列,线程池会给每个工作者线程维护一个本地队列。
当我们调用ThreadPool.QueueUserWorkItem方法时,工作项会被放入全局队列;使用定时器Timer的时候,也会将工作项放入全局队列;但是,当我们使用任务Task的时候,假如使用默认的任务调度器,任务会被调度到工作者线程的本地队列中。
工作者线程优先执行本地队列中最新进入的任务,如果本地队列中已经没有任务,线程会尝试从其他工作者线程任务队列的队尾取任务执行,这里需要进行同步。如果所有工作者线程的本地队列都没有任务可以执行,工作者线程才会从全局队列取最新的工作项来执行。所有任务执行完毕后,线程睡眠,睡眠一定时间后,线程醒来并销毁自己以释放资源。
4.3 线程池处理异步IO的内部原理
上面的例子中,从网站获取信息需要用到线程池的异步IO线程,线程池内部利用IOCP(IO完成端口)与硬件设备建立连接。异步IO实现过程如下:
- 托管的IO请求线程调用Win32本地代码ReadFile方法
- ReadFile方法分配IO请求包IRP并发送至Windows内核
- Windows内核把收到的IRP放入对应设备驱动程序的IRP队列中,此时IO请求线程已经可以返回托管代码
- 驱动程序处理IRP并将处理结果放入.NET线程池的IRP结果队列中
- 线程池分配IO线程处理IRP结果
4.4 小结
.NET线程池是并发编程的主要实现方式。C#中Timer、Parallel、Task在内部都是利用线程池实现的异步功能,深入理解线程池在并行编程中十分重要。
5 并行
https://www.cnblogs.com/chenbaoshun/p/10572639.html
Parallel
并行循环主要用来处理数据并行的,如,同时对数组或列表中的多个数据执行相同的操作。
在C#编程中,我们使用并行类System.Threading.Tasks.Parallel提供的静态方法Parallel.For和Parallel.ForEach来实现并行循环。从方法名可以看出,这两个方法是对常规循环for和foreach的并行化。
5.1 简单用法
使用并行循环时需要传入循环范围(集合)和操作数据的委托Action<T>:
Parallel.For(0,100, i => { Console.WriteLine(i); });
Parallel.ForEach(Enumerable.Range(0,100), i => { Console.WriteLine(i); });
5.2 使用场景
对于数据的处理需要耗费较长时间的循环适宜使用并行循环,利用多线程加快执行速度。
对于简单的迭代操作,且迭代范围较小,使用常规循环更好好,因为并行循环涉及到线程的创建、上下文切换和销毁,使用并行循环反而影响执行效率。
对于迭代操作简单但迭代范围很大的情况,我们可以对数据进行分区,再执行并行循环,减少线程数量。
5.1 循环结果
Parallel.For和Parallel.ForEach方法的所有重载有着同样的返回值类型ParallelLoopResult,并行循环结果包含循环是否完成以及最低迭代次数两项信息。
下面的例子使用Parallel.ForEach展示了并行循环的结果。
ParallelLoopResult result = Parallel.ForEach(Enumerable.Range(0,100), (i,loop) =>
{// 委托传入ParallelLoopState,用来控制循环执行
Console.WriteLine(i +1);
Thread.Sleep(100);
if(i ==30)// 此处设置循环停止的确切条件
{
loop.Break();
//loop.Stop();
}
});
Console.WriteLine($"{result.IsCompleted}-{result.LowestBreakIteration}");
值得一提的是,循环的Break()和Stop()只能尽早地跳出或者停止循环,而不能立即停止。
5.2 取消循环操作
有时候,我们需要在中途取消循环操作,但又不知道确切条件是什么,比如用户触发的取消。这时候,可以利用循环的ParallelOptions传入一个CancellationToken,同时使用异常处理捕获OperationCanceledException以进行取消后的处理。下面是一个简单的例子。
/// <summary>
/// 取消通知者
/// </summary>
publicstaticCancellationTokenSource CTSource {get;set; } =newCancellationTokenSource();
/// <summary>
/// 取消并行循环
/// </summary>
public static void CancelParallelLoop()
{
Task.Factory.StartNew(() =>
{
try
{
Parallel.ForEach(Enumerable.Range(0,100),newParallelOptions { CancellationToken = CTSource.Token },
i =>
{
Console.WriteLine(i +1);
Thread.Sleep(1000);
});
}
catch(OperationCanceledException oce)
{
Console.WriteLine(oce.Message);
}
});
}
static void Main(string[] args)
{
ParallelDemo.CancelParallelLoop();
Thread.Sleep(3000);
ParallelDemo.CTSource.Cancel();
Console.ReadKey();
}
5.3 循环异常收集
并行循环执行过程中,可以捕获并收集迭代操作引发的异常,循环结束时抛出一个AggregateException异常,并将收集到的异常赋给它的内部异常集合InnerExceptions。外部使用时,捕获AggregateException,即可进行并行循环的异常处理。
下面的例子模拟了并行循环的异常抛出、收集及处理的过程。
/// <summary>
/// 捕获循环异常
/// </summary>
public static void CaptureTheLoopExceptions()
{
ConcurrentQueue<Exception> exceptions =newConcurrentQueue<Exception>();
Parallel.ForEach(Enumerable.Range(0,100), i =>
{
try
{
if(i %10==0)
{//模拟抛出异常
thrownewException($"{DateTime.Now}=> Thread-[{Thread.CurrentThread.ManagedThreadId}] had thrown a exception. [{i}]");
}
Console.WriteLine(i +1);
Thread.Sleep(100);
}
catch(Exception ex)
{//捕获并收集异常
exceptions.Enqueue(ex);
}
});
if(!exceptions.IsEmpty)
{// 方法内部可直接进行异常处理,若需外部处理,将收集到的循环异常抛出
thrownewAggregateException(exceptions);
}
}
外部处理方式
try
{
ParallelDemo.CaptureTheLoopExceptions();
}
catch (AggregateException aex)
{
foreach(Exception exinaex.InnerExceptions)
{// 模拟异常处理
Console.WriteLine(ex.Message);
}
}
6 Task
在C#编程中,实现并行可以直接使用线程,但使用起来很繁琐;也可以使用线程池,线程池很大程度上简化了线程的使用,但是也有着一些局限,比如我们不知道作业什么时候完成,也取不到作业的返回值;解决线程池局限性的方案是使用任务。本文将总结C#中Task的使用。
类似于线程池工作项对异步操作的封装,任务是对异步操作的另一种形式的封装,这种封装抽象层次更高,让我们能够对异步操作进行更多的控制。
6.1 显式使用
相对于使用Parallel.Invoke执行并行操作,更常用的是使用Task和Task<T>提供的方法进行异步和并行处理。下面是任务最基本的使用:
Task.Run(() =>
{
//TODO
});
Task.Factory.StartNew(() =>
{
//TODO
});
任务的常用操作Thread.Sleep(3000);//模拟操作用时
returnDateTime.Now.Day;
});
6.2 任务的常用操作
6.2.1 获取任务的返回值task.Result
具有返回值的任务使用Task<T>,T可根据我们的需求指定,下面是获取任务返回值的方法。
Task<int> task = Task<int>.Factory.StartNew(() =>
{
Thread.Sleep(3000);//模拟操作用时
returnDateTime.Now.Day;
});
int day = task.Result;
需要说明的是,获取任务的结果会阻塞当前线程。
6.2.2 等待任务完成
有时候,我们需要等待一些任务全部完成后才能执行后续操作,有时候只要多个任务中的一个完成了,就可以执行后续操作。Task提供了Wait、WaitAll和WaitAny等方法满足我们的需求。下面的例子展示了各种等待方法的使用。
/// <summary>
/// 任务等待测试
/// </summary>
public static void TaskWait()
{
Stopwatch watch =newStopwatch();
1)等待执行完成Wait
#region 场景1:等待一个任务完成
Task task = Task.Run(() => DoWorkOfTask(1000));
Console.WriteLine("start wait. work duration: 1000");
watch.Start();
task.Wait();//等待1秒左右
watch.Stop();
Console.WriteLine($"end wait. time: {watch.ElapsedMilliseconds}");
#endregion
2)等待全部完成Task.WaitAll
#region 场景2:等待多个任务完成
Task[] tasks =newTask[3]
{
Task.Run(() => DoWorkOfTask(1000)),
Task.Run(() => DoWorkOfTask(2000)),
Task.Run(() => DoWorkOfTask(3000)),
};
Console.WriteLine("start wait all. work duration: min 1000 max 3000.");
watch.Restart();
Task.WaitAll(tasks);//等待3秒左右
watch.Stop();
Console.WriteLine($"end wait. time: {watch.ElapsedMilliseconds}");
#endregion
3)等待某个完成Task.WaitAny
#region 场景3:等待某个任务完成
tasks =newTask[3]
{
Task.Run(() => DoWorkOfTask(1000)),
Task.Run(() => DoWorkOfTask(2000)),
Task.Run(() => DoWorkOfTask(3000)),
};
Console.WriteLine("start wait any. work duration: min 1000 max 3000.");
watch.Restart();
Task.WaitAny(tasks);//等待1秒左右
watch.Stop();
Console.WriteLine($"end wait. time: {watch.ElapsedMilliseconds}");
#endregion
}
/// <summary>
/// 做任务
/// </summary>
/// <param name="workDuration">任务时长</param>
private static void DoWorkOfTask(int workDuration)
{
Console.WriteLine($"{DateTime.Now}=> Thread[{Thread.CurrentThread.ManagedThreadId}] started task[{Task.CurrentId}].");
Thread.Sleep(workDuration);
Console.WriteLine($"{DateTime.Now}=> Thread[{Thread.CurrentThread.ManagedThreadId}] completed task[{Task.CurrentId}].");
}
使用Wait、WaitAll和WaitAny方法时,我们可以设置超时时间或者传入取消Token,以控制等待时间。但这些方法返回布尔值,只能表明是否等待成功;假如我们需要知道所等待的任务返回值,则可以使用WhenAll或WhenAny方法,这两个方法不能控制等待时间,但会返回一个完成的任务。如下例:
Task<int>[] tasks =newTask<int>[3]
{
Task<int>.Factory.StartNew(() =>
{
Console.WriteLine($"task #{Task.CurrentId} run");
Thread.Sleep(100);
Console.WriteLine($"task #{Task.CurrentId} done");
return100;
}),
Task<int>.Factory.StartNew(() =>
{
Console.WriteLine($"task #{Task.CurrentId} run");
Thread.Sleep(500);
Console.WriteLine($"task #{Task.CurrentId} done");
return1000;
}),
Task<int>.Factory.StartNew(() =>
{
Console.WriteLine($"task #{Task.CurrentId} run");
Thread.Sleep(1000);
Console.WriteLine($"task #{Task.CurrentId} done");
return10000;
}),
};
//int[] results = Task.WhenAll(tasks).Result;
//Console.WriteLine($"[{string.Join(",",results)}]");
Task<int> task = Task.WhenAny(tasks).Result;
Console.WriteLine($"task #{task.Id}. result {task.Result}");
Task.WhenAll 和Task.WhenAny在等待结束时,都会创建一个完成状态的任务,WhenAll将等待的所有已完成任务的结果放入创建任务的结果中,WhenAny则将等待的已完成任务放到创建任务的结果中。
6.2.3 任务接续task.ContinueWith(接下来的新任务)
有时候,我们需要在一个任务完成时开始另一个任务。对于这种需求,我们可以使用Task的ContinueWith等方法来处理。
Task task = Task.Run(() => DoWorkOfTask(3000));
task.ContinueWith(t => DoWorkOfTask(1000));
运行结果:
2019/3/27 21:25:09=> Thread[10] started task[1].
2019/3/27 21:25:12=> Thread[10] completed task[1].
2019/3/27 21:25:12=> Thread[11] started task[2].
2019/3/27 21:25:13=> Thread[11] completed task[2].
我们还可以通过TaskContinuationOptions指定延续任务的执行条件,如任务取消时或者任务出现异常时才执行,等。
6.2.4 子任务的使用
有时候,我们要在一个任务里面创建一些其他任务,并且还要在任务里面等待创建的任务完成,此时我们可以使用子任务。
Task parent = Task.Factory.StartNew(() =>
{
Console.WriteLine($"parent task #{Task.CurrentId} run.");
for(inti =0; i <10; i++)
{
Task.Factory.StartNew(() =>
{
Console.WriteLine($"child task #{Task.CurrentId} run.");
Thread.Sleep(1000);
Console.WriteLine($"child task #{Task.CurrentId} done.");
}, TaskCreationOptions.AttachedToParent);
}
});
parent.Wait();
Console.WriteLine($"parent task #{parent.Id} done.");
在一个任务中创建的新任务,默认情况下与父级任务是分离的,各自的运行不受影响,除非在创建任务时显式附加到父级任务中。例如,上例中如果不指定TaskCreationOptions.AttachedToParent,parent.Wait()就不会持续到所有子任务都执行完成。
6.2.5 任务的取消
我们在启动任务时,传入取消令牌CancellationToken,当收到取消请求时,抛出取消异常并在等待任务完成时捕获异常TaskCanceledException。我们通过这种方式控制任务的取消。
/// <summary>
/// 任务取消
/// </summary>
public static void TaskCancle()
{
Console.WriteLine("Press any key to begin. Press 'c' to cancel. ");
Console.ReadKey(true);
Console.WriteLine();
CancellationTokenSource tokenSource =newCancellationTokenSource();
ConcurrentBag<Task> tasks =newConcurrentBag<Task>();
//单任务取消
Task task1 = Task.Factory.StartNew(() => DoWorkOfTask(5000, tokenSource.Token), tokenSource.Token);
tasks.Add(task1);
//嵌套任务取消
Task task2 = Task.Factory.StartNew(() =>
{
for(inti =0; i <10; i++)
{
intduration =1000* i;
tasks.Add(Task.Factory.StartNew(()=>DoWorkOfTask(duration, tokenSource.Token), tokenSource.Token));
}
DoWorkOfTask(5000,tokenSource.Token);
}, tokenSource.Token);
tasks.Add(task2);
charch = Console.ReadKey().KeyChar;
if(ch =='c'|| ch =='C')
{
tokenSource.Cancel();
Console.WriteLine($"{DateTime.Now}=> Task cancellation requested.");
}
try
{
Task.WaitAll(tasks.ToArray());
}
catch(AggregateException ae)
{
foreach(Exception exinae.InnerExceptions)
{//任务取消通过抛出TaskCanceledException实现
TaskCanceledException tce = exasTaskCanceledException;
stringcancelledTask = tce ==null?string.Empty :$"Task #{tce.Task.Id}";
Console.WriteLine($"Exception: {ex.GetType().Name}. {cancelledTask}");
}
}
finally
{
tokenSource.Dispose();
}
Console.WriteLine();
//显示任务状态
foreach(Task taskintasks)
{
Console.WriteLine($"Task: #{task.Id} now is {task.Status}");
}
}
/// <summary>
/// 带取消令牌的作业
/// </summary>
/// <param name="workDuration">作业时长</param>
/// <param name="cancleToken">取消令牌</param>
private static void DoWorkOfTask(int workDuration, CancellationToken cancleToken)
{
if(cancleToken.IsCancellationRequested)
{//开始之前取消
Console.WriteLine($"{DateTime.Now}=> Task #{Task.CurrentId} was cancelled before it got started.");
cancleToken.ThrowIfCancellationRequested();
}
Console.WriteLine($"{DateTime.Now}=> Thread[{Thread.CurrentThread.ManagedThreadId}] started task #{Task.CurrentId}.");
Thread.Sleep(workDuration);
if(cancleToken.IsCancellationRequested)
{//开始之后取消
Console.WriteLine($"{DateTime.Now}=> Task #{Task.CurrentId} was cancelled.");
cancleToken.ThrowIfCancellationRequested();
}
Console.WriteLine($"{DateTime.Now}=> Thread[{Thread.CurrentThread.ManagedThreadId}] completed task #{Task.CurrentId}.");
}
6.3 任务的异常处理
上面提到通过取消令牌抛出TaskCanceledException的方式控制任务的取消,实际上,Task会把自身执行过程中的所有异常都包装到一个AggregateException中,并传回调用线程。我们在主线程中通过捕获AggregateException来进行异常处理。
6.3.1 简单的处理方式
我们可以在任务的调用线程捕获并遍历AggregateException的内部异常,或者使用AggregateException提供的Handle方法进行处理,如下:
Task task = Task.Run(() =>
{
thrownewException($"Task #{Task.CurrentId} thrown an exception");
});
try
{
task.Wait();
}
catch (AggregateException ae)
{
//处理方式1:遍历内部异常进行处理
foreach(Exception exinae.InnerExceptions)
{
Console.WriteLine($"foreach: {ex.Message}");
}
//处理方式2:使用AggregateException的Handle方法
ae.Handle(ex=>
{
Console.WriteLine($"handle: {ex.Message}");
returntrue;
});
}
6.3.2 使用延续任务处理任务的异常
有时候,我们可以给任务附加一个任务异常时才会执行的延续任务,并在延续任务中进行异常处理。
Task.Run(() => {thrownewException($"Task #{Task.CurrentId} thrown an exception"); })
.ContinueWith(t =>
{
Console.WriteLine($"{t.Exception?.InnerException?.Message}");
}, TaskContinuationOptions.OnlyOnFaulted);
6.3.3 嵌套任务的异常处理
下面是一个3层嵌套的任务。
Task parent = Task.Factory.StartNew(() =>
{//父级任务
for(inti =0; i <10; i++)
{
Task.Factory.StartNew(() =>
{//1代子任务
for(intj =0; j <10; j++)
{
Task.Factory.StartNew(() =>
{//2代子任务
thrownewException($"Task #{Task.CurrentId} thrown an exception. ");
}/*, TaskCreationOptions.AttachedToParent*/);
}
thrownewException($"Task #{Task.CurrentId} thrown an exception. ");
}/*, TaskCreationOptions.AttachedToParent*/);
}
thrownewException($"Task #{Task.CurrentId} thrown an exception. ");
});
try
{
parent.Wait();
}
catch (AggregateException ae)
{
ae.Flatten().Handle(ex =>
{
Console.WriteLine(ex.Message);
returntrue;
});
}
运行上面的代码只会得到一行输出:
Task #1 thrown an exception.
看起来有点奇怪,为什么只捕获到一个异常呢?其实也是在情理之中的:任务默认只会把自身异常传递到它自己的调用线程,子任务是在父任务中调用的,其异常只会传递到父任务的执行线程,所以我们在父任务的调用线程,也就是我们的主线程中是捕获不到子任务的异常的。
取消上面代码的两处/*, TaskCreationOptions.AttachedToParent*/,就会捕获到所有异常。
6.4 任务调度器
6.4.1 .NET提供的任务调度器
任务是由TaskScheduler调度的,启动任务时,默认使用线程池任务调度器,任务将会被派发到线程池工作线程。线程池的调度前面已经总结过,这里不再展开。.NET提供的另一种任务调度器是同步上下文调度器,用TaskScheduler.FromCurrentSynchronizationContext()获取,这个调度器会把任务派发给当前的上下文线程,常用在GUI应用程序中。
例如,我们在一个窗体中新建一个ListBox,新建几个任务向其中添加项,代码如下:
this.lbxMsg.Items.Add($"{DateTime.Now:O}=>Current thread is thread #{Thread.CurrentThread.ManagedThreadId} .");
for(inti =0; i <10; i++)
{
newTask(() =>
{
for(intj =0; j <3; j++)
{
this.lbxMsg.Items.Add($"{DateTime.Now:O}=> Task #{Task.CurrentId} add an item with thread #{Thread.CurrentThread.ManagedThreadId}.");
}
}).Start(TaskScheduler.FromCurrentSynchronizationContext());
}
运行上面的代码可以发现创建的任务都是由界面线程执行的。这里如果使用默认的任务调度器将产生"线程间操作无效"的异常。
实际使用时,可以给一个异步任务添加延续任务,来处理异步任务的结果或者异常等。如下:
Task.Run(() =>
{
Thread.Sleep(3000);// 模拟操作过程
return1000;// 模拟结果
}).ContinueWith(t =>
{
this.lbxMsg.Items.Add(t.Result);// 在界面呈现结果或做其他处理
}, TaskScheduler.FromCurrentSynchronizationContext());
6.4.2 自定义任务调度器
除了使用.NET提供的调度器外,我们能够继承类TaskScheduler来实现自己的任务调度器。这里不再展开,需要了解的可以参考Samples for Parallel Programming with the .NET Framework。
7 线程安全的集合
对于并行任务,与其相关紧密的就是对一些共享资源,数据结构的并行访问。经常要做的就是对一些队列进行加锁-解锁,然后执行类似插入,删除等等互斥操作。 .NetFramework 4.0 中提供了一些封装好的支持并行操作数据容器,可以减少并行编程的复杂程度。
基本信息
.NetFramework中并行集合的名字空间: System.Collections.Concurrent
并行容器:
•ConcurrentQueue
•ConcurrentStack
•ConcurrentBag : 一个无序的数据结构集,当不需要考虑顺序时非常有用。
•BlockingCollection : 与经典的阻塞队列数据结构类似
•ConcurrentDictionary
这些集合在某种程度上使用了无锁技术(CAS Compare-and-Swap和内存屏障 Memory Barrier),与加互斥锁相比获得了性能的提升。但在串行程序中,最好不用这些集合,它们必然会影响性能。
关于CAS:
•http://www.tuicool.com/articles/zuui6z
•http://www.360doc.com/content/11/0914/16/7656248_148221200.shtml
关于内存屏障
•http://en.wikipedia.org/wiki/Memory_barrier
浙公网安备 33010602011771号