Я вижу, что люди повсюду рекомендуют использовать ConfigureAwait(false) там, где это возможно, и это обязательно для авторов библиотек и так далее.
Но поскольку продолжение ConfigureAwait(false) может выполняться в любом потоке из пула потоков, то как вы можете безопасно защитить себя от доступа нескольких потоков к одному и тому же состоянию в вашей библиотеке?
Допустим, у вас есть следующий API для вашей библиотеки:
async Task FooAsync()
{
// Do something
//barAsync and saveToFileAsync are private methods.
await barAsync().ConfigureAwait(false);
// counter is a private field
counter++;
await saveToFileAsync().ConfigureAwait(false);
// Do other things
}
Если поток пользовательского интерфейса продолжает вызывать этот FooAsync (например, из-за нажатия кнопки пользователем), не повредит ли этот код значение counter и сохраненный файл? Поскольку может выполняться несколько потоков?
Мне трудно обдумывать использование ConfigureAwait(false), не будучи потокобезопасным, за исключением простых случаев, когда состояние не изменяется.
Возможно, я не совсем понял, но в нашей команде мы решили, что будем использовать однопоточную среду. Итак, из ответов ниже, кажется, что мы не можем использовать ConfigureAwait(false), поскольку он вводит возможность параллелизма, которым необходимо управлять с помощью блокировок и так далее.
Вам, вероятно, нужно будет изучить блокировку в этот момент
Рекомендуется использовать ConfigureAwait (false), если код не учитывает контекст. Примером является HttpContext в приложении asp .NET. Вы не можете получить доступ к этому из потоков, кроме потока запроса.
@Lasse Вы правы. Большинство людей не проектируют потокобезопасность и думают, что работает только один поток, который используется по умолчанию. Обычный async / await прекрасно сочетается с этим, однако в тот момент, когда вы добавляете ConfigureAwait (false), вам нужна потокобезопасность, но никто, похоже, не решает это в большинстве вопросов, которые я посетил, и создается впечатление, что ConfigureAwait (false) это простое изменение, но оно имеет последствия и может привести к повреждению состояния.
Most people don't design for thread-safety Не думаю, что это правда. Большинство библиотек хорошо являются потокобезопасными просто потому, что существующее небольшое глобальное состояние заблокировано. Нет никаких ожиданий, что отдельный объект должен быть потокобезопасным, если не указано иное (например, классы Concurrent...). Например, List<T> не является потокобезопасным как отдельный объект. SqlConnection не является потокобезопасным, но использует внутренний пул соединений.
@Charlieface Это внутренняя библиотека для мобильного приложения, а не для публики. Вы можете увидеть обновление, спасибо.
Как я уже сказал: главное не в параллелизме и потокобезопасности. Ключ в том, чтобы как можно больше избегать глобального состояния, чтобы не думать о блокировке.
@Charlieface Вы не можете избежать всего глобального состояния, да, вы можете уменьшить его, но вы не можете его устранить (я говорю здесь о частных полях, а не о глобально видимых полях между объектами). В таком случае ConfigureAwait(false) проблематичен, если не обращать внимания. Это не простое изменение.
Я не понимаю: если это частные поля, зачем вам блокировать? Зачем использовать объект в разных потоках, если в этом нет необходимости?





ConfigureAwait не о потокобезопасности. Речь идет о том, чтобы избежать захвата контекста.
Если вы хотите, чтобы ваш код был потокобезопасным, вам следует реализовать его так, чтобы он был. Обычно это связано с использованием какой-либо конструкции (конструкций) синхронизации, такой как, например, блокировка.
Как уже указывалось, ваш FooAsync() является поточно-ориентированным нет, даже если вы удалите вызовы ConfigureAwait(false). Два или более потока все еще могут вызывать его одновременно, даже в приложении пользовательского интерфейса, где есть SynchronizationContext.
how can you safely protect against multiple threads accessing the same state in your library?
Путем синхронизации доступа к любому общему ресурсу. Предполагая, что counter - единственный критический раздел в вашем коде, вы можете сделать метод поточно-ориентированным, используя Interlocked.Increment API:
async Task FooAsync()
{
...
Interlocked.Increment(ref counter);
...
}
Это увеличит counter и сохранит новый результат как атомарную операцию.
Есть также множество других конструкций синхронизации. Какой из них использовать, в основном зависит от того, что вы делаете. Однако избегание вызова ConfigureAwait(false) - это не способ сделать код потокобезопасным.
Спасибо, @ mm8. Думаю, я мог быть не совсем ясен. Если в нашей команде мы решили, что будем использовать однопоточность, то ConfigureAwait(false), похоже, нарушит эту роль. Что касается FooAsync(), он гарантированно будет вызываться из потока пользовательского интерфейса.
But since the continuation of ConfigureAwait(false) can run on any thread from thread pool, then how can you safely protect against multiple threads accessing the same state in your library?
await действительно вводит возможность повторного входа, но когда он действительно вызывает проблему, редко. Асинхронный код по своей природе поощряет более функциональный вид структуры (входные данные метода - это его параметры, а выходы - его возвращаемые значения). Это возможный, чтобы асинхронные методы имели побочные эффекты и зависели от состояния, но это не очень распространено.
Обратите внимание, что именно await вызывает случайный повторный вход. ConfigureAwait(false) возобновляет работу в пуле потоков, но это не вызывает проблемы.
If a UI thread keeps calling this FooAsync (e.g. because of user pressing button), wouldn't this code corrupt the value of counter and the file saved? Since multiple threads might be executing?
Да и вроде того. Да, счетчик может получить неожиданное значение, но это не обязательно из-за нескольких потоков. Рассмотрим тот же код без ConfigureAwait(false): у вас все еще есть несколько вызовов этой функции, только в одном потоке. Они все еще борются из-за прилавка и любого другого общего государства. В этом случае из-за единственного потока counter++ является атомарным, но поскольку он совместно используется, при однократном вызове этой функции значение может неожиданно измениться при возобновлении с await.
С ConfigureAwait(false) у вас делать есть дополнительная проблема, связанная с случайным параллелизм (с await у вас есть случайный возврат), поэтому, если у вас нет безопасного общего состояния, все может ухудшиться. Повторный вход может вызвать неожиданные состояния, но параллелизм может вызвать недопустимые состояния.
Вы не можете. Не знаю, что еще сказать. Ваш код не является потокобезопасным даже без использования ConfigureAwait для этого контекста, потому что у вас нет гарантии, что вызывающий объект находится в потоке пользовательского интерфейса в первую очередь.