Асинхронный код, общие переменные, потоки пула потоков и безопасность потоков - PullRequest
8 голосов
/ 07 октября 2019

Когда я пишу асинхронный код с помощью async / await, обычно с ConfigureAwait(false), чтобы избежать захвата контекста, мой код переходит от одного потока пула потоков к следующему после каждого await. Это вызывает опасения по поводу безопасности потоков. Безопасен ли этот код?

static async Task Main()
{
    int count = 0;
    for (int i = 0; i < 1_000_000; i++)
    {
        Interlocked.Increment(ref count);
        await Task.Yield();
    }
    Console.WriteLine(count == 1_000_000 ? "OK" : "Error");
}

Переменная i не защищена и доступна нескольким потокам пула потоков *. Хотя схема доступа не является одновременной, теоретически для каждого потока должна быть возможность увеличить локально кэшированное значение i, что приведет к более чем 1 000 000 итераций. Я не могу представить этот сценарий на практике, хотя. Код выше всегда печатает ОК на моей машине. Означает ли это, что код является потокобезопасным? Или я должен синхронизировать доступ к переменной i, используя lock?

(* один поток переключается в среднем каждые 2 итерации, согласно моим тестам)

Ответы [ 2 ]

3 голосов
/ 07 октября 2019

Проблема с безопасностью потоков связана с чтением / записью памяти. Даже если это может продолжаться в другом потоке, здесь ничего не выполняется одновременно.

0 голосов
/ 18 октября 2019

Полагаю, эта статья Стивена Туба может пролить свет на это. В частности, это релевантный отрывок о том, что происходит во время переключения контекста:

Всякий раз, когда код ожидает ожидающего, ожидающий которого говорит, что он еще не завершен (то есть IsCompleted ожидающего возвращает false), метод долженприостановить, и он возобновится через продолжение ожидания. Это одна из тех асинхронных точек, о которых я говорил ранее, и, следовательно, ExecutionContext должен перетекать из кода, выдающего ожидание, в выполнение делегата продолжения. Это автоматически обрабатывается платформой. Когда асинхронный метод собирается приостановить работу, инфраструктура перехватывает ExecutionContext. Делегат, который передается ожидающему, имеет ссылку на этот экземпляр ExecutionContext и будет использовать его при возобновлении метода. Это то, что позволяет важной «окружающей» информации, представленной ExecutionContext, проходить через ожидание.

Стоит отметить, что YieldAwaitable, возвращаемый Task.Yield(), всегда возвращает false.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...