我在某个db队列中有无限数量的任务。使程序在n个不同的线程上同时处理n个任务的最佳方法是什么,在旧的任务完成后启动新任务?当一个任务完成时,另一个任务应该异步开始。当前运行的计数应始终为n。
我最初的想法是使用线程池,但考虑到要在各个线程中检索要处理的任务,这似乎是不必要的。换句话说,每个线程将自己去获取它的下一个任务,而不是让主线程获取任务然后分发它们。
我看到了这样做的多种选择,我不知道应该使用哪一个来获得最佳性能。
1)线程池 - 鉴于不一定有任何等待线程,我不确定这是否必要。
2)信号量 - 与1.相同如果主线程没有等待分配任务,信号量有什么好处?
3)永远相同的线程 - 用n个线程关闭程序。当线程完成工作时,它将自己获得下一个任务。主线程只是监视以确保n个线程仍然存活。
4)事件处理 - 与3相同,除了线程完成任务时,它会在死亡之前触发ImFinished事件。一个ImFinished事件处理程序启动一个新线程。这似乎就像3但有更多的开销(因为不断创建新的线程)
5)还有别的吗?
答案 0 :(得分:4)
BlockingCollection
使整件事变得微不足道:
var queue = new BlockingCollection<Action>();
int numWorkers = 5;
for (int i = 0; i < numWorkers; i++)
{
Thread t = new Thread(() =>
{
foreach (var action in queue.GetConsumingEnumerable())
{
action();
}
});
t.Start();
}
然后,您可以在启动工作程序之后(或之前,如果需要)将主项添加到阻止集合中。您甚至可以生成多个生成器线程以将项添加到队列中。
请注意,更常规的方法是使用Tasks
而不是直接使用Thread
类。我没有先提出建议的主要原因是你特意要求运行确切数量的线程(而不是最大值),而你对Task
个对象的运行方式没有那么多的控制权(这很好;可以代表您进行优化)。如果控制不如您所说的那样重要,则以下结果最终可能更为可取:
var queue = new BlockingCollection<Action>();
int numWorkers = 5;
for (int i = 0; i < numWorkers; i++)
{
Task.Factory.StartNew(() =>
{
foreach (var action in queue.GetConsumingEnumerable())
{
action();
}
}, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default);
}
答案 1 :(得分:0)
我喜欢模特#3,之前曾用过它;它减少了启动和停止的线程数,并使主线程成为真正的“主管”,减少了它必须做的工作。
正如Servy所指出的,System.Collections.Concurrent命名空间有一些在这里非常有价值的构造。 ConcurrentQueue是一个线程安全的FIFO集合实现,旨在用于这样的模型;一个或多个“生产者”线程将元素添加到队列的“输入”侧,而一个或多个“消费者”从另一端获取元素。如果队列中没有任何内容,则获取项目的调用只返回false;您可以通过退出任务方法对此做出反应(然后主管可以决定是否启动另一个任务,可能是通过监视队列的输入并在更多项目进入时加速)。
BlockingCollection会添加导致线程在尝试从队列中获取值时等待的行为(如果队列没有任何内容)。它也可以配置为具有最大容量,超过该容量将阻止“生产者”线程添加任何更多元素,直到有可用容量。 BlockingCollection默认使用ConcurrentQueue,但如果您愿意,可以将其设置为Stack,Dictionary或Bag。使用此模型,您可以无限期地运行任务;当没有什么可做的时候,他们只会阻塞,直到至少有一个人能够继续工作,所以主管必须检查任务是否错误(任何强大的线程工作流模式的关键元素)。
答案 2 :(得分:0)
使用TPL Dataflow library很容易实现。
首先,我们假设你有一个BufferBlock<T>
,这是你的队列:
var queue = new BufferBlock<T>();
然后,您需要在块上执行操作,这由ActionBlock<T>
class表示:
var action = new ActionBlock<T>(t => { /* Process t here */ },
new ExecutionDataflowBlockOptions {
// Number of concurrent tasks.
MaxDegreeOfParallelism = ...,
});
请注意上面的构造函数,它采用ExecutionDataflowBlockOptions
的实例,并将MaxDegreeOfParallelism
property设置为您希望同时处理的并发项数。
在表面之下,任务并行库用于处理为任务分配线程等.TPL数据流意味着更高级别的抽象,允许您调整多少并行性/你想要的节流/等。
例如,如果您不希望ActionBlock<TInput>
缓冲任何项目(更喜欢将它们放在BufferBlock<T>
中),您还可以设置BoundedCapacity
property,这将限制ActionBlock<TInput>
将立即保留的项目数(包括正在处理的项目数以及保留项目):
var action = new ActionBlock<T>(t => { /* Process t here */ },
new ExecutionDataflowBlockOptions {
// Number of concurrent tasks.
MaxDegreeOfParallelism = ...,
// Set to MaxDegreeOfParallelism to not buffer.
BoundedCapacity ...,
});
此外,如果您想要一个全新的Task<TResult>
实例来处理每个项目,那么您可以将MaxMessagesPerTask
property设置为1,表示每个Task<TResult>
都会处理一个项目:
var action = new ActionBlock<T>(t => { /* Process t here */ },
new ExecutionDataflowBlockOptions {
// Number of concurrent tasks.
MaxDegreeOfParallelism = ...,
// Set to MaxDegreeOfParallelism to not buffer.
BoundedCapacity ...,
// Process once item per task.
MaxMessagesPerTask = 1,
});
请注意,根据您的应用程序运行的其他任务的数量,这对您来说可能是最佳的,也可能不是最佳的,您可能还想考虑为{{}中的每个项目启动新任务的成本。 {1}}。
从那里开始,通过调用LinkTo
method将ActionBlock<TInput>
与BufferBlock<T>
相关联是一件简单的事情:
ActionBlock<TInput>
您在此处将PropogateCompletion
property设置为true,以便在等待IDisposable connection = queue.LinkTo(action, new DataflowLinkOptions {
PropagateCompletion = true;
});
时,完成将发送到ActionBlock<T>
(如果/当没有其他项目需要处理时)您可能随后等待。
请注意,如果您希望删除块之间的链接,则可以调用ActionBlock<T>
调用返回的Dispose
method实施内的IDisposable
interface。
最后,使用Post
method:
LinkTo
当你完成后(如果你完成了),你打电话给Complete
method:
queue.Post(new T());
然后,在操作块上,您可以通过等待Task
公开的Completion
property实例来等待它完成:
queue.Complete();
希望,这种优雅很明显:
action.Completion.Wait();
实例/线程/等的创建来管理工作,块会根据您提供的设置为您执行此操作(这是在上每个块基础)。答案 3 :(得分:-1)
我是一个VB人,但你可以轻松翻译:
Private Async Sub foo()
Dim n As Integer = 16
Dim l As New List(Of Task)
Dim jobs As New Queue(Of Integer)(Enumerable.Range(1, 100))
For i = 1 To n
Dim j = jobs.Dequeue
l.Add(Task.Run((Sub()
Threading.Thread.Sleep(500)
Console.WriteLine(j)
End Sub)))
Next
While l.Count > 0
Dim t = Await Task.WhenAny(l)
If jobs.Count > 0 Then
Dim j = jobs.Dequeue
l(l.IndexOf(t)) = (Task.Run((Sub()
Threading.Thread.Sleep(500)
Console.WriteLine(j)
End Sub)))
Else
l.Remove(t)
End If
End While
End Sub
有一篇来自Stephen Toub的文章,为什么你不应该以这种方式使用Task.WhenAny ...有一个大的任务列表,但是有了“一些”任务你通常不会遇到问题
这个想法非常简单:你有一个列表,你可以在其中添加你想要并行运行的任务(运行)任务。然后你(a)等待第一个完成。如果队列中仍有作业,则将作业分配给新任务,然后(a)再次等待。如果队列中没有作业,则只需删除已完成的任务。如果你的任务列表和队列都是空的,那你就完成了。
Stephen Toub文章:http://blogs.msdn.com/b/pfxteam/archive/2012/08/02/processing-tasks-as-they-complete.aspx