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的线程也是线程池的工作线程。

在上文中,使用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));
}

运行结果如下图所示。

1.4 线程池与并行度

在本节中,主要是使用普通创建线程和使用线程池内的线程在任务量比较大的情况下有什么区别,我们模拟了一个场景,创建了很多不同的线程,然后分别使用普通创建线程方式和线程池方式看看有什么不同。

static void Main(string[] args)
{
const int numberOfOperations = 500;
var sw = new Stopwatch();
sw.Start();
UseThreads(numberOfOperations);
sw.Stop();
WriteLine($"使用线程执行总用时: {sw.ElapsedMilliseconds}"); sw.Reset();
sw.Start();
UseThreadPool(numberOfOperations);
sw.Stop();
WriteLine($"使用线程池执行总用时: {sw.ElapsedMilliseconds}"); Console.ReadLine();
} static void UseThreads(int numberOfOperations)
{
using (var countdown = new CountdownEvent(numberOfOperations))
{
WriteLine("通过创建线程调度工作");
for (int i = 0; i < numberOfOperations; i++)
{
var thread = new Thread(() =>
{
Write($"{CurrentThread.ManagedThreadId},");
Sleep(TimeSpan.FromSeconds(0.1));
countdown.Signal();
});
thread.Start();
}
countdown.Wait();
WriteLine();
}
} static void UseThreadPool(int numberOfOperations)
{
using (var countdown = new CountdownEvent(numberOfOperations))
{
WriteLine("使用线程池开始工作");
for (int i = 0; i < numberOfOperations; i++)
{
ThreadPool.QueueUserWorkItem(_ =>
{
Write($"{CurrentThread.ManagedThreadId},");
Sleep(TimeSpan.FromSeconds(0.1));
countdown.Signal();
});
}
countdown.Wait();
WriteLine();
}
}

执行结果如下,可见使用原始的创建线程执行,速度非常快。只花了2秒钟,但是创建了500多个线程,而使用线程池相对来说比较慢,花了9秒钟,但是只创建了很少的线程,为操作系统节省了线程和内存空间,但花了更多的时间。

1.5 实现一个取消选项

在之前的文章中有提到,如果需要终止一个线程的执行,那么可以使用Abort()方法,但是有诸多的原因并不推荐使用Abort()方法。

这里推荐的方式是使用协作式取消(cooperative cancellation),这是一种可靠的技术来安全取消不再需要的任务。其主要用到CancellationTokenSourceCancellationToken两个类,具体用法见下面演示代码。

以下延时代码主要是实现了使用CancellationTokenCancellationTokenSource来实现任务的取消。但是任务取消后可以进行三种操作,分别是:直接返回、抛出ThrowIfCancellationRequesed异常和执行回调。详细请看代码。

static void Main(string[] args)
{
// 使用CancellationToken来取消任务 取消任务直接返回
using (var cts = new CancellationTokenSource())
{
CancellationToken token = cts.Token;
ThreadPool.QueueUserWorkItem(_ => AsyncOperation1(token));
Sleep(TimeSpan.FromSeconds(2));
cts.Cancel();
} // 取消任务 抛出 ThrowIfCancellationRequesed 异常
using (var cts = new CancellationTokenSource())
{
CancellationToken token = cts.Token;
ThreadPool.QueueUserWorkItem(_ => AsyncOperation2(token));
Sleep(TimeSpan.FromSeconds(2));
cts.Cancel();
} // 取消任务 并 执行取消后的回调函数
using (var cts = new CancellationTokenSource())
{
CancellationToken token = cts.Token;
token.Register(() => { WriteLine("第三个任务被取消,执行回调函数。"); });
ThreadPool.QueueUserWorkItem(_ => AsyncOperation3(token));
Sleep(TimeSpan.FromSeconds(2));
cts.Cancel();
} ReadLine();
} static void AsyncOperation1(CancellationToken token)
{
WriteLine("启动第一个任务.");
for (int i = 0; i < 5; i++)
{
if (token.IsCancellationRequested)
{
WriteLine("第一个任务被取消.");
return;
}
Sleep(TimeSpan.FromSeconds(1));
}
WriteLine("第一个任务运行完成.");
} static void AsyncOperation2(CancellationToken token)
{
try
{
WriteLine("启动第二个任务."); for (int i = 0; i < 5; i++)
{
token.ThrowIfCancellationRequested();
Sleep(TimeSpan.FromSeconds(1));
}
WriteLine("第二个任务运行完成.");
}
catch (OperationCanceledException)
{
WriteLine("第二个任务被取消.");
}
} static void AsyncOperation3(CancellationToken token)
{
WriteLine("启动第三个任务.");
for (int i = 0; i < 5; i++)
{
if (token.IsCancellationRequested)
{
WriteLine("第三个任务被取消.");
return;
}
Sleep(TimeSpan.FromSeconds(1));
}
WriteLine("第三个任务运行完成.");
}

