C# · 12月 25, 2021

C#线程池用法详细介绍

介绍

.NET Framework提供了包含ThreadPool类的System.Threading 空间,这是一个可直接访问的静态类,该类对线程池是必不可少的。它是公共“线程池”设计样式的实现。对于后台运行许多各不相同的任务是有用的。对于单个的后台线种而言有更好的选项。

线程的最大数量。这是完全无须知道的。在.NET中ThreadPool的所有要点是它自己在内部管理线程池中线程。多核机器将比以往的机器有更多的线程。微软如此陈述“线程池通常有一个线程的最大数量,如果所有的线程都忙,增加的任务被放置在队列中直到它们能被服务,才能作为可用的线程。”

用法位置

线程池类型能被用于服务器和批处理应用程序中,线程池有更廉价的得到线程的内部逻辑,因为当需要时这些线程已被形成和刚好“连接”,所以线程池风格代码被用在服务器上。

MSDN表述:“线程池经常用在服务器应用程序中,每一个新进来的需求被分配给一个线程池中的线程,这样该需求能被异步的执行,没有阻碍主线程或推迟后继需求的处理。”

MSDN 参考

ThreadPool VS BackgroundWorker

如果你正在使用Windows窗体,宁可使用BackgroundWorker来对付那些更简单的线程需求,BackgroundWorker在网络访问和其他一些简单的事情方面做得很好。但对于多处理器的批处理来说,你需要ThreadPool。

BackgroundWorker 教程

当你的程序要批处理时,考虑线程池

当你的程序产生很多(3个以上)线程时,考虑线程池

当你的程序使用Windows窗体时,考虑后台执行。

线程要考虑的事 同样,如何使用线程的细节能帮助发现最好的代码。下面比较线程情形和哪个类是最好的。

你需要一个额外的线程 使用后台执行

你有许多短期的线程 使用线程池

需求

线程很重要,但对于那些不会花很长时间来执行且只做一件事情的大多数应用程序来说却并不重要的。线程对于界面可用性不是很重要的的应用程序而言也不是很重要,要尽量避免使用线程(译者注:比如进度条不是很重要的应用程序)。

连接方法

可使用QueueUserWorkItem连接方法(methods)到线程池。方法要运行在线程上,则必须把它连接到QueueUserWorkItem。如何实现呢?必须使用WaitCallback。在MSDN中,WaitCallback被描述成当线程池执行时要被调用的委托回调方法,是回调它的参数的委托。

WaitCallback

只需指定“new WaitCallback”语句作为ThreadPool.QueueUserWorkItem的第一个参数来使用WaitCallback.不需要任何其他的代码来使用这方法生效。

