Семафорная дроссельная заслонка с асинхронным/ожиданием

Недавно я встретил пример дросселирования потоков для вызовов async/wait. После анализа и игры с кодом на моей машине я придумал несколько другой способ сделать то же самое. Что я сомневаюсь в том, что то, что происходит под капотом, почти одинаково или если есть какие-то тонкие различия, которые стоит отметить?

Здесь код, основанный на исходном примере:

private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(5);

public async Task CallThrottledTasks()
{
    var tasks = new List<Task>();

    for (int count = 1; count <= 20; count++)
    {
        await _semaphore.WaitAsync();

        tasks.Add(Task.Run(async () =>
            {
                try
                {
                    int result = await LongRunningTask();
                    Debug.Print(result.ToString());
                }
                finally
                {
                    _semaphore.Release();
                }
            }));
    }

    await Task.WhenAll(tasks);

    Debug.Print("Finished CallThrottledTasks");
}

И вот мой пример по тому же коду:

private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(5);

public async Task CallThrottledTasks()
{
    var tasks = new List<Task>();

    for (int count = 1; count <= 20; count++)
    {
        await _semaphore.WaitAsync();

        tasks.Add(LongRunningTask().ContinueWith(t =>
        {
            try
            {
                int result = t.Result;
                Debug.Print(result.ToString());
            }
            finally
            {
                _semaphore.Release();
            }
        }));
    }

    await Task.WhenAll(tasks);

    Debug.Print("Finished CallThrottledTasks");
}

Я, вероятно, ушел, но похоже, что подход Task.Run создает задачу для запуска LongRunningTask(), а затем добавляет продолжение для печати результата, тогда как мой подход обходит задачу, созданную Task.Run, и в результате получается немного более компактный. Является ли это точным или я отсюда без основания?

Ответ 1

Это не намного скуднее, всего лишь немного. Обычно я избегаю ContinueWith в async, потому что await является более чистым и имеет более async -дружественную по умолчанию семантику. Сначала оптимизируйте время разработки, а затем оптимизируйте для других соображений.

Ваш код немного меняет семантику: в исходном коде LongRunningTask выполнялся из контекста пула потоков, а в вашем коде он выполнялся из любого контекста CallThrottledTasks. Кроме того, ваш код не будет распространять исключения из LongRunningTask чисто; Task<T>.Result будет обертывать исключения в AggregateException, а await не будет делать никаких оберток.