C#多线程之线程池篇3,

图片 5

目录

C#多线程之线程池篇3,

  在上一篇C#多线程之线程池篇2中,我们主要学习了线程池和并行度以及如何实现取消选项的相关知识。在这一篇中,我们主要学习如何使用等待句柄和超时、使用计时器和使用BackgroundWorker组件的相关知识。

五、使用等待句柄和超时

  在这一小节中,我们将学习如何在线程池中实现超时和正确地实现等待。具体操作步骤如下:

1、使用Visual Studio 2015创建一个新的控制台应用程序。

2、双击打开“Program.cs”文件,编写代码如下所示:

 1 using System;
 2 using System.Threading;
 3 using static System.Console;
 4 using static System.Threading.Thread;
 5 
 6 namespace Recipe05
 7 {
 8     class Program
 9     {
10         // CancellationTokenSource:通知System.Threading.CancellationToken,告知其应被取消。
11         static void WorkerOperationWait(CancellationTokenSource cts, bool isTimedOut)
12         {
13             if (isTimedOut)
14             {
15                 // 传达取消请求。
16                 cts.Cancel();
17                 WriteLine("Worker operation timed out and was canceled.");
18             }
19             else
20             {
21                 WriteLine("Worker operation succeeded.");
22             }
23         }
24 
25         // CancellationToken:传播有关应取消操作的通知。
26         // ManualResetEvent:通知一个或多个正在等待的线程已发生事件。
27         static void WorkerOperation(CancellationToken token, ManualResetEvent evt)
28         {
29             for (int i = 0; i < 6; i++)
30             {
31                 // 获取是否已请求取消此标记。如果已请求取消此标记,则为 true;否则为 false。
32                 if (token.IsCancellationRequested)
33                 {
34                     return;
35                 }
36                 Sleep(TimeSpan.FromSeconds(1));
37             }
38             // 将事件状态设置为终止状态,允许一个或多个等待线程继续。
39             evt.Set();
40         }
41 
42         static void RunOperations(TimeSpan workerOperationTimeout)
43         {
44             using (var evt = new ManualResetEvent(false))
45             using (var cts = new CancellationTokenSource())
46             {
47                 // 注册一个等待System.Threading.WaitHandle的委托,并指定一个System.TimeSpan值来表示超时时间。
48                 // 第一个参数:要注册的System.Threading.WaitHandle。使用System.Threading.WaitHandle而非 System.Threading.Mutex。
49                 // 第二个参数:waitObject参数终止时调用的System.Threading.WaitOrTimerCallback 委托。
50                 // 第三个参数:传递给委托的对象。
51                 // 第四个参数:System.TimeSpan表示的超时时间。如果timeout为0(零),则函数将测试对象的状态并立即返回。如果timeout为 -1,则函数的超时间隔永远不过期。
52                 // 第五个参数:如果为true,表示在调用了委托后,线程将不再在waitObject参数上等待;如果为false,表示每次完成等待操作后都重置计时器,直到注销等待。
53                 // 返回值:封装本机句柄的System.Threading.RegisteredWaitHandle。
54                 var worker = ThreadPool.RegisterWaitForSingleObject(evt, (state, isTimedOut) => WorkerOperationWait(cts, isTimedOut), null, workerOperationTimeout, true);
55 
56                 WriteLine("Starting long running operation...");
57                 // ThreadPool.QueueUserWorkItem:将方法排入队列以便执行。此方法在有线程池线程变得可用时执行。
58                 // cts.Token:获取与此System.Threading.CancellationTokenSource关联的System.Threading.CancellationToken。
59                 ThreadPool.QueueUserWorkItem(_ => WorkerOperation(cts.Token, evt));
60 
61                 Sleep(workerOperationTimeout.Add(TimeSpan.FromSeconds(2)));
62 
63                 // 取消由System.Threading.ThreadPool.RegisterWaitForSingleObject方法发出的已注册等待操作。
64                 worker.Unregister(evt);
65             }
66         }
67 
68         static void Main(string[] args)
69         {
70             // 实现超时
71             RunOperations(TimeSpan.FromSeconds(5));
72             // 实现等待
73             RunOperations(TimeSpan.FromSeconds(7));
74         }
75     }
76 }

