Разрешить метод асинхронного вызова только по одному экземпляру за раз - программирование
Подтвердить что ты не робот

Разрешить метод асинхронного вызова только по одному экземпляру за раз

У меня есть метод, который не может выполняться одновременно в нескольких потоках (он записывает в файл). Я не могу использовать lock, потому что метод async. Как избежать вызова метода в другом потоке? Вместо того, чтобы вызывать его второй раз, программа должна дождаться завершения предыдущего вызова (также асинхронно).

Например, создайте новое консольное приложение С# со следующим кодом:

using System;
using System.IO;
using System.Threading.Tasks;
namespace ConsoleApplication1 {
    internal class Program {

        private static void Main () {
            CallSlowStuff();
            CallSlowStuff();
            Console.ReadKey();
        }

        private static async void CallSlowStuff () {
            try {
                await DoSlowStuff();
                Console.WriteLine("Done!");
            }
            catch (Exception e) {
                Console.WriteLine(e.Message);
            }
        }

        private static async Task DoSlowStuff () {
            using (File.Open("_", FileMode.Create, FileAccess.Write, FileShare.None)) {
                for (int i = 0; i < 10; i++) {
                    await Task.Factory.StartNew(Console.WriteLine, i);
                    await Task.Delay(100);
                }
            }
        }
    }
}

В этом примере второй вызов CallSlowStuff выдает исключение, потому что он не может получить доступ к уже открытому файлу. Добавление lock не является опцией, потому что lock и async не смешиваются. (Main метод должен считаться нечувствительным. В реальном приложении CallSlowStuff - это метод интерфейса, который можно назвать где угодно.)

Вопрос. Как сделать последующие вызовы CallSlowStuff ждать завершения текущего выполняемого вызова без блокировки основного потока? Может ли это быть сделано с помощью async/wait и задач (и Rx может быть)?

4b9b3361

Ответ 1

Вам нужна какая-то асинхронная блокировка. У Stephen Toub есть целая серия статей о создании async примитивов синхронизации (включая AsyncLock). Версия AsyncLock также содержится в Stephen Cleary AsyncEx library.

Но, возможно, более простым решением было бы использовать встроенный SemaphoreSlim, который поддерживает асинхронное ожидание:

private static SemaphoreSlim SlowStuffSemaphore = new SemaphoreSlim(1, 1);

private static async void CallSlowStuff () {
    await SlowStuffSemaphore.WaitAsync();
    try {
        await DoSlowStuff();
        Console.WriteLine("Done!");
    }
    catch (Exception e) {
        Console.WriteLine(e.Message);
    }
    finally {
        SlowStuffSemaphore.Release();
    }
}

Ответ 2

Я бы подумал об изменении тела метода CallSlowStuff, чтобы отправлять сообщения в TPL DataFlow ActionBlock и настраивать его на одну степень parallelism:

Итак, держите один ActionBlock где-то:

ActionBlock actionBlock = 
    new ActionBlock<object>(
         (Func<object,Task>)CallActualSlowStuff,
         new ExecutionDataflowBlockOptions(){MaxDegreeOfParallelism=1});

Сейчас:

public void CallSlowStuff()
{
    actionBlock.Post(null);
}

private async Task CallActualSlowStuff(object _)
{
    using (File.Open("_", FileMode.Create, FileAccess.Write, FileShare.None)) {
        for (int i = 0; i < 10; i++) {
            await Task.Factory.StartNew(Console.WriteLine, i);
            await Task.Delay(100);
        }
    }
}