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 更好。


目录
相关文章
|
存储 缓存 Java
一文读懂线程池的实现原理
一文读懂线程池的实现原理
245 0
一文读懂线程池的实现原理
|
7月前
|
安全 Java
一文搞懂线程!!
一文搞懂线程!!
43 6
一文搞懂线程!!
|
7月前
|
消息中间件 前端开发 NoSQL
面试官:说说线程池的工作原理?
面试官:说说线程池的工作原理?
87 0
|
7月前
|
存储 SQL 编解码
面试必备的线程池知识-线程池的原理
面试必备的线程池知识-线程池的原理 线程池是一种多线程处理形式,它可以在执行大量短时间的任务时提高程序的性能和稳定性。线程池的核心思想是将需要执行的任务添加到线程池中,线程池会自动分配空闲线程来执行这些任务,当任务执行完毕后,线程会返回线程池中等待下一次任务的分配。
|
Java Spring
JAVA线程池相关原理
JAVA线程池相关原理
84 0
|
存储 SQL 监控
Java线程池实现原理详解
在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。在Java中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收。所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁。如何利用已有对象来服务就是一个需要解决的关键问题,其实这就是一些"池化资源"技术产生的原因
120 0
Java线程池实现原理详解
|
缓存 Java 数据库
Java并发 之 线程池系列 (1) 让多线程不再坑爹的线程池
Java并发 之 线程池系列 (1) 让多线程不再坑爹的线程池
135 0
Java并发 之 线程池系列 (1) 让多线程不再坑爹的线程池
线程池详解(通俗易懂超级好)
这个参数的设计完全参考系统运行环境和硬件压力设定,没有固定的参考值,用户可以根据经验和系统产生任务的时间间隔合理设置一个值即可;
线程池详解(通俗易懂超级好)
线程池:第一章:线程池的底层原理
线程池:第一章:线程池的底层原理
线程池:第一章:线程池的底层原理
|
存储 监控 Java
Java线程池理解与学习
线程过多就容易引发内存溢出,因此我们有必要使用线程池的技术 线程池的好处 降低资源消耗: 通过重复利用已创建的线程降低线程创建和销毁造成的消耗 提高响应速度: 当任务到达时,任务可以不需要等待线程创建就能立即执行 提高线程管理性: 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源还会降低系统的稳定性,使用线程池可以进行统一的分配、调优和监控
69 0
Java线程池理解与学习