3、运行该控制台应用程序,运行效果如下图所示:

图片 1

  线程池还有另一个有用的方法:ThreadPool.RegisterWaitForSingleObject,该方法允许我们将回调方法放入线程池的队列中,当所提供的等待句柄发送信号或者超时发生时,该回调方法即被执行。这允许我们对线程池中的操作实现超时。

  在第71行代码处,我们在主线程中调用了“RunOperations”方法,并给它的workerOperationTimeout参数传递了数值5,表示超时时间为5秒。

  在第54行代码处,我们调用了ThreadPool的“RegisterWaitForSingleObject”静态方法,并指定了回调方法所要执行的操作是“WorkerOperationWait”方法,超时时间是5秒。

  在第59行代码处,我们调用ThreadPool的“QueueUserWorkItem”静态方法来执行“WorkerOperation”方法,而该方法所消耗的时间为6秒,在这六秒中内已经在线程池中发送了超时,所以会执行第13~18行和第32~35行处的代码。

  在第73行代码处,我们传递了数值7给“RunOperations”方法,设置线程池的超时时间为7秒,因为“WorkerOperation”方法的执行时间为6秒,所以在这种情况下没有发生超时,成功执行完毕“WorkerOperation”方法。

 六、使用计时器

  在这一小节中,我们将学习如何使用System.Threading.Timer对象在线程池中定期地调用一个异步操作。具体操作步骤如下所示:

1、使用Visual Studio 2015创建一个新的控制台应用程序。

2、双击打开“Program.cs”文件,编写代码如下所示:

 1 using System;
 2 using System.Threading;
 3 using static System.Console;
 4 using static System.Threading.Thread;
 5 
 6 namespace Recipe06
 7 {
 8     class Program
 9     {
10         static Timer timer;
11 
12         static void TimerOperation(DateTime start)
13         {
14             TimeSpan elapsed = DateTime.Now - start;
15             WriteLine($"{elapsed.Seconds} seconds from {start}. Timer thread pool thread id: {CurrentThread.ManagedThreadId}");
16         }
17 
18         static void Main(string[] args)
19         {
20             WriteLine("Press 'Enter' to stop the timer...");
21             DateTime start = DateTime.Now;
22             // 初始化Timer类的新实例,使用System.TimeSpan值来度量时间间隔。
23             // 第一个参数:一个System.Threading.TimerCallback委托,表示要执行的方法。
24             // 第二个参数:一个包含回调方法要使用的信息的对象,或者为null。
25             // 第三个参数:System.TimeSpan,表示在callback参数调用它的方法之前延迟的时间量。指定-1毫秒以防止启动计时器。指定零(0)可立即启动计时器。
26             // 第四个参数:在调用callback所引用的方法之间的时间间隔。指定-1毫秒可以禁用定期终止。
27             timer = new Timer(_ => TimerOperation(start), null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(2));
28             try
29             {
30                 Sleep(TimeSpan.FromSeconds(6));
31                 // 更改计时器的启动时间和方法调用之间的时间间隔,使用System.TimeSpan值度量时间间隔。
32                 // 第一个参数:一个System.TimeSpan,表示在调用构造System.Threading.Timer时指定的回调方法之前的延迟时间量。指定负-1毫秒以防止计时器重新启动。指定零(0)可立即重新启动计时器。
33                 // 第二个参数:在构造System.Threading.Timer时指定的回调方法调用之间的时间间隔。指定-1毫秒可以禁用定期终止。
34                 timer.Change(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(4));
35                 ReadLine();
36             }
37             finally
38             {
39                 timer.Dispose();
40             }
41         }
42     }
43 }

3、运行该控制台应用程序,运行效果(每次运行效果可能不同)如下图所示:

图片 2

  首先,我们创建了一个Timer实例,它的构造方法的第一个参数是一个lambda表达式,表示要在线程池中执行的代码,在该表达式中我们调用了“TimerOperation”方法,并给它提供了一个开始时间值。由于我们没有使用state对象,因此我们给Timer的构造方法的第二个参数传递了null。第三个参数表示第一次执行“TimerOperation”所要花费的时间为1秒钟。第四个参数表示每次调用“TimerOperation”之间的时间间隔为2秒钟。

  在主线程阻塞6秒钟之后,我们调用了Timer实例的“Change”方法,更改了每次调用“TimerOperation”之间的时间间隔为4秒钟。

  最后,我们等待输入“Enter”键来结束应用程序。

