[英]TPL with LongRunning state and thread synchronization and performance
关于TPL与LongRunning状态的使用,我有一个疑问。
来自MSDN TPL的目的是通过简化向应用程序添加并行性和并发性的过程来提高开发人员的工作效率。 TPL 动态地扩展并发度,以最有效地使用所有可用的处理器核心。 TPL的另一个好处是, 您不必处理线程创建和同步。
但是如果我设置LongRunning选项,TPL会从线程池外部分配一个专用线程。 因此,在这种情况下,它将工作一些类似于传统的线程(我相信,如果我错了,请纠正)。 那么在这种情况下,TPL本身会处理如上所述的线程创建和同步吗? 它还会自动/内部动态地扩展并发度,以最有效地使用所有处理器内核或开发人员需要编写代码来处理所有这些吗?
在下面某处总是存在“传统线程”。
本机线程“很重”。 如果你为每个任务执行一个线程,然后创建很多任务(因此线程),那么你可能会饿死/停止进程(甚至整个机器或某些系统)。 这使得注册许多微小的操作并以这种方式处理并且该障碍影响您的代码体系结构是不可能/不可行的。
这就是线程池的用武之地。将想法更改为不为每个作业运行一个线程,但是让一些线程池化并让它们在共享任务队列上工作,将线程数量限制为池中的N个,并获得背景消息处理的好处。
草拟这个想法,重要的是要注意线程池(通常)仅限于N个线程。 这意味着如果您将许多长时间运行的任务注册到线程池,您可能会饿死它。 如果处理微小的快速工作,Threadpool的效果最好。
这就是TPL允许您指定哪些作业“长”的原因。 他们想让你有能力减轻线程池的压力。 使用基于任务的操作,线程池继续运行非常重要。 允许它被饿死,所有任务都必须等待一些长时间的操作才能完成。 这完全不是它的全部意义!
我确信TPL处理专用于LongRunning作业的单独线程的创建和管理。
至于第二个问题 - 实际上我不知道。 “选择最有效的”通常是一项艰巨的任务,所以我很安全地说“不,它并不是在所有情况下都这样做”:)))我认为TPL中DoP / DoC的扩展是就像将线程池大小调整为机器上的逻辑处理器数一样简单。 LongRunning作业的单独线程仍将超出限制,因此ThreadPool是安全的。 将它们包含在DoP / DoC限制中会使池以同样的方式饿死,因为它会减少可用线程的数量。 我不认为TPL在扩展方面做得更多。 Maaybe它安排在相同线程上对相同数据进行操作的子任务以获得一些缓存或NUMA提升..但我不知道,无论如何这都是相当远的猜测。
我刚刚发现了一篇你可能会感兴趣的文章: 新的和改进的CLR 4线程池引擎 - 我很确定默认的TaskScheduler使用该池。 (来自JohnSkeet: https ://stackoverflow.com/a/4534902/717732)
另一个例子,有人对线程池大小和LongRunning标志执行了一些测试: Threadpool线程饥饿 - 一个实际的例子
TPL动态地扩展并发度,以最有效地使用所有可用的处理器核心。 TPL的另一个好处是,您不必处理线程创建和同步。
此语句适用于Parallel.For
TPL API系列。 它不适用于Task.Run
或Task.Factory.StartNew
,您可以在其中明确控制并行度。
对于Task.Run
(以及具有默认选项的Task.Factory.StartNew
),没有智能的“缩放”。 它只是简单的循环执行工作项,就像使用ThreadPool.QueueUserWorkItem
。 这实际上可能会使所有可用的池线程(最多为ThreadPool.GetMaxThreads
),然后在忙池线程可用时将新任务ThreadPool.GetMaxThreads
队列以进行延迟执行。 它也可能是线程池口吃问题的主题 。
使用Task.Factory.StartNew
与LongRunning
处只在于你可以逃脱线程池口吃的问题不同,但最终,你可以简单地耗尽操作系统的内存和其他资源,为OS线程是一个非常昂贵的资源。
在使用Parallel.For
等的情况下,TPL调度程序更加智能。 它不会在每个工作项的一个线程上浪费线程。 相反,它有一个非常复杂的命令逻辑,考虑到CPU /核心的数量以及可能的一些其他运行时指标。
更新以解决评论,这是一个简单的例子:
using System;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApplication
{
class Program
{
static void Main(string[] args)
{
int max = 50;
int delay = 30; // ~30s per work item
ThreadPool.SetMaxThreads(max, max);
Console.WriteLine("starting, threads: {0}", Process.GetCurrentProcess().Threads.Count);
var tasks = Enumerable.Range(0, max).Select(n => Task.Factory.StartNew(() =>
{
Console.WriteLine("task: {0}, threads: {1}, pool thread: {2}",
n, Process.GetCurrentProcess().Threads.Count, Thread.CurrentThread.IsThreadPoolThread);
for (int i = 0; i < delay * 1000; i++)
{
Thread.Sleep(1);
}
})).ToArray();
Console.WriteLine("waiting, threads: {0}", Process.GetCurrentProcess().Threads.Count);
Task.WaitAll(tasks);
Console.WriteLine("done, threads: {0}", Process.GetCurrentProcess().Threads.Count);
Console.ReadLine();
}
}
}
输出(发布版本,未附带调试器,.NET 4.5,4核CPU):
starting, threads: 3 task: 0, threads: 11, pool thread: True task: 2, threads: 11, pool thread: True waiting, threads: 11 task: 1, threads: 11, pool thread: True task: 3, threads: 11, pool thread: True ... task: 48, threads: 56, pool thread: True task: 49, threads: 57, pool thread: True done, threads: 47
它确认了ThreadPool
的增长和口吃行为,直到max
线程数。 新线程创建时延迟约500毫秒。
现在,如果我们将TaskCreationOptions.LongRunning
添加到Task.Factory.StartNew
,我们就消除了口吃,我们不再受ThreadPool
大小的限制,但是我们仍然最终会接受新线程的max
数量,一个每个任务(取决于每个工作项执行的程度)。
它还会自动/内部动态地扩展并发度,以最有效地使用所有处理器内核或开发人员需要编写代码来处理所有这些吗?
因此,如果开发人员想要使用TPL的Task.Run
或Task.Factory.StartNew
API,他或她确实需要手动处理并行级别。 但这并不困难,例如SemaphoreSlim
。
那么在这种情况下,TPL本身会处理如上所述的线程创建和同步吗?
LongRunning
Task
只不过是一个包含在任务中的线程。 这样做的好处是,您可以查询其状态,设置延续,等待它并传播错误。 您也可以使用WaitAll
等组合器。
这就是LongRunning
选项的全部LongRunning
。
它还会自动/内部动态地扩展并发度,以最有效地使用所有处理器内核或开发人员需要编写代码来处理所有这些吗?
你将如何“扩展”单个线程/任务? 它本质上是不可扩展的。 您需要多个独立的工作单元(例如任务或数据项)才能使用多个处理器。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.