N个线程异步获取/执行任务

时间:2012-11-26 16:29:27

标签: c# multithreading asynchronous threadpool semaphore

我在某个db队列中有无限数量的任务。使程序在n个不同的线程上同时处理n个任务的最佳方法是什么,在旧的任务完成后启动新任务?当一个任务完成时,另一个任务应该异步开始。当前运行的计数应始终为n。

我最初的想法是使用线程池,但考虑到要在各个线程中检索要处理的任务,这似乎是不必要的。换句话说,每个线程将自己去获取它的下一个任务,而不是让主线程获取任务然后分发它们。

我看到了这样做的多种选择,我不知道应该使用哪一个来获得最佳性能。

1)线程池 - 鉴于不一定有任何等待线程,我不确定这是否必要。

2)信号量 - 与1.相同如果主线程没有等待分配任务,信号量有什么好处?

3)永远相同的线程 - 用n个线程关闭程序。当线程完成工作时,它将自己获得下一个任务。主线程只是监视以确保n个线程仍然存活。

4)事件处理 - 与3相同,除了线程完成任务时,它会在死亡之前触发ImFinished事件。一个ImFinished事件处理程序启动一个新线程。这似乎就像3但有更多的开销(因为不断创建新的线程)

5)还有别的吗?

4 个答案:

答案 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 methodActionBlock<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