专栏首页喵叔's 专栏6搞懂线程池(二)

6搞懂线程池(二)

抱歉各位多线程专栏托更这么久,这篇文章我们继续讲线程池的相关知识,其中将涉及到如下知识:

  1. 取消异步操作
  2. 等待事件处理器及超时
  3. 计时器
  4. 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 更好。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 委托表示回调

    回调是一种由服务端提供一部反馈的机制,它在某些情况下会涉及到多线程或者为同步更新提供入口,在 C# 中如果要编写良好的回调那么就必须用委托来表示回调。委托我们经...

    喵叔
  • C# 事件

    使用委托的时候通常会出现两个角色,分别是广播者和订阅者。广播者包含委托字段,通过调用委托来决定何时进行广播。订阅者是方法目标的接收者,订阅者决定什么时候开始监听...

    喵叔
  • Flask架站基础篇(一)--环境配置

    喵叔
  • C#类库使用技巧

    remark:-. dll引用后,如果类库工程路径没有发生变更,在工程中点击F12依然能定位到源代码!

    zls365
  • 盘点GIS项目中的需求------查询。

    国内大部分GIS项目是根据标书来的,标书一开始也是根据GIS功能制定的。造成20多年来GIS项目和平台都千篇一律,技术和体验没有质的提高,也没有在客户工作中使用...

    MiaoGIS
  • 三层与mvc

    说明,每一张表对应有crud综合分析可以得知区别在于对应的类型不同以及一些参数不一样,

    wfaceboss
  • Layui 分页

    1、需要加载模块 laypage 2、 <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <...

    用户5760343
  • 工作被别人打断了怎么办?

    我们经常在自己设置的番茄钟也就是25分钟内遇到被打断的情况。这里我们从几个角度切入分析应该如何应对。

    RobinsonZhang
  • .NET CORE 框架ABP的代码生成器(ABP Code Power Tools )使用说明文档

    前言 各位好,又是一个多月没更新文章了。 原因嘛,大家都懂的,太忙了~ 临近年末,公司的项目、年会的做技术支持,同事朋友聚餐也比较频繁。 当然视频教程也没有继...

    角落的白板报
  • Makelog(造物记)在线编辑器使用

    因为比赛要在官方指定的平台上传文章,所以就研究一下使用。其实我和很喜欢造物记这个名字,有一种我是一个设计师的感jio~闲话不多说,开干~

    云深无际

扫码关注云+社区

领取腾讯云代金券