运行结果如下所示,符合预期结果。

1.6 在线程池中使用等待事件处理器及超时

本节将介绍如何在线程池中使用等待任务和如何进行超时处理,其中主要用到ThreadPool.RegisterWaitForSingleObject()方法,该方法允许传入一个WaitHandle对象,和需要执行的任务、超时时间等。通过使用这个方法,可完成线程池情况下对超时任务的处理。

演示代码如下所示,运行了两次使用ThreadPool.RegisterWaitForSingleObject()编写超时代码的RunOperations()方法,但是所传入的超时时间不同,所以造成一个必然超时和一个不会超时的结果。

static void Main(string[] args)
{
// 设置超时时间为 5s WorkerOperation会延时 6s 肯定会超时
RunOperations(TimeSpan.FromSeconds(5)); // 设置超时时间为 7s 不会超时
RunOperations(TimeSpan.FromSeconds(7));
} static void RunOperations(TimeSpan workerOperationTimeout)
{
using (var evt = new ManualResetEvent(false))
using (var cts = new CancellationTokenSource())
{
WriteLine("注册超时操作...");
// 传入同步事件 超时处理函数 和 超时时间
var worker = ThreadPool.RegisterWaitForSingleObject(evt
, (state, isTimedOut) => WorkerOperationWait(cts, isTimedOut)
, null
, workerOperationTimeout
, true); WriteLine("启动长时间运行操作...");
ThreadPool.QueueUserWorkItem(_ => WorkerOperation(cts.Token, evt)); Sleep(workerOperationTimeout.Add(TimeSpan.FromSeconds(2))); // 取消注册等待的操作
worker.Unregister(evt); ReadLine();
}
} static void WorkerOperation(CancellationToken token, ManualResetEvent evt)
{
for (int i = 0; i < 6; i++)
{
if (token.IsCancellationRequested)
{
return;
}
Sleep(TimeSpan.FromSeconds(1));
}
evt.Set();
} static void WorkerOperationWait(CancellationTokenSource cts, bool isTimedOut)
{
if (isTimedOut)
{
cts.Cancel();
WriteLine("工作操作超时并被取消.");
}
else
{
WriteLine("工作操作成功.");
}
}

运行结果如下图所示,与预期结果相符。

1.7 使用计时器

计时器是FCL提供的一个类,叫System.Threading.Timer,可要结果与创建周期性的异步操作。该类使用比较简单。

以下的演示代码使用了定时器,并设置了定时器延时启动时间和周期时间。

static void Main(string[] args)
{
WriteLine("按下回车键,结束定时器...");
DateTime start = DateTime.Now; // 创建定时器
_timer = new Timer(_ => TimerOperation(start), null
, TimeSpan.FromSeconds(1)
, TimeSpan.FromSeconds(2));
try
{
Sleep(TimeSpan.FromSeconds(6)); _timer.Change(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(4)); ReadLine();
}
finally
{
//实现了IDispose接口 要及时释放
_timer.Dispose();
}
} static Timer _timer; static void TimerOperation(DateTime start)
{
TimeSpan elapsed = DateTime.Now - start;
WriteLine($"离 {start} 过去了 {elapsed.Seconds} 秒. " +
$"定时器线程池 线程 id: {CurrentThread.ManagedThreadId}");
}

运行结果如下所示,可见定时器根据所设置的周期时间循环的调用TimerOperation()方法。

1.8 使用BackgroundWorker组件