七、使用BackgroundWorker组件

   在这一小节中,我们学习另外一种异步编程的方式:BackgroundWorker组件。在这个组件的帮助下,我们可以通过一系列事件和事件处理方法组织我们的异步代码。具体操作步骤如下所示:

1、使用Visual Studio 2015创建一个新的控制台应用程序。

2、双击打开“Program.cs”文件,编写代码如下所示:

  1 using System;
  2 using System.ComponentModel;
  3 using System.Threading;
  4 using static System.Console;
  5 using static System.Threading.Thread;
  6 
  7 namespace Recipe07
  8 {
  9     class Program
 10     {
 11         static void WorkerDoWork(object sender, DoWorkEventArgs e)
 12         {
 13             WriteLine($"DoWork thread pool thread id: {CurrentThread.ManagedThreadId}");
 14             var bw = (BackgroundWorker)sender;
 15             for (int i = 1; i <= 100; i++)
 16             {
 17                 // 获取一个值,指示应用程序是否已请求取消后台操作。
 18                 // 如果应用程序已请求取消后台操作,则为 true;否则为 false。默认值为 false。
 19                 if (bw.CancellationPending)
 20                 {
 21                     e.Cancel = true;
 22                     return;
 23                 }
 24 
 25                 if (i % 10 == 0)
 26                 {
 27                     // 引发 System.ComponentModel.BackgroundWorker.ProgressChanged 事件。
 28                     // 参数:已完成的后台操作所占的百分比,范围从 0% 到 100%。
 29                     bw.ReportProgress(i);
 30                 }
 31 
 32                 Sleep(TimeSpan.FromSeconds(0.1));
 33             }
 34 
 35             // 获取或设置表示异步操作结果的值。
 36             e.Result = 42;
 37         }
 38 
 39         static void WorkerProgressChanged(object sender, ProgressChangedEventArgs e)
 40         {
 41             // e.ProgressPercentage:获取异步任务的进度百分比。
 42             WriteLine($"{e.ProgressPercentage}% completed. Progress thread pool thread id: {CurrentThread.ManagedThreadId}");
 43         }
 44 
 45         static void WorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
 46         {
 47             WriteLine($"Completed thread pool thread id: {CurrentThread.ManagedThreadId}");
 48             // e.Error:获取一个值,该值指示异步操作期间发生的错误。
 49             if (e.Error != null)
 50             {
 51                 // 打印出异步操作期间发生的错误信息。
 52                 WriteLine($"Exception {e.Error.Message} has occured.");
 53             }
 54             else if (e.Cancelled) // 获取一个值,该值指示异步操作是否已被取消。
 55             {
 56                 WriteLine($"Operation has been canceled.");
 57             }
 58             else
 59             {
 60                 // e.Result:获取表示异步操作结果的值。
 61                 WriteLine($"The answer is: {e.Result}");
 62             }
 63         }
 64 
 65         static void Main(string[] args)
 66         {
 67             // 初始化System.ComponentModel.BackgroundWorker类的新实例。该类在单独的线程上执行操作。
 68             var bw = new BackgroundWorker();
 69             // 获取或设置一个值,该值指示System.ComponentModel.BackgroundWorker能否报告进度更新。
 70             // 如果System.ComponentModel.BackgroundWorker支持进度更新,则为true;否则为false。默认值为false。
 71             bw.WorkerReportsProgress = true;
 72             // 获取或设置一个值,该值指示System.ComponentModel.BackgroundWorker是否支持异步取消。
 73             // 如果System.ComponentModel.BackgroundWorker支持取消,则为true;否则为false。默认值为false。
 74             bw.WorkerSupportsCancellation = true;
 75 
 76             // 调用System.ComponentModel.BackgroundWorker.RunWorkerAsync时发生。
 77             bw.DoWork += WorkerDoWork;
 78             // 调用System.ComponentModel.BackgroundWorker.ReportProgress(System.Int32)时发生。
 79             bw.ProgressChanged += WorkerProgressChanged;
 80             // 当后台操作已完成、被取消或引发异常时发生。
 81             bw.RunWorkerCompleted += WorkerCompleted;
 82 
 83             // 开始执行后台操作。
 84             bw.RunWorkerAsync();
 85 
 86             WriteLine("Press C to cancel work");
 87 
 88             do
 89             {
 90                 // 获取用户按下的下一个字符或功能键。按下的键可以选择显示在控制台窗口中。
 91                 // 确定是否在控制台窗口中显示按下的键。如果为 true,则不显示按下的键;否则为 false。
 92                 if (ReadKey(true).KeyChar == 'C')
 93                 {
 94                     // 请求取消挂起的后台操作。
 95                     bw.CancelAsync();
 96                 }
 97             }
 98             // 获取一个值,指示System.ComponentModel.BackgroundWorker是否正在运行异步操作。
 99             // 如果System.ComponentModel.BackgroundWorker正在运行异步操作,则为true;否则为false。
100             while (bw.IsBusy);
101         }
102     }
103 }

3、运行该控制台应用程序,运行效果(每次运行效果可能不同)如下图所示:

图片 3

   在第68行代码处,我们创建了一个BackgroundWorker组件的实例,并且在第71行代码和第74行代码处明确地说明该实例支持进度更新和异步取消操作。

  在第77行代码、第79行代码和第81行代码处,我们给该实例挂载了三个事件处理方法。每当DoWork、ProgressChanged和RunWorkerCompleted事件发生时,都会执行相应的“WorkerDoWork方法”、“WorkerProgressChanged”方法和“WorkerCompleted”方法。

  其他代码请参考注释。

  源码下载

在上一篇C#多线程之线程池篇2中,我们主要学习了线程池和并行度以及如何实现取消选项的相关知识。在这一篇中,…

  • 1.1
    简介
  • 1.2
    在线程池中调用委托
  • 1.3
    向线程池中放入异步操作
  • 1.4
    线程池与并行度
  • 1.5
    实现一个取消选项
  • 1.6
    在线程池中使用等待事件处理器及超时
  • 1.7
    使用计时器
  • 1.8
    使用BackgroundWorker组件
  • 参考书籍
  • 笔者水平有限,如果错误欢迎各位批评指正!


1.1 简介

在本章中,主要介绍线程池(ThreadPool)的使用;在C#中它叫System.Threading.ThreadPool,在使用线程池之前首先我们得明白一个问题,那就是为什么要使用线程池。其主要原因是创建一个线程的代价是昂贵的,创建一个线程会消耗很多的系统资源。

那么线程池是如何解决这个问题的呢?线程池在初始时会自动创建一定量的线程供程序调用,使用时,开发人员并不直接分配线程,而是将需要做的工作放入线程池工作队列中,由线程池分配已有的线程进行处理,等处理完毕后线程不是被销毁,而是重新回到线程池中,这样节省了创建线程的开销。

但是在使用线程池时,需要注意以下几点,这将非常重要。

  • 线程池不适合处理长时间运行的作业,或者处理需要与其它线程同步的作业。
  • 避免将线程池中的工作线程分配给I/O首先的任务,这种任务应该使用TPL模型。
  • 如非必须,不要手动设置线程池的最小线程数和最大线程数,CLR会自动的进行线程池的扩张和收缩,手动干预往往让性能更差。

1.2 在线程池中调用委托

本节展示的是如何在线程池中如何异步的执行委托,然后将介绍一个叫异步编程模型(Asynchronous
Programming Model,简称APM)
的异步编程方式。

在本节及以后,为了降低代码量,在引用程序集声明位置默认添加了using static System.Consoleusing static System.Threading.Thead声明,这样声明可以让我们在程序中少些一些意义不大的调用语句。

演示代码如下所示,使用了普通创建线程和APM方式来执行同一个任务。

static void Main(string[] args)
{
    int threadId = 0;

    RunOnThreadPool poolDelegate = Test;

    var t = new Thread(() => Test(out threadId));
    t.Start();
    t.Join();

    WriteLine($"手动创建线程 Id: {threadId}");

    // 使用APM方式 进行异步调用  异步调用会使用线程池中的线程
    IAsyncResult r = poolDelegate.BeginInvoke(out threadId, Callback, "委托异步调用");
    r.AsyncWaitHandle.WaitOne();

    // 获取异步调用结果
    string result = poolDelegate.EndInvoke(out threadId, r);

    WriteLine($"Thread - 线程池工作线程Id: {threadId}");
    WriteLine(result);

    Console.ReadLine();
}

// 创建带一个参数的委托类型
private delegate string RunOnThreadPool(out int threadId);

private static void Callback(IAsyncResult ar)
{
    WriteLine("Callback - 开始运行Callback...");
    WriteLine($"Callback - 回调传递状态: {ar.AsyncState}");
    WriteLine($"Callback - 是否为线程池线程: {CurrentThread.IsThreadPoolThread}");
    WriteLine($"Callback - 线程池工作线程Id: {CurrentThread.ManagedThreadId}");
}

private static string Test(out int threadId)
{
    string isThreadPoolThread = CurrentThread.IsThreadPoolThread ? "ThreadPool - ": "Thread - ";

    WriteLine($"{isThreadPoolThread}开始运行...");
    WriteLine($"{isThreadPoolThread}是否为线程池线程: {CurrentThread.IsThreadPoolThread}");
    Sleep(TimeSpan.FromSeconds(2));
    threadId = CurrentThread.ManagedThreadId;
    return $"{isThreadPoolThread}线程池工作线程Id: {threadId}";
}

运行结果如下图所示,其中以Thread开头的为手动创建的线程输出的信息,而TheadPool为开始线程池任务输出的信息,Callback为APM模式运行任务结束后,执行的回调方法,可以清晰的看到,Callback的线程也是线程池的工作线程。

图片 4

在上文中,使用BeginOperationName/EndOperationName方法和.Net中的IAsyncResult对象的方式被称为异步编程模型(或APM模式),这样的方法被称为异步方法。使用委托的BeginInvoke方法来运行该委托,BeginInvoke接收一个回调函数,该回调函数会在任务处理完成后背调用,并且可以传递一个用户自定义的状态给回调函数。

现在这种APM编程方式用的越来越少了,更推荐使用任务并行库(Task Parallel
Library,简称TPL)
来组织异步API。

1.3 向线程池中放入异步操作

本节将介绍如何将异步操作放入线程池中执行,并且如何传递参数给线程池中的线程。本节中主要用到的是ThreadPool.QueueUserWorkItem()方法,该方法可将需要运行的任务通过委托的形式传递给线程池中的线程,并且允许传递参数。

使用比较简单,演示代码如下所示。演示了线程池使用中如何传递方法和参数,最后需要注意的是使用了Lambda表达式和它的闭包机制。

static void Main(string[] args)
{
    const int x = 1;
    const int y = 2;
    const string lambdaState = "lambda state 2";

    // 直接将方法传递给线程池
    ThreadPool.QueueUserWorkItem(AsyncOperation);
    Sleep(TimeSpan.FromSeconds(1));

    // 直接将方法传递给线程池 并且 通过state传递参数
    ThreadPool.QueueUserWorkItem(AsyncOperation, "async state");
    Sleep(TimeSpan.FromSeconds(1));

    // 使用Lambda表达式将任务传递给线程池 并且通过 state传递参数
    ThreadPool.QueueUserWorkItem(state =>
    {
        WriteLine($"Operation state: {state}");
        WriteLine($"工作线程 id: {CurrentThread.ManagedThreadId}");
        Sleep(TimeSpan.FromSeconds(2));
    }, "lambda state");

    // 使用Lambda表达式将任务传递给线程池 通过 **闭包** 机制传递参数
    ThreadPool.QueueUserWorkItem(_ =>
    {
        WriteLine($"Operation state: {x + y}, {lambdaState}");
        WriteLine($"工作线程 id: {CurrentThread.ManagedThreadId}");
        Sleep(TimeSpan.FromSeconds(2));
    }, "lambda state");

    ReadLine();
}

private static void AsyncOperation(object state)
{
    WriteLine($"Operation state: {state ?? "(null)"}");
    WriteLine($"工作线程 id: {CurrentThread.ManagedThreadId}");
    Sleep(TimeSpan.FromSeconds(2));
}

运行结果如下图所示。

图片 5

You can leave a response, or trackback from your own site.

Leave a Reply

网站地图xml地图