线程 - 幕后






4.99/5 (57投票s)
本文探讨了各种 .NET 线程实现(Threading Implementations)的性能、可伸缩性和局限性。
引言
线程实现(Threading Implementation)只是一种创建线程的方式——为应用程序添加并行性和并发性。
本文提供的所有研究和分析都通过编程得到验证,并且提供了源代码。这些结果对于密集型线程的应用程序来说 certainly 很有趣且有用。
排队
.NET 线程实现(Threading Implementations)都不会立即创建线程。线程请求会被排队,然后 .NET Runtime 会决定何时创建线程。最初的几个线程几乎会立即创建,但后续线程的创建速度取决于具体的线程实现。
显式与隐式线程
.NET 提供了多种线程实现,可以分为两大类:
- 显式线程(Explicit Threading)
- 显式创建线程:
new Thread(new ThreadStart(Work)).Start();
- 所有线程几乎立即创建。
- 可以排队数千个线程。
- 显式创建线程:
- 隐式线程(Implicit Threading)
- 隐式创建线程:任务排队后,线程会在后台自动创建。
- 只有最初的几个线程几乎立即创建。
- 可以排队数百万个任务。
四种线程实现
有四种线程实现,四种创建线程的方式——为应用程序添加并行性和并发性。以下是这四种线程实现:
- 异步调用(Asynchronous Invoke)
- 显式线程(Explicit Threading)
- 任务并行库(Task Parallel Library, TPL)
- 线程池
异步调用(Asynchronous Invoke)
异步调用(Asynchronous Invoke)属于隐式线程类别。以下是基本示例代码:
void CreateThread_Via_AsynchronousInvoke()
{
new Delegate_SimulateWork(SimulateWork).BeginInvoke(null, null);
}
delegate void Delegate_SimulateWork();
void SimulateWork()
{
Thread.Sleep(1000);
}
显式线程(Explicit Threading)
显式线程类别中只有一种线程实现,这种线程实现也称为:显式线程(Explicit Threading)。以下是基本示例代码:
void CreateThread_Via_ExplicitThreading()
{
new Thread(new ThreadStart(SimulateWork)).Start();
}
void SimulateWork()
{
Thread.Sleep(1000);
}
任务并行库(Task Parallel Library, TPL)
任务并行库(Task Parallel Library, TPL)属于隐式线程类别。以下是基本示例代码:
void CreateThread_Via_TaskParallelLibrary()
{
Task.Factory.StartNew(SimulateWork);
}
void SimulateWork()
{
Thread.Sleep(1000);
}
线程池
线程池(Thread Pool)属于隐式线程类别。以下是基本示例代码:
void CreateThread_Via_ThreadPool()
{
ThreadPool.QueueUserWorkItem(SimulateWork);
}
void SimulateWork(object state)
{
Thread.Sleep(1000);
}
线程实现分析软件
该软件分析线程实现。
- 确定最大和安全队列限制
- 测试工作线程(Worker Threads)的创建速度
- 测试可创建的工作线程数量
- 比较线程实现
该软件易于使用,按钮仅在适当的时候启用;但在此截图之后将提供有关该软件的更多详细信息。
以下是对上述屏幕截图中每个控件的简要描述:
- 按钮
- 左上角的四个按钮:这些按钮用于启动对四种线程实现的分析。
- 停止分析:停止正在进行的任何分析。
- 更新线程实现比较:用分析结果更新底部的网格数据。
- 选定的队列限制:软件根据资源消耗确定线程实现的最大队列限制;但是,如果选定的队列限制在资源耗尽之前达到,则选定的队列限制将用作最大队列限制。
- Labels:
- 分析阶段:正在进行的分析阶段。
- 线程实现:正在分析的线程实现。
- 最大队列限制:在应用程序抛出 Out Of Memory Exception 之前可以排队的任务或线程的最大数量。
- 安全队列限制:安全队列限制是最大队列限制的 90%;但是,如果选定的队列限制用作最大队列限制,则安全队列限制是最大队列限制的 100%。
- 已排队的任务或线程数:随着分析的进行,已排队的任务或线程的数量。
- 活动工作线程数:随着分析的进行,正在模拟工作的线程数量。
- 线程数:应用程序进程中的总线程数。
- 分析阶段开始时间:当前分析阶段开始的时间。
- 分析阶段持续时间:当前分析阶段的持续时间。
- 平均工作线程创建时间 (毫秒):创建工作线程所需的平均毫秒数。
- 第一秒创建的工作线程数:开始排队任务或线程后的第一秒内创建的工作线程数量。
- 进程内存利用率 (MB):应用程序进程使用的内存量。
- 数据网格列:
- 安全限制:与“安全队列限制”标签相同。
- 工作线程:与“活动工作线程数”标签相同。
- 平均创建时间:与“平均工作线程创建时间 (毫秒)”标签相同。
- 1 秒内工作线程数:与“第一秒创建的工作线程数”标签相同。
- 内存使用量:与“进程内存利用率 (MB)”标签相同。
线程分析阶段
- 空闲:没有进行分析。
- 确定最大已排队任务或线程数:确定可为线程实现排队的最大任务或线程数。
- 等待队列清空:等待任务或线程队列清空后再继续下一阶段。
- 分析线程创建:测试可以生成多少工作线程以及需要多长时间。
- 分析完成:分析完成,用户可以更新线程实现比较数据网格。
隐式线程分析不会完成
除非选择了较低的队列限制,否则在分析隐式线程实现时会排队数百万个任务,而当今的硬件通常无法运行数百万个并发线程;因此,隐式线程分析通常不会达到“分析完成”阶段。用户可以观察创建线程的平均时间以及创建的线程数。总会有线程创建几乎停止或变得非常慢的时候,这代表了所分析线程实现的局限性。此时,可以更新线程实现比较数据网格并停止分析。
线程实现比较
线程实现比较数据网格中的数据是整个练习的关键。从这些数据中,我们可以看到以下优势:
- 显式线程(Explicit Threading)
- 生成更多工作线程
- 生成线程的速度快得多
- 隐式线程(Implicit Threading)
- 允许排队数百万个任务
下面的图表可视化了线程实现比较数据。为了在同一图表中可视化数据,有必要将安全队列限制除以 10,000,以确保所有值都在相同的数值范围内(0-3000)。
注意:结果会因计算机/服务器规格而异;但是,各种线程实现的优缺点应保持不变。
在 Visual Studio 内执行代码与在 Visual Studio 外执行代码
值得注意的是,在 Visual Studio 外执行代码时有以下改进:
- 应用程序可用内存翻倍,因此
- 显式线程(Explicit Threading)
- 队列限制翻倍
- 生成的工作线程数量翻倍。
- 隐式线程(Implicit Threading)
- 队列限制翻倍
- 奇怪的是,生成的工作线程数量没有明显提高。
- 显式线程(Explicit Threading)
- 性能
- 显式线程:生成工作线程的速度快 6 倍。
- 隐式线程:奇怪的是,没有明显提高。
文章中的代码
所有代码都有非常好的文档记录,因此应该很容易找到您需要的内容。本文中只展示最重要/最有趣的代码。
模拟工作
四种线程实现使用以下代码来模拟工作。
//This delegate is only used by the Asynchronous Invoke Threading Implementation.
delegate void Delegate_SimulateWork();
/// <summary>
/// This overloaded function is only used by the Thread Pool Threading Implementation.
/// </summary>
/// <param name="state">This parameter is not used; simply set it to null.</param>
void SimulateWork(object state)
{
SimulateWork();
}
/// <summary>
/// This function is used by all the Threading Implementations to simulate work.
/// </summary>
void SimulateWork()
{
//This function is called millions of times by numerous threads;
//so, thread safety is a concern.
//It is necessary to use the Interlocked functionality
//to ensure the accuracy of these variables.
Interlocked.Increment(ref activeWorkerThreadCount);
//Threads simulate work indefinitely until
//workerThreadsContinueSimulatingWork is set to false.
//Often threads simulate work here for 20+ minutes
//depending on the analysis duration.
while (workerThreadsContinueSimulatingWork)
Thread.Sleep(100);
Interlocked.Decrement(ref activeWorkerThreadCount);
Interlocked.Decrement(ref tasksOrThreads_queued);
}
排队任务或线程
以下代码根据正在分析的线程实现来排队任务或线程。
void QueueTask_or_thread()
{
switch (threadingImplementationToAnalyze)
{
case Constants.ThreadingImplementation.AsynchronousInvoke:
new Delegate_SimulateWork(SimulateWork).BeginInvoke(null, null);
break;
case Constants.ThreadingImplementation.ExplicitThreading:
new Thread(new ThreadStart(SimulateWork)).Start();
break;
case Constants.ThreadingImplementation.TaskParallelLibrary:
Task.Factory.StartNew(SimulateWork);
break;
case Constants.ThreadingImplementation.ThreadPool:
ThreadPool.QueueUserWorkItem(SimulateWork);
break;
}
}
确定最大已排队任务或线程数
这段代码用于确定可为线程实现排队的最大任务或线程数。
try
{
while (true)
{
// Exit Point: Analysis will exit here if the Stop Analysis button is clicked
if (!applicationIsInAnalysisMode)
return;
//Check for a Memory Fail Point.
//This is the point where too much memory is used and
//an OutOrMemory Exception is thrown.
//A Memory Fail Point indicates the: Max Queued Tasks Or Threads
//This check works for all Threading Implementations except
//for Explicit Threading
if (threadingImplementationToAnalyze !=
Constants.ThreadingImplementation.ExplicitThreading)
{
if (tasksOrThreads_queued % 100000 == 0)//Performance Optimization
{
try
{
new System.Runtime.MemoryFailPoint(100);
}
catch (Exception)
{
break;
}
}
}
//Chosen Queue Limit (chosen by the user):
//If the System Resources don't first limit the: Max Queued Tasks Or Threads,
//Then the Chosen Queue Limit is the Max Queued Tasks Or Threads.
if (tasksOrThreads_queued == chosenQueueLimit)
{
break;
}
QueueTask_or_thread();
//The tasksOrThreads_queued variable is used to count the:
//Max Queued Tasks Or Threads
tasksOrThreads_queued++;
CountWorkerThreadsSpawnedInFirstSecond();
}
}
catch (Exception)
{
//Do nothing.
//Explicit Threading throws an OutOfMemory Exception; however,
//it is expected and handled below.
//Max explicit threads is determined by creating explicit threads
//until an error is thrown.
}
确定安全队列限制
此处根据最大队列限制和选定的队列限制确定安全队列限制。
maxQueueLimit = tasksOrThreads_queued;
if (maxQueueLimit == chosenQueueLimit)
{
//The system did not run out of resources.
//As such, the Chosen Queue Limit is considered a safe limit.
safeQueueLimit = chosenQueueLimit;
}
else
{
//The system did run out of resources.
//As such, the Safe Queue Limit must be less than the Max Queued Tasks Or Threads.
//Safe Queue Limit is set to 90% of the Max Queued Tasks Or Threads.
safeQueueLimit = (int)((double)maxQueueLimit * .90);
}
分析线程创建
现在已知安全队列限制,应用程序将排队任务或线程,直到达到安全队列限制。然后用户可以看到生成了多少工作线程以及需要多长时间。
workerThreadsContinueSimulatingWork=true;
for (tasksOrThreads_queued = 0; tasksOrThreads_queued < safeQueueLimit;
tasksOrThreads_queued++)
{
//Exit Point: Analysis will exit here if the Stop Analysis button is clicked
if (!applicationIsInAnalysisMode)
return;
QueueTask_or_thread();
}
看见就说
目标是提供清晰、无错误的内容,对此您的帮助将不胜感激。如果您发现错误或潜在的改进之处,请务必发表评论。欢迎所有反馈。
摘要
本文深入探讨了 .NET 线程的“幕后”。虽然远非完全详尽,但它突出了各种 .NET 线程实现之间在性能和可伸缩性方面的显著差异。希望本文和软件能在您下次开发需要选择线程实现的密集型线程应用程序时有所帮助。
历史
- 2017年3月13日:初始版本