6搞懂线程池(二)
抱歉各位多线程专栏托更这么久,这篇文章我们继续讲线程池的相关知识,其中将涉及到如下知识:
- 取消异步操作
- 等待事件处理器及超时
- 计时器
- BackgroundWorker
零、取消异步操作
这一小节将引入两个类 CancellationTokenSource 和 CancellationToken 。这两个类是在 .NET 4.0 中被引入的,因此如果需要使用这两个类我们必须在 .NET 4.0 及其以上版本中使用,目前是取消异步操作的标准。下面我们通过厨师做饭,中途撤销订单的例子来看一下这两个类具体该怎么用。
using System.Threading;
using static System.Console;
using static System.Threading.Thread;
namespace NoSix
{
class Program
{
static void Main(string[] args)
{
using(var cts=new CancellationTokenSource())
{
CancellationToken token = cts.Token;
ThreadPool.QueueUserWorkItem(_ => Cookie(token));
Sleep(2000);
cts.Cancel();
}
Read();
}
static void Cookie(CancellationToken token)
{
WriteLine("开始做饭.......");
for (int i = 0; i < 5; i++)
{
if (token.IsCancellationRequested)
{
WriteLine("取消做饭");
return;
}
Sleep(2000);
}
WriteLine("我做完饭了");
}
}
}
在上面的代码中我们在 Cookie 方法中通过轮询的方式来检查 CancellationToken.IsCancellationRequested 属性。如果该属性为 true ,则说明操作需要被取消,我们必须放弃该操作。下面我们将 Cookie 方法修改一下,用另一种方式来实现取消操作
static void Cookie(CancellationToken token)
{
try
{
WriteLine("开始做饭.......");
for (int i = 0; i < 5; i++)
{
token.ThrowIfCancellationRequested();
Sleep(2000);
}
WriteLine("我做完饭了");
}
catch(OperationCanceledException)
{
WriteLine("取消做饭");
}
}
这种方法我们抛出一个 OperationCancelledException 异常。这允许我们在线程池之外控制取消执行过程。需要取消操作时通过操作之外的代码来处理。下面我们再来修改一下 Cookie 方法,用第三种方法来是先取消操作。
static void Cookie(CancellationToken token)
{
WriteLine("开始做饭.......");
bool cancellationFlag = false;
token.Register(() => cancellationFlag = true);
for (int i = 0; i < 5; i++)
{
if (cancellationFlag)
{
WriteLine("取消做饭");
return;
}
Sleep(2000);
}
WriteLine("我做完饭了");
}
第三种方式是注册一个回调函数。操作被取消时线程池将调用该回调函数。.NET 可以链式的传递一个取消逻辑到另一个异步操作中。
一、等待事件处理器及超时
在线程池中存在一个非常棒的方法 RegisterWaitForSingleObject 。它允许我们把回调函数放入线程池,每当等待事件处理器收到信号或者等待超时时将执行这个回调函数。下面的代码通过模拟初始等待下单做饭,到了下班时间(超时)后就停止接单。
using System;
using System.Threading;
using static System.Console;
using static System.Threading.Thread;
namespace RegisterWaitForSingleObject
{
class Program
{
static void Main(string[] args)
{
Cookie(TimeSpan.FromSeconds(5));
Cookie(TimeSpan.FromSeconds(7));
Read();
}
static void Cookie(TimeSpan timeSpan)
{
using (var evt = new ManualResetEvent(false))
using (var cts = new CancellationTokenSource())
{
WriteLine("等待做饭");
var cookie = ThreadPool.RegisterWaitForSingleObject(evt, (state, isTimeOut) => CookieWait(cts, isTimeOut), null, timeSpan, true);
ThreadPool.QueueUserWorkItem(_ => WorkOperation(cts.Token, evt));
Sleep(2000);
cookie.Unregister(evt);
}
}
private static void WorkOperation(CancellationToken token, ManualResetEvent evt)
{
for (int i = 0; i < 6; i++)
{
if (token.IsCancellationRequested)
{
return;
}
Sleep(1000);
}
evt.Set();
}
private static void CookieWait(CancellationTokenSource cts, bool isTimeOut)
{
if (isTimeOut)
{
cts.Cancel();
WriteLine("我下班了!!!");
}
else
{
WriteLine("开始做饭!!!");
}
}
}
}
我们注册了处理超时的异步操作。当接收到了 ManualRestEvent 对象的信号,工作者操作成功完成后会发出信号。如果操作完成之前超时,那么会使用 CancellationToken 来取消第一个操作。我们向线程池中放入一个耗时长的操作。它会运行 6 秒钟,如果成功完成则会设置一个 ManualResetEvent 信号类。在其他情况下,比如需要取消该操作,那么该操作会被丢弃。最后,为操作提供5秒的超时时间是不够的。这是因为操作会花费 6 秒来完成,只能取消该操作。所以如果提供 7 秒的超时时间是可行的,该操作会顺利完成。在有大量线程处于阻塞状态等待线程事件信号时这种方式非常有用。
二、计时器
我们前面所讲的都是一次性调用,那么如何进行周期性调用呢?这时我们就用到了计时器功能,下面我们通过例子来看一下。
using System;
using System.Threading;
using static System.Console;
using static System.Threading.Thread;
namespace _Timer_
{
class Program
{
static void Main(string[] args)
{
WriteLine("点击回车暂停计时器");
timer = new Timer(_ => TimerOpration(DateTime.Now), null, 1000, 2000);
try
{
Sleep(6000);
timer.Change(1000, 4000);
Read();
}
finally
{
timer.Dispose();
}
}
static Timer timer;
static void TimerOpration(DateTime dateTime)
{
TimeSpan elapsed = DateTime.Now - dateTime;
WriteLine($"{elapsed.Seconds} {dateTime} {CurrentThread.ManagedThreadId}");
}
}
}
我们首先创建 TimerOpration 方法传递一个起始时间,在方法中我们计算运行的时间差,并打印出来。同时我们打印出起始时间和进程 ID 。然后我们在主方法中初始化 Timer,第一个参数传入的时一个 lambda 表达式,它会在线程池中被执行。第二个参数时 null,是因为我们不需要知道用户状态对象。接着第三个参数指定了调用 TimerOpration 之前延迟的时间,也就是说延迟 N 秒后执行第一次。第四个参数代表间隔多久执行一次 TimerOpration 。最后我们 6 秒后我们修改计时器,在调用 Change 一秒后启动运行 TimerOpration 方法,以后每间隔 4 秒运行一次。
三、BackgroundWorker
在这一小节我们将不使用线程池和委托而是使用了事件。事件表示了一些通知的源或当通知到达时会有所响应的一系列订阅者。下面我们先来看一下例子。
using System;
using System.ComponentModel;
using static System.Console;
using static System.Threading.Thread;
namespace Background_Worker
{
class Program
{
static void Main(string[] args)
{
BackgroundWorker bw = new BackgroundWorker();
bw.WorkerReportsProgress = true;
bw.WorkerSupportsCancellation = true;
bw.DoWork += DoWork;
bw.ProgressChanged += ProgressChanged;
bw.RunWorkerCompleted += CompletedChanged;
bw.RunWorkerAsync();
WriteLine("输入E取消");
do
{
if(ReadKey(true).KeyChar=='E')
{
bw.CancelAsync();
}
} while (bw.IsBusy);
}
static void DoWork(object sender, DoWorkEventArgs e)
{
WriteLine($"DoWork 线程池线程ID: {CurrentThread.ManagedThreadId}");
BackgroundWorker bw = (BackgroundWorker)sender;
for (int i = 1; i <= 100; i++)
{
if (bw.CancellationPending)
{
e.Cancel = true;
return;
}
if (i % 10 == 0)
{
bw.ReportProgress(i);
}
Sleep(100);
}
e.Result = 42;
}
static void ProgressChanged(object sender, ProgressChangedEventArgs e)
{
WriteLine($"{e.ProgressPercentage} 已完成 。Progress 线程池线程ID: {CurrentThread.ManagedThreadId}");
}
static void CompletedChanged(object sender, RunWorkerCompletedEventArgs e)
{
WriteLine($"Completed 线程池线程ID: {CurrentThread.ManagedThreadId}");
if (e.Error != null)
{
WriteLine($"异常信息: {e.Error.Message} ");
}
else if (e.Cancelled)
{
WriteLine($"操作被取消");
}
else
{
WriteLine($"答案是: {e.Result}");
}
}
}
}
上述代码中我们创建了 BackgroundWorker 组件的实例。显式指出该后台工作者线程支持取消操作及该操作进度的通知。我们还定义了三个事件,当事件发生时会调用响应的事件处理器。每当事件通知订阅者时就会将具有特殊的定义签名的方法将被调用。我们可以只启动一个异步操作然后订阅给不同的事件。事件在操作执行时会被触发,这种方式被称为基于事件的异步模式。我们定义的 DoWork 事件会在后台工作对象通过 RunWorkerAsync 方法启动一个异步操作时被调用。我们在得到结果后将结果设置给事件参数,接着会运行 RunWorkerCompleted 事件处理器。在该方法中可以知道操作是成功完成、发生错误或被取消。BackgroundWorker 主要用于 WPF 中,通过后台工作事件处理器代码可以直接与 UI 控制器交互。与直接在线程池中与 UI 控制器交互的方式相比较,使用 BackgroundWorker 更好。