如何限制多个异步任务?

时间:2015-08-17 09:35:35

标签: c# asynchronous async-await task-parallel-library

我有以下形式的代码:

static async Task DoSomething(int n) 
{
  ...
}

static void RunThreads(int totalThreads, int throttle) 
{
  var tasks = new List<Task>();
  for (var n = 0; n < totalThreads; n++)
  {
    var task = DoSomething(n);
    tasks.Add(task);
  }
  Task.WhenAll(tasks).Wait(); // all threads must complete
}

麻烦的是,如果我没有限制线程,事情就会开始崩溃。现在,我想启动最多throttle个线程,并且仅在旧线程完成时启动新线程。我尝试过几种方法,迄今为止没有一种方法可行。我遇到的问题包括:

  • tasks集合必须完全填充所有任务,无论是活动还是等待执行,否则最终.Wait()调用只会查看它开始的线程。
  • 链接执行似乎需要使用Task.Run()等。但是我需要从一开始就对每个任务进行引用,并且实例化任务似乎会自动启动它,这是我不想要的。

怎么做?

8 个答案:

答案 0 :(得分:8)

最简单的选择IMO是使用TPL Dataflow。您只需创建一个ActionBLock,按所需的并行度限制它并开始将项目发布到其中。它确保只同时运行一定数量的任务,当任务完成时,它开始执行下一个项目:

async Task RunAsync(int totalThreads, int throttle) 
{
    var block = new ActionBlock<int>(
        DoSomething,
        new ExecutionDataFlowOptions { MaxDegreeOfParallelism = throttle });

    for (var n = 0; n < totalThreads; n++)
    {
        block.Post(n);
    }

    block.Complete();
    await block.Completion;
}

答案 1 :(得分:6)

如果我理解正确,您可以启动throttle参数提到的任务数量有限的任务,并在下一个开始之前等待它们完成..

要在开始新任务之前等待所有已启动的任务完成,请使用以下实现。

static async Task RunThreads(int totalThreads, int throttle)
{
    var tasks = new List<Task>();
    for (var n = 0; n < totalThreads; n++)
    {
        var task = DoSomething(n);
        tasks.Add(task);

        if (tasks.Count == throttle)
        {
            await Task.WhenAll(tasks);
            tasks.Clear();
        }
    }
    await Task.WhenAll(tasks); // wait for remaining
}

要在完成任务时添加任务,您可以使用以下代码

static async Task RunThreads(int totalThreads, int throttle)
{
    var tasks = new List<Task>();
    for (var n = 0; n < totalThreads; n++)
    {
        var task = DoSomething(n);
        tasks.Add(task);

        if (tasks.Count == throttle)
        {
            var completed = await Task.WhenAny(tasks);
            tasks.Remove(completed);
        }
    }
    await Task.WhenAll(tasks); // all threads must complete
}

答案 2 :(得分:6)

Stephen Toub在他的The Task-based Asynchronous Pattern文档中提供了以下关于限制的示例。

const int CONCURRENCY_LEVEL = 15;
Uri [] urls = …;
int nextIndex = 0;
var imageTasks = new List<Task<Bitmap>>();
while(nextIndex < CONCURRENCY_LEVEL && nextIndex < urls.Length)
{
    imageTasks.Add(GetBitmapAsync(urls[nextIndex]));
    nextIndex++;
}

while(imageTasks.Count > 0)
{
    try
    {
        Task<Bitmap> imageTask = await Task.WhenAny(imageTasks);
        imageTasks.Remove(imageTask);

        Bitmap image = await imageTask;
        panel.AddImage(image);
    }
    catch(Exception exc) { Log(exc); }

    if (nextIndex < urls.Length)
    {
        imageTasks.Add(GetBitmapAsync(urls[nextIndex]));
        nextIndex++;
    }
}

答案 3 :(得分:4)

Microsoft的Reactive Extensions(Rx) - NuGet&#34; Rx-Main&#34; - 这个问题排序得非常好。

这样做:

static void RunThreads(int totalThreads, int throttle) 
{
    Observable
        .Range(0, totalThreads)
        .Select(n => Observable.FromAsync(() => DoSomething(n)))
        .Merge(throttle)
        .Wait();
}

完成工作。

答案 4 :(得分:4)

首先,从线程中抽象出来。特别是由于您的操作是异步的,因此您根本不应该考虑“线程”。在异步世界中,您拥有任务,与线程相比,您可以拥有巨大个任务。

可以使用SemaphoreSlim来限制异步代码:

static async Task DoSomething(int n);

static void RunConcurrently(int total, int throttle) 
{
  var mutex = new SemaphoreSlim(throttle);
  var tasks = Enumerable.Range(0, total).Select(async item =>
  {
    await mutex.WaitAsync();
    try { DoSomething(item); }
    finally { mutex.Release(); }
  });
  Task.WhenAll(tasks).Wait();
}

答案 5 :(得分:0)

以下是一些基于Sriram Sakthivel答案的扩展方法变体。

在用法示例中,对DoSomething的调用被包装在显式转换的闭包中以允许传递参数。

public static async Task RunMyThrottledTasks()
{
    var myArgsSource = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    await myArgsSource
        .Select(a => (Func<Task<object>>)(() => DoSomething(a)))
        .Throttle(2);
}

public static async Task<object> DoSomething(int arg)
{
    // Await some async calls that need arg..
    // ..then return result async Task..
    return new object();
}

public static async Task<IEnumerable<T>> Throttle<T>(IEnumerable<Func<Task<T>>> toRun, int throttleTo)
{
    var running = new List<Task<T>>(throttleTo);
    var completed = new List<Task<T>>(toRun.Count());
    foreach(var taskToRun in toRun)
    {
        running.Add(taskToRun());
        if(running.Count == throttleTo)
        {
            var comTask = await Task.WhenAny(running);
            running.Remove(comTask);
            completed.Add(comTask);
        }
    }
    return completed.Select(t => t.Result);
}

public static async Task Throttle(this IEnumerable<Func<Task>> toRun, int throttleTo)
{
    var running = new List<Task>(throttleTo);
    foreach(var taskToRun in toRun)
    {
        running.Add(taskToRun());
        if(running.Count == throttleTo)
        {
            var comTask = await Task.WhenAny(running);
            running.Remove(comTask);
        }
    }
}

答案 6 :(得分:-1)

您需要的是自定义任务调度程序。您可以从 System.Threading.Tasks.TaskScheduler 派生一个类并实现两个主要函数 GetScheduledTasks()QueueTask() 以及其他函数以完全控制节流任务。这是一个有据可查的示例。

https://youtu.be/5VsRFJjyMjU

答案 7 :(得分:-1)

您实际上可以模拟作为 .NET 6 的一部分引入的 final MultiThreadedHttpConnectionManager connectionManager = new MultiThreadedHttpConnectionManager(); connectionManager.setMaxTotalConnections(150); connectionManager.setMaxConnectionsPerHost(90); connectionManager.setConnectionTimeout(15000); connectionManager.setSoTimeout(600000); 方法。为了模拟相同的内容,您可以使用以下代码。

Parallel.ForEachAsync