限制并发System.Threading.Tasks.Task的数量

时间:2016-11-21 14:59:05

标签: c# asynchronous concurrency

我需要对我的数据库执行大量数据插入。我可以使用限制调度程序以多线程方式实现代码,该调度程序限制并发操作的数量。在每个M行上,形成一个块并作为原子操作插入到数据库中。应该发生多个并发操作,因为数据库比读取和解析数据文件慢。我经常使用多线程来实现这个模型。

如果我决定使用await / async实现我的代码(实体框架支持异步编程),如何确保不超过N个并发任务执行(即转到数据库)at同时?

在我的初始设计中,我实例化了一个List<Task>,一读到要以原子方式插入的数据块就添加了新任务,然后让我的方法在await之后返回任务。设计时问题是并发Task的数量(以及内存占用量)将会爆炸,因为任务的输送速度比完成大数据文件要快。

我在考虑使用SemaphoreSlim,但我对异步编程的经验不多(与多线程不同)。所以我问这个问题是否有关于最佳实践的反馈,如果有的话。

3 个答案:

答案 0 :(得分:1)

  

设计时问题是并发任务的数量(以及因此内存占用量)将会爆炸,因为任务的执行速度比完成大数据文件要快。我在考虑使用SemaphoreSlim

是的,SemaphoreSlim是限制并发异步操作的合适选择:

private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(10);

async Task ThrottledWorkAsync()
{
  await _semaphore.WaitAsync();
  try
  {
    await WorkAsync();
  }
  finally
  {
    _semaphore.Release();
  }
}

...然而

  

如果我决定使用await / async实现我的代码(实体框架支持异步编程),我如何确保不会同时执行N个并发任务(即转到数据库)?

需要注意的一点是,实体框架 - 虽然它支持异步API - 仍然需要每个请求一个连接。因此,您不能拥有多个具有相同DbContext的并发异步请求;您需要为每个并发请求创建一个单独的连接(或至少N个并发请求“借用”的连接)。

答案 1 :(得分:1)

如果您最初要插入n个值(n是最大并发任务数),您可以采取以下方法:

  1. 使用不同的值调用InsertAsync() n次。
  2. 当每项任务完成后,继续拨打InsertAsync()的新电话(重复2)。
  3. 这样,您不需要使用信号量来控制并发级别,并且不会阻塞。

    I've just published a package对此方案有用,它会公开2个方法Times()Map()https://github.com/jorgebay/concurrent-utils

    例如:

    // Execute MyMethodAsync() 1,000,000 times limiting the maximum amount
    // of parallel async operations to 512
    await ConcurrentUtils.Times(1000000, 512, (index) => MyMethodAsync(index));
    

答案 2 :(得分:0)

我使用这段代码来执行我的线程:

public static async Task WhenAll(this List<Func<Task>> actions, int threadCount)
{
    var executeTaskHelper = new ConcurrentTaskHelper(threadCount);
    return executeTaskHelper.Execute(actions);
}

public class ConcurrentTaskHelper
{
    int _threadCount;
    CountdownEvent _countdownEvent;
    SemaphoreSlim _throttler;

    public ConcurrentTaskHelper(int threadCount)
    {
        _threadCount = threadCount;
         _throttler  = new SemaphoreSlim(threadCount);
    }

    public async Task Execute(List<Func<Task>> tasks)
    {
        _countdownEvent = new CountdownEvent(tasks.Count);

        foreach (var task in tasks)
        {
            await _throttler.WaitAsync();
            Execute(task);
        }

        _countdownEvent.Wait();
    }

    private async Task Execute(Func<Task> task)
    {
        try { await task(); }
        finally { Completed(); }
    }

    private void Completed()
    {
        _throttler.Release();
        _countdownEvent.Signal();
    }
}

此代码基于此主题中提供的代码:How to limit the amount of concurrent async I/O operations?

不使用CountdownEvent,最好实现AsyncCountdownEvent。这样就可以使用_await countdownEvent.WaitAsync();

调用它应该看起来像这样。它将执行所有任务,但只有40个(在这种情况下)并发:

var tasks = new List<Func<Task>>();
tasks.Add(() => saveAsync());
//add more
await tasks.WhenAll(40);