Parallel class 是如何动态调整并行度的?
How does the Parallel class dynamically adjust the level of parallelism?
TPL 使用什么反馈来动态调整工作线程数?
我之前的理解是,它衡量任务完成的速度,看是否值得添加或删除线程。但是,为什么这段代码会不断增加线程数,即使存在信号量引入的瓶颈?
当然,每秒不能超过 20 个任务完成,超过 2 个线程不会改善这一点。
var activeThreads = 0;
var semaphore = new SemaphoreSlim(2);
var res = Parallel.For(0, 1_000_000, i =>
{
Interlocked.Increment(ref activeThreads);
semaphore.Wait();
try
{
Thread.Sleep(100);
Console.WriteLine("Threads: " + activeThreads);
}
finally
{
Interlocked.Decrement(ref activeThreads);
semaphore.Release();
}
});
我相信 ParallelOptions 是您要指定并行度的内容。
Parallel.For(0, 1000, new ParallelOptions
{
MaxDegreeOfParallelism = 2
}, i => { Console.WriteLine(i); });
就我个人而言,我认为 TPL 库在很多情况下都能正常工作,但它在执行分配方面并不是很聪明(请原谅我的英文)。每当您在应用程序的执行中遇到瓶颈时,例如查看管道模式。这是一个link,它很好地描述了并行执行的不同方法:https://www.dotnetcurry.com/patterns-practices/1407/producer-consumer-pattern-dotnet-csharp
TL;DR: TPL 用来证明创建新线程的代码是阻塞 . (同步或休眠,或执行 I/O 都算作阻塞。)
更长的解释...
当你的任务 运行s 时,它占用线程 100 毫秒(因为你 Sleep(100)
)。当您处于睡眠状态时,该线程不能用于 运行 其他任务,因为它可能会在睡眠时间段到期时处于不处于 运行 可用状态的风险。通常我们会休眠而不是执行异步操作,因为我们需要保持我们的 调用堆栈 完好无损。因此,我们依靠堆栈来维护我们的状态。堆栈是线程的一种独一无二的资源。 (线程实际上并没有比它的堆栈更多。)
所以 TPL(特别是线程池)试图保持高 占用率 但线程数低。实现这一点的一种方法是确保系统中的 运行nable 线程数量与虚拟处理器数量大致相同。每次它需要增加线程数,就必须为线程创建一个比较昂贵的stack,所以最好不要那么多。并且无法调度 运行 不可用的线程,因此当 CPU 空闲时,您需要调度一些东西以利用可用的处理资源。如果线程正在休眠,则无法将其调度到 运行。因此,一个线程将被添加到线程池中,下一个任务将被安排在它上面。
当您编写这样的并行代码(如在您的 parallel for 循环中)时,可以由 TPL 进行分区和管理,您应该小心将线程放入非 运行 可用状态。执行同步 I/O、等待同步对象(例如信号量、事件或互斥锁等)或休眠将使线程进入一种状态,在 I/O 完成之前线程无法执行任何其他操作,睡眠间隔到期,或同步对象变为有信号。线程在此期间对TPL没有好处。
在您的情况下,您需要执行以下几项操作:等待信号量、睡眠,然后通过写入控制台执行 I/O。第一件事是等待那个信号量。如果它没有发出信号,那么您会立即遇到线程不可 运行 的情况,并且您的百万左右任务的下一个任务需要 运行 必须安排在不同的线程上.如果没有,则 TPL 可以证明创建一个新线程以启动更多任务是合理的。毕竟,如果线程 #987,321 最终将设置信号量以解锁任务 #1 怎么办? TPL 不知道您的代码做了什么,因此本着 效率 的精神,它可以延迟创建线程一段时间,但为了 正确性,最终它将不得不创建更多的线程来开始削减任务列表。有一个复杂的、特定于实现的启发式方法,它适用于监控、预测和以其他方式正确地进行效率猜测。
现在你的具体问题其实是问它用什么反馈来调整线程数。正如我所说,实际实现很复杂,您可能应该将其视为黑盒。但简而言之,如果没有 运行 可用线程,它 可能会 创建另一个线程以继续在任务列表中逐渐减少(或者可能会等待一段时间再这样做,希望事情会释放出来),如果有太多空闲线程,它将终止空闲线程以回收它们的资源。
重申一下,正如我在顶部所说的那样,希望这次能回答您的问题,您所做的允许 TPL 证明创建新线程的一件事是 阻止 。 ...甚至在第一个信号量上。
运行 到 GitHub 上的 article analysing the thread injection algorithm in 2017. As of 2019-08-01, the hillclimbing.cpp file 并没有真正改变,所以这篇文章应该仍然是最新的。
相关详情:
The .NET thread pool has two main mechanisms for injecting threads: a
starvation-avoidance mechanism that adds worker threads if it sees no
progress being made on queued items and a hill-climbing heuristic that
tries to maximize throughput while using as few threads as possible.
...
It calculates the desired number of threads based on the ‘current
throughput’, which is the ‘# of tasks completed’ (numCompletions)
during the current time-period (sampleDuration in seconds).
...
It also takes the current thread count (currentThreadCount) into
consideration.
...
The real .NET Thread Pool only increases the thread-count by one
thread every 500 milliseconds. It keeps doing this until the ‘# of
threads’ has reached the amount that the hill-climbing algorithm
suggests.
...
The [hill-climbing] algorithm only returns values that respect the limits
specified by ThreadPool.SetMinThreads(..) and
ThreadPool.SetMaxThreads(..)
...
In addition, [the hill-climbing algorithm] will only recommend
increasing the thread count if the CPU Utilization is below 95%
原来线程池确实有一个基于任务完成率的反馈机制。它也不会明确检查其线程是否被阻塞或 运行,但它确实会关注总体 CPU 利用率以检测阻塞。所有这些也意味着它应该大致了解其他线程和进程在做什么。
另一方面,它总是会急切地生成至少与 ThreadPool.SetMinThreads()
指定的线程一样多的线程,默认为机器上逻辑处理器的数量。
总而言之,有问题的测试代码做了两件事,使其不断堆积更多线程:
- 有很多任务在排队,并且在队列中等待了很长时间,这表明饥饿
- CPU利用率可以忽略不计,这意味着一个新的线程应该可以使用它
TPL 使用什么反馈来动态调整工作线程数?
我之前的理解是,它衡量任务完成的速度,看是否值得添加或删除线程。但是,为什么这段代码会不断增加线程数,即使存在信号量引入的瓶颈?
当然,每秒不能超过 20 个任务完成,超过 2 个线程不会改善这一点。
var activeThreads = 0;
var semaphore = new SemaphoreSlim(2);
var res = Parallel.For(0, 1_000_000, i =>
{
Interlocked.Increment(ref activeThreads);
semaphore.Wait();
try
{
Thread.Sleep(100);
Console.WriteLine("Threads: " + activeThreads);
}
finally
{
Interlocked.Decrement(ref activeThreads);
semaphore.Release();
}
});
我相信 ParallelOptions 是您要指定并行度的内容。
Parallel.For(0, 1000, new ParallelOptions
{
MaxDegreeOfParallelism = 2
}, i => { Console.WriteLine(i); });
就我个人而言,我认为 TPL 库在很多情况下都能正常工作,但它在执行分配方面并不是很聪明(请原谅我的英文)。每当您在应用程序的执行中遇到瓶颈时,例如查看管道模式。这是一个link,它很好地描述了并行执行的不同方法:https://www.dotnetcurry.com/patterns-practices/1407/producer-consumer-pattern-dotnet-csharp
TL;DR: TPL 用来证明创建新线程的代码是阻塞 . (同步或休眠,或执行 I/O 都算作阻塞。)
更长的解释...
当你的任务 运行s 时,它占用线程 100 毫秒(因为你 Sleep(100)
)。当您处于睡眠状态时,该线程不能用于 运行 其他任务,因为它可能会在睡眠时间段到期时处于不处于 运行 可用状态的风险。通常我们会休眠而不是执行异步操作,因为我们需要保持我们的 调用堆栈 完好无损。因此,我们依靠堆栈来维护我们的状态。堆栈是线程的一种独一无二的资源。 (线程实际上并没有比它的堆栈更多。)
所以 TPL(特别是线程池)试图保持高 占用率 但线程数低。实现这一点的一种方法是确保系统中的 运行nable 线程数量与虚拟处理器数量大致相同。每次它需要增加线程数,就必须为线程创建一个比较昂贵的stack,所以最好不要那么多。并且无法调度 运行 不可用的线程,因此当 CPU 空闲时,您需要调度一些东西以利用可用的处理资源。如果线程正在休眠,则无法将其调度到 运行。因此,一个线程将被添加到线程池中,下一个任务将被安排在它上面。
当您编写这样的并行代码(如在您的 parallel for 循环中)时,可以由 TPL 进行分区和管理,您应该小心将线程放入非 运行 可用状态。执行同步 I/O、等待同步对象(例如信号量、事件或互斥锁等)或休眠将使线程进入一种状态,在 I/O 完成之前线程无法执行任何其他操作,睡眠间隔到期,或同步对象变为有信号。线程在此期间对TPL没有好处。
在您的情况下,您需要执行以下几项操作:等待信号量、睡眠,然后通过写入控制台执行 I/O。第一件事是等待那个信号量。如果它没有发出信号,那么您会立即遇到线程不可 运行 的情况,并且您的百万左右任务的下一个任务需要 运行 必须安排在不同的线程上.如果没有,则 TPL 可以证明创建一个新线程以启动更多任务是合理的。毕竟,如果线程 #987,321 最终将设置信号量以解锁任务 #1 怎么办? TPL 不知道您的代码做了什么,因此本着 效率 的精神,它可以延迟创建线程一段时间,但为了 正确性,最终它将不得不创建更多的线程来开始削减任务列表。有一个复杂的、特定于实现的启发式方法,它适用于监控、预测和以其他方式正确地进行效率猜测。
现在你的具体问题其实是问它用什么反馈来调整线程数。正如我所说,实际实现很复杂,您可能应该将其视为黑盒。但简而言之,如果没有 运行 可用线程,它 可能会 创建另一个线程以继续在任务列表中逐渐减少(或者可能会等待一段时间再这样做,希望事情会释放出来),如果有太多空闲线程,它将终止空闲线程以回收它们的资源。
重申一下,正如我在顶部所说的那样,希望这次能回答您的问题,您所做的允许 TPL 证明创建新线程的一件事是 阻止 。 ...甚至在第一个信号量上。
运行 到 GitHub 上的 article analysing the thread injection algorithm in 2017. As of 2019-08-01, the hillclimbing.cpp file 并没有真正改变,所以这篇文章应该仍然是最新的。
相关详情:
The .NET thread pool has two main mechanisms for injecting threads: a starvation-avoidance mechanism that adds worker threads if it sees no progress being made on queued items and a hill-climbing heuristic that tries to maximize throughput while using as few threads as possible.
...
It calculates the desired number of threads based on the ‘current throughput’, which is the ‘# of tasks completed’ (numCompletions) during the current time-period (sampleDuration in seconds).
...
It also takes the current thread count (currentThreadCount) into consideration.
...
The real .NET Thread Pool only increases the thread-count by one thread every 500 milliseconds. It keeps doing this until the ‘# of threads’ has reached the amount that the hill-climbing algorithm suggests.
...
The [hill-climbing] algorithm only returns values that respect the limits specified by ThreadPool.SetMinThreads(..) and ThreadPool.SetMaxThreads(..)
...
In addition, [the hill-climbing algorithm] will only recommend increasing the thread count if the CPU Utilization is below 95%
原来线程池确实有一个基于任务完成率的反馈机制。它也不会明确检查其线程是否被阻塞或 运行,但它确实会关注总体 CPU 利用率以检测阻塞。所有这些也意味着它应该大致了解其他线程和进程在做什么。
另一方面,它总是会急切地生成至少与 ThreadPool.SetMinThreads()
指定的线程一样多的线程,默认为机器上逻辑处理器的数量。
总而言之,有问题的测试代码做了两件事,使其不断堆积更多线程:
- 有很多任务在排队,并且在队列中等待了很长时间,这表明饥饿
- CPU利用率可以忽略不计,这意味着一个新的线程应该可以使用它