使用WaitCallback[c#]的例子

复制代码 代码如下:
void Example()

{

// 连接 ProcessFile 方法到线程池.

//注意: ‘a’ 是一个作为参数的对象

ThreadPool.QueueUserWorkItem(new WaitCallback(ProcessFile),a);

}

private void ProcessFile(object a)

{

// 我被连接到线程池通过 WaitCallback.

}

参数

我们能通过定义一个特定的类并把一些重要的值放在类里面来使用参数,那么,方法接收了对象,就能通过对象向方法传递多个参数了。以下是一个早期的例子。

使用带参数QueueUserWorkItem 的例子[c#]

复制代码 代码如下:
//指定作为线程池方法的参数的类

class ThreadInfo

{

public string FileName { get; set; }

public int SelectedIndex { get; set; }

}

class Example

{

public Example()

{

// 声明一个新的参数对象

ThreadInfo threadInfo = new ThreadInfo();

threadInfo.FileName = “file.txt”;

threadInfo.SelectedIndex = 3;

//发送自定义的对象到线程方法

ThreadPool.QueueUserWorkItem(new WaitCallback(ProcessFile),threadInfo);

}

private void ProcessFile(object a)

{

ThreadInfo threadInfo = a as ThreadInfo;

string fileName = threadInfo.FileName;

int index = thread.SelectedIndex;

}

}

发生了什么事?我们发送两个值给这个线程化的ProcessFile方法,它需要知道文件名和选择索引,而我们在这个对象中把参数都发送了给它。

进度条

能通过从设计器中右边的工具盒面板中增加Windows窗体控件到你的窗体程序来使用进度条并设置 progressBar1.Value,progressBar1.Minimum 和progressBar1.Maximum。 progressBar1.Value是最小值和最大值中间的位置,以下代码用来初始化进度条:

设置进度条的例子 [C#]

复制代码 代码如下:
//设置进度条的长度.

// 这里我们有6个单位来完成,所以6是最大值。

// 最小值通常是0

progressBar1.Maximum = 6; // 或其他数字

progressBar1.Minimum = 0;

进度条位置 你的进度条中的有颜色部分是当前值与最大值的百分比。所以,如果最大值是6,而值是3即表示做完了一半。

ProgressBar 例子 (Windows Forms)

在进度条中调用Invoke(援引)

让我们看如何在进度条实例中使用Invoke方法。遗憾的是,你不能在辅助线程中访问Windows控件,因为UI线程是分离的,必须使用委托(delegate)和Invoke到进度条。

请求Invoke(调用)的例子[C#]

@L_403_3@ 代码如下:
public partial class MainWindow : Form

{

// 这是运行在UI线程来更新条的委托

public delegate void BarDelegate();

//该窗体的构造器(由Visual Studio自己产生)

public MainWindow()

{

InitializeComponent();

}

//当按下按钮,启动一个新的线程

private void button_Click(object sender,EventArgs e)

{

// 设定进度条的长度.

progressBar1.Maximum = 6;

progressBar1.Minimum = 0;

// 给线程传递这些值.

ThreadInfo threadInfo = new ThreadInfo();

threadInfo.FileName = “file.txt”;

threadInfo.SelectedIndex = 3;

ThreadPool.QueueUserWorkItem(new WaitCallback(ProcessFile),threadInfo);

}

// 运行在后台线程上的东西

private void ProcessFile(object a)

{

// (省略)

// 使用’a’做一些重要的事.

// 告诉UI 我们已经完成了.

try

{

// 在窗体中调用委托 .

this.Invoke(new BarDelegate(UpdateBar));

}

catch

{

//当一些问题发生后我们能使程序恢复正常

}

}

//更新进度条.

private void UpdateBar()

{

progressBar1.Value++;

if (progressBar1.Value == progressBar1.Maximum)

{

// 结束了,进度条满了.

}

}

}

委托语法 以上代码的开始处,可以看到声明 UpdateBar 的委托。它告诉Visual Studio 和C# 需要来使用这个作为对象的方法。

更多需要的工作 以上程序演示了如何设定进度条的最大值和最小值,如何在工作完成后“Invoke”委托方法来增加进度条的大小。

在调试器中的线程

这儿要显示如何在Visual Studio的调试器中查看线程。一旦你有一个运行的程序,你能采取这些措施来可视化你的线程。首先,以调试模式打开你的线程应用程序,一旦你的应用程序运行在调试器,告知它去做它的工作而且运行这些线程,通过绿色箭头运行调试器,当线程正在运行,在工具条中单击“pause”按钮。

下一步 调试>窗口>线程.该菜单项将打开一个类似下图的窗口,你能看见有多少线程正在线程池中运行。

四个辅助线程 上图显示了共有10个线程,但只有四个辅助线程(Worker Thread)在程序中被分配给MainWindow.ProcessFile.

约束辅助线程

如果你有一个双核或四核系统,你将考虑最多两个四个很费力的线程。我们能在运行的线程中保持一个_threadCount 字段并且跟踪它的数值。用这个线程计数字段,你将需要在C#语言中使用一个锁来避免造成这个字段读和写的错误,锁保护你的线程被其他线程所改变。

计数线程的例子 [C#]

复制代码 代码如下:
// 锁住这个对象.

readonly object _countLock = new object();

private void ProcessFile(object argument)

{

// 约束辅助线程的数量

while (true)

{

lock (_countLock)

{

if (_threadCount < 4)

{

// Start the processing

_threadCount++;

break;

}

}

Thread.Sleep(50);

}

// Do work…

}

我们看到什么 以是代码是异步执行的方法。只有其他辅助线程少于4个时它才会工作。这对于一个四核机器是好的。请看描述锁声明的更多上下文的文章

Lock Statement

控制线程计数器

你可以在ThreadPool上使用SetMinThreads 来在连发活动中提高吞吐量和性能。以下是关于可使用的最佳的最小线程数量的材料。

ThreadPool.SetMinThreads Method

总结

我们了解了如何在C#程序中使用线程池来有效管理多个线程,在Windows 窗体应用程序的进度条和用户界面中能给人留很深印象并且也不难实现。然而,线程带来了很多的复杂性并导致漏洞,线程池是一个有用的简化,但它仍然是困难的。