本节主要介绍BackgroundWorker组件的使用,该组件实际上被用于Windows窗体应用程序(Windows Forms Application,简称 WPF)中,通过它实现的代码可以直接与UI控制器交互,更加自认和好用。

演示代码如下所示,使用BackgroundWorker来实现对数据进行计算,并且让其支持报告工作进度,支持取消任务。

static void Main(string[] args)
{
var bw = new BackgroundWorker();
// 设置可报告进度更新
bw.WorkerReportsProgress = true;
// 设置支持取消操作
bw.WorkerSupportsCancellation = true; // 需要做的工作
bw.DoWork += Worker_DoWork;
// 工作处理进度
bw.ProgressChanged += Worker_ProgressChanged;
// 工作完成后处理函数
bw.RunWorkerCompleted += Worker_Completed; bw.RunWorkerAsync(); WriteLine("按下 `C` 键 取消工作");
do
{
if (ReadKey(true).KeyChar == 'C')
{
bw.CancelAsync();
} }
while (bw.IsBusy);
} static void Worker_DoWork(object sender, DoWorkEventArgs e)
{
WriteLine($"DoWork 线程池 线程 id: {CurrentThread.ManagedThreadId}");
var 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(TimeSpan.FromSeconds(0.1));
} e.Result = 42;
} static void Worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
WriteLine($"已完成{e.ProgressPercentage}%. " +
$"处理线程 id: {CurrentThread.ManagedThreadId}");
} static void Worker_Completed(object sender, RunWorkerCompletedEventArgs e)
{
WriteLine($"完成线程池线程 id: {CurrentThread.ManagedThreadId}");
if (e.Error != null)
{
WriteLine($"异常 {e.Error.Message} 发生.");
}
else if (e.Cancelled)
{
WriteLine($"操作已被取消.");
}
else
{
WriteLine($"答案是 : {e.Result}");
}
}

运行结果如下所示。

在本节中,使用了C#中的另外一个语法,叫事件(event)。当然这里的事件不同于之前在线程同步章节中提到的事件,这里是观察者设计模式的体现,包括事件源、订阅者和事件处理程序。因此,除了异步APM模式意外,还有基于事件的异步模式(Event-based Asynchronous Pattern,简称 EAP)

参考书籍

本文主要参考了以下几本书,在此对这些作者表示由衷的感谢你们提供了这么好的资料。

  1. 《CLR via C#》
  2. 《C# in Depth Third Edition》
  3. 《Essential C# 6.0》
  4. 《Multithreading with C# Cookbook Second Edition》
  5. 《C#多线程编程实战》

源码下载点击链接 示例源码下载

笔者水平有限,如果错误欢迎各位批评指正!

C#多线程编程系列(四)- 使用线程池的更多相关文章

  1. Java并发编程系列-(6) Java线程池

    6. 线程池 6.1 基本概念 在web开发中,服务器需要接受并处理请求,所以会为一个请求来分配一个线程来进行处理.如果每次请求都新创建一个线程的话实现起来非常简便,但是存在一个问题:如果并发的请求数 ...

  2. java并发编程(四) 线程池 & 任务执行、终止源码分析

    参考文档 线程池任务执行全过程:https://blog.csdn.net/wojiaolinaaa/article/details/51345789 线程池中断:https://www.cnblog ...

  3. 并发编程系列:Java线程池的使用方式,核心运行原理、以及注意事项

    并发编程系列: 高并发编程系列:4种常用Java线程锁的特点,性能比较.使用场景 线程池的缘由 java中为了提高并发度,可以使用多线程共同执行,但是如果有大量线程短时间之内被创建和销毁,会占用大量的 ...

  4. .Net多线程编程—Parallel LINQ、线程池

    Parallel LINQ 1 System.Linq.ParallelEnumerable 重要方法概览: 1)public static ParallelQuery<TSource> ...

  5. Java并发编程系列-(7) Java线程安全

    7. 线程安全 7.1 线程安全的定义 如果多线程下使用这个类,不过多线程如何使用和调度这个类,这个类总是表示出正确的行为,这个类就是线程安全的. 类的线程安全表现为: 操作的原子性 内存的可见性 不 ...

  6. Python GUI之tkinter窗口视窗教程大集合(看这篇就够了) JAVA日志的前世今生 .NET MVC采用SignalR更新在线用户数 C#多线程编程系列(五)- 使用任务并行库 C#多线程编程系列(三)- 线程同步 C#多线程编程系列(二)- 线程基础 C#多线程编程系列(一)- 简介

    Python GUI之tkinter窗口视窗教程大集合(看这篇就够了) 一.前言 由于本篇文章较长,所以下面给出内容目录方便跳转阅读,当然也可以用博客页面最右侧的文章目录导航栏进行跳转查阅. 一.前言 ...

  7. C#多线程编程(1)--线程,线程池和Task

    新开了一个多线程编程系列,该系列主要讲解C#中的多线程编程.    利用多线程的目的有2个: 一是防止UI线程被耗时的程序占用,导致界面卡顿:二是能够利用多核CPU的资源,提高运行效率. 我没有进行很 ...

  8. java多线程系列六、线程池

    一. 线程池简介 1. 线程池的概念: 线程池就是首先创建一些线程,它们的集合称为线程池. 2. 使用线程池的好处 a) 降低资源的消耗.使用线程池不用频繁的创建线程和销毁线程 b) 提高响应速度,任 ...

  9. C#多线程编程系列(二)- 线程基础

    目录 C#多线程编程系列(二)- 线程基础 1.1 简介 1.2 创建线程 1.3 暂停线程 1.4 线程等待 1.5 终止线程 1.6 检测线程状态 1.7 线程优先级 1.8 前台线程和后台线程 ...

  10. Java并发编程:Java的四种线程池的使用,以及自定义线程工厂

    目录 引言 四种线程池 newCachedThreadPool:可缓存的线程池 newFixedThreadPool:定长线程池 newSingleThreadExecutor:单线程线程池 newS ...

随机推荐

  1. How to set an Apache Kafka multi node – multi broker cluster【z】

    Set a multi node Apache ZooKeeper cluster On every node of the cluster add the following lines to th ...

  2. centos一键安装lnmp成功后无法访问ip(解决办法)

    自己搞了个服务器 (我的服务器网络类型是 专有网络)如下图点击 配置规则 进入到 进.出端口规则配置 点击添加安全组规则 如图所配置  添加完成后 就如下面所示 (配置完成后 通过ip就已经可以访问了 ...

  3. oracle的分析函数over(Partition by...) 及开窗函数

        over(Partition by...) 一个超级牛皮的ORACLE特有函数. oracle的分析函数over 及开窗函数一:分析函数overOracle从8.1.6开始提供分析函数,分析函 ...

  4. Java 8 接口中的默认方法与静态方法

    Java 8 接口中的默认方法与静态方法 1. 接口中的默认方法 允许接口中包含具有具体实现的方法,该方法称"默认方法",默认方法使用用 default 关键字修饰. public ...

  5. part1:3-VMware及redhat enterprise Linux 6 的安装

    创建虚拟机PC FILE->NEW Virtual machine->custom(自定义,定制)->...->I WILL INSTALL THE OS LATER-> ...

  6. oss browser

    版本问题 https://github.com/aliyun/oss-browser/blob/master/all-releases.md?spm=5176.doc61872.2.7.Ic2az6& ...

  7. java实现word,ppt,excel,jpg转pdf

    word,excel,jpeg 转 pdf 首先下载相关jar包:http://download.csdn.net/detail/xu281828044/6922499 import java.io. ...

  8. java中配置自定义拦截器中exclude-mapping path是什么意思?

    <mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/**"/>//过滤全部请求 & ...

  9. 2018.07.20 bzoj3211: 花神游历各国(线段树)

    传送门 维护区间开方,区间求和.这个是线段树常规操作. 显然一个数被开方若干次之后要么是1,要么是0,所以用线段树维护区间最大和区间和,如果区间最大不超过1就剪枝剪掉,不然就继续递归直到叶节点时停下进 ...

  10. java判断字符串是否为数字,包括负数

    /** * 判断是否为数字,包含负数情况 * @param str * @return */ private boolean isNumeric(String str){ Boolean flag = ...