SemaphoreSlim不会限制任务

时间:2019-09-30 23:11:24

标签: c# .net multithreading semaphore

我创建了以下方法TestThrottled来尝试限制我的任务,但是当我调用WhenAll时,它根本没有节流,并且此方法都具有相同的经过时间。我做错什么了吗?

    private static async Task<T[]> TestThrottled<T>(List<Task<T>> tasks, int maxDegreeOfParallelism)
    {
        var semaphore = new SemaphoreSlim(maxDegreeOfParallelism);
        var tasksParallelized = new List<Task<T>>();

        foreach (var task in tasks)
        {
            var taskParallelized = Task.Run(async () =>
            {
                try
                {
                    await semaphore.WaitAsync();

                    return await task;
                }
                finally
                {
                    semaphore.Release();
                }
            });
            tasksParallelized.Add(taskParallelized);
        }

        return await Task.WhenAll(tasksParallelized);
    }

    private static async Task<int> TestAsync()
    {
        await Task.Delay(1000);

        return 1;
    }

    static async Task Main(string[] args)
    {
        var sw = Stopwatch.StartNew();

        var tasks = new List<Task<int>>();
        var ints = new List<int>();

        for (int i = 0; i < 30; i++)
        {
            tasks.Add(TestAsync());
        }
        ints.AddRange(await TestThrottled(tasks, 1));

        Console.WriteLine($"{sw.ElapsedMilliseconds}, count: {ints.Count}");
        Console.ReadLine();
    }

4 个答案:

答案 0 :(得分:3)

这里的主要问题是async/await的行为。打电话时会发生什么

private static async Task<int> TestAsync()
{ 
    await Task.Delay(1000);
    return 1;
}

TestAync();

TestAsync()被呼叫。在该方法中,将调用Task.Delay()。这将创建一个在1000毫秒后完成的任务。最后,您返回该任务(实际上是另一个任务,它是由Task.Delay()返回的任务的继续执行)。

您可以在Main()的循环中几乎同时创建所有这些任务。因此,尽管您可能已经准备好了一个信号量,以防止多个线程同时调用await task,但无论如何,它们都计划在大约同一时间完成。 await仅在任务尚未完成时等待。因此,只要第一个线程释放了信号量(大约一秒钟之后),下一个线程便可以进入关键区域,在关键区域中它将发现任务已经完成(或即将完成)。然后,它可以立即释放信号量。其余任务也会发生这种情况,您的总运行时间约为一秒钟。

答案 1 :(得分:3)

您可以使用TPL DataFlow完成此操作的另一种方式,它已经拥有了您所需的一切,并且可以满足更复杂的管道铺设的需要,并且面条更具可配置性。就像示例解决方案

一样,它还节省了您转移到另一个任务的负担。
private static async Task<IList<T>> TestThrottled<T>(IEnumerable<Func<Task<T>>> tasks, int maxDegreeOfParallelism)
{
   var options = new ExecutionDataflowBlockOptions() { EnsureOrdered = false, MaxDegreeOfParallelism = maxDegreeOfParallelism };

   var transform = new TransformBlock<Func<Task<T>>, T>(func => func.Invoke(), options);
   var outputBufferBlock = new BufferBlock<T>();

   transform.LinkTo(outputBufferBlock, new DataflowLinkOptions(){PropagateCompletion = true});

   foreach (var task in tasks)
      transform.Post(task);

   transform.Complete();
   await outputBufferBlock. Completion;

   outputBufferBlock.TryReceiveAll(out var result);

   return result;
}

答案 2 :(得分:1)

我解决了我的问题(创建一个接收到异步方法列表的通用节流任务运行器),如下所示:

    private static async Task<T[]> RunAsyncThrottled<T>(IEnumerable<Func<Task<T>>> tasks, int maxDegreeOfParallelism)
    {
        var tasksParallelized = new List<Task<T>>();

        using (var semaphore = new SemaphoreSlim(maxDegreeOfParallelism))
        {
            foreach (var task in tasks)
            {
                var taskParallelized = Task.Run(async () =>
                {
                    await semaphore.WaitAsync();
                    try
                    {
                        return await task.Invoke();
                    }
                    finally
                    {
                        semaphore.Release();
                    }
                });
                tasksParallelized.Add(taskParallelized);
            }

            return await Task.WhenAll(tasksParallelized);
        }
    }

    private static async Task<int> TestAsync(int num)
    {
        await Task.Delay(1000);

        return 1 + num;
    }

    static async Task Main(string[] args)
    {
        var sw = Stopwatch.StartNew();

        var tasks = new List<Func<Task<int>>>();
        var ints = new List<int>();

        for (int i = 0; i < 10; i++)
        {
            tasks.Add(() => TestAsync(12000));
        }

        ints.AddRange(await RunAsyncThrottled(tasks, 1000));

        Console.WriteLine($"{sw.Elapsed.TotalMilliseconds}, count: {ints.Count}");
        Console.ReadLine();
    }

答案 3 :(得分:1)

解决此问题的关键是让油门启动任务,而不是事先启动它们。而且,由于使用旧的Task.Start方法显式启动任务是非常严格的(先行且无法利用async-await机制),因此唯一的选择是让调节器创建任务。有多种方法可以做到这一点:

1)传递任务工厂而不是任务。此方法已在其他答案中得到证实。

private static async Task<TResult[]> RunAsyncThrottled<TResult>(
    IEnumerable<Func<Task<TResult>>> taskFactories,
    int maxDegreeOfParallelism)
{
    //...
    foreach (var taskFactory in taskFactories)
        //...
        var task = taskFactory();
        TResult result = await task;
}

2)传递一个项目序列和一个接受一个项目作为参数的单个任务工厂。这是最常用的方法:

private static async Task<TResult[]> RunAsyncThrottled<TSource, TResult>(
    IEnumerable<TSource> items, Func<TSource, Task<TResult>> taskFactory,
    int maxDegreeOfParallelism)
{
    //...
    foreach (var item in items)
        //...
        var task = taskFactory(item);
        TResult result = await task;
}

3)传递一个递延的枚举任务。可以使用LINQ或迭代器(yield的方法)创建此类可枚举的对象。 Here是一个完整的示例。

private static async Task<TResult[]> RunAsyncThrottled<TResult>(
    IEnumerable<Task<TResult>> tasks, int maxDegreeOfParallelism)
{
    if (tasks is ICollection<Task<TResult>>) throw new ArgumentException(
        "The enumerable should not be materialized.", nameof(tasks));
    //...
    foreach (var task in tasks)
        //...
        TResult result = await task;
}

由于C# 8现在已发布,因此该方法的返回值还有其他选择。无需返回Task<TResult[]>,而是可以返回 IAsyncEnumerable<TResult>,允许与await foreach进行异步枚举。

private static async IAsyncEnumerable<TResult> RunAsyncThrottled<TSource, TResult>(
    IEnumerable<TSource> items, Func<TSource, Task<TResult>> taskFactory,
    int maxDegreeOfParallelism)
{
    //...
    foreach (var item in items)
        //...
        yield return await taskFactory(item);
}