Являются ли не поточно-безопасные функции асинхронными? - PullRequest
0 голосов
/ 07 июня 2018

Рассмотрим следующую асинхронную функцию, которая изменяет список, не поддерживающий потоки:

async Task AddNewToList(List<Item> list)
{
    // Suppose load takes a few seconds
    Item item = await LoadNextItem();
    list.Add(item);
}

Проще говоря: это безопасно?

Меня беспокоит, что можно вызвать метод асинхронностии затем во время загрузки (либо в другом потоке, либо в качестве операции ввода-вывода) вызывающая сторона может изменить список.

Предположим, что вызывающая сторона находится в процессе выполнения list.Clear (),например, и внезапно метод Load завершается!Что произойдет?

Будет ли задача немедленно прервана и выполнит код list.Add(item);?Или он будет ждать, пока основной поток завершит выполнение всех запланированных задач ЦП (т. Е. Дождаться завершения Clear ()), прежде чем запускать код? Редактировать : Поскольку я в основном ответил на это ниже для себя, вот дополнительный вопрос: почему?Почему он немедленно прерывается, а не ожидает завершения операций, связанных с процессором?Кажется нелогичным не ставить себя в очередь, что было бы совершенно безопасно.

Редактировать: Вот другой пример, который я проверял сам.В комментариях указывается порядок исполнения.Я разочарован!

TaskCompletionSource<bool> source;
private async void buttonPrime_click(object sender, EventArgs e)
{
    source = new TaskCompletionSource<bool>();  // 1
    await source.Task;                          // 2
    source = null;                              // 4
}

private void buttonEnd_click(object sender, EventArgs e)
{
    source.SetResult(true);                     // 3
    MessageBox.Show(source.ToString());         // 5 and exception is thrown
}

Ответы [ 4 ]

0 голосов
/ 08 июня 2018

Короткий ответ

Вы всегда должны быть осторожны при использовании асинхронного.

Более длинный ответ

Это зависит от ваших SynchronizationContext и TaskScheduler, и что вы подразумеваете под «безопасным».

Когда ваш код awaits что-то, он создает продолжение и оборачивает его в задачу, которая затем публикуется в TaskScheduler текущего SynchronizationContext.Затем контекст определит, когда и где будет выполняться продолжение.Планировщик по умолчанию просто использует пул потоков, но различные типы приложений могут расширять планировщик и предоставлять более сложную логику синхронизации.

Если вы пишете приложение, которое не имеет SynchronizationContext (например, консольное приложение или что-нибудь в ядре .NET ), продолжение просто помещается в пул потоков и может выполняться параллельно с вашим основным потоком.В этом случае вы должны использовать lock или синхронизированные объекты, такие как ConcurrentDictionary<> вместо Dictionary<>, для всего, кроме локальных ссылок или ссылок, которые закрыты с задачей.

Если вы пишете приложение WinForms, продолжения помещаются в очередь сообщений и будут выполняться в основном потоке.Это делает безопасным использование несинхронизированных объектов.Однако есть и другие проблемы, такие как взаимоблокировки .И, конечно, если вы порождаете какие-либо потоки, вы должны убедиться, что они используют lock или объекты Concurrent, и любые вызовы пользовательского интерфейса должны быть перенаправлены обратно в поток пользовательского интерфейса .Кроме того, если вы достаточно сумасшедший, чтобы написать приложение WinForms с более чем одним обработчиком сообщений (это весьма необычно), вам придется беспокоиться о синхронизации любых распространенных переменных.

Если вы пишете ASP.NETВ приложении SynchronizationContext будет гарантировать, что для данного запроса не будут выполняться одновременно два потока.Ваше продолжение может выполняться в другом потоке (из-за функции производительности, известной как гибкость потока ), но они всегда будут иметь одинаковый SynchronizationContext, и вам гарантировано, что никакие два потока не получат доступ к вашим переменным одновременно(конечно, при условии, что они не являются статичными, в этом случае они охватывают HTTP-запросы и должны быть синхронизированы).Кроме того, конвейер будет блокировать параллельные запросы для одного и того же сеанса, чтобы они выполнялись последовательно, поэтому ваше состояние сеанса также защищено от проблем с потоками.Однако вам все равно нужно беспокоиться о взаимоблокировках.

И, конечно, вы можете написать свой собственный SynchronizationContext и назначить его своим потокам, что означает, что вы определяете свои собственные правила синхронизации, которые будут использоваться сasync.

См. Также Как выполнить и дождаться выполнения потока управления в .NET?

0 голосов
/ 07 июня 2018

Предполагая, что в LoadNextItem() происходит "неправильный доступ": Task сгенерирует исключение.Поскольку контекст захвачен, он будет передан потоку вызывающих, поэтому list.Add не будет достигнут.

Итак, нет, он не является потокобезопасным.

0 голосов
/ 07 июня 2018

Да, я думаю, что это может быть проблемой.

Я бы вернул элемент и добавил бы его в список на главном шаге.

private async void GetIntButton(object sender, RoutedEventArgs e)
{
    List<int> Ints = new List<int>();
    Ints.Add(await GetInt());
}
private async Task<int> GetInt()
{
    await Task.Delay(100);
    return 1;
}

Но вы должны позвонить с и выполнить асинхронный вызов, поэтому я не делаю этого, это также будет работать.

0 голосов
/ 07 июня 2018

Нет, это не безопасно.Однако учтите также, что вызывающая сторона могла также порождать поток и передавать List его дочернему потоку перед вызовом вашего кода, даже в не асинхронной среде, что будет иметь такой же вредный эффект.

So;хотя это и небезопасно, в любом случае нет ничего поточно-ориентированного в получении списка от вызывающей стороны - нет никакого способа узнать, действительно ли список обрабатывается из других ваших потоков.

...