Quando escrevo código assíncrono com async / waitit, geralmente com ConfigureAwait(false)
para evitar capturar o contexto, meu código está saltando de um segmento de pool de threads para o próximo após cada um await
. Isso levanta preocupações sobre a segurança do encadeamento. Esse código é seguro?
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");
}
A variável não i
está protegida e é acessada por vários threads * do conjunto de encadeamentos. Embora o padrão de acesso seja não simultâneo, teoricamente deve ser possível para cada encadeamento incrementar um valor armazenado em cache localmente i
, resultando em mais de 1.000.000 de iterações. Eu sou incapaz de produzir esse cenário na prática. O código acima sempre imprime OK na minha máquina. Isso significa que o código é seguro para threads? Ou devo sincronizar o acesso à i
variável usando um lock
?
(* uma opção de thread ocorre a cada 2 iterações, em média, de acordo com meus testes)
fonte
i
é armazenado em cache em cada thread? Veja este SharpLab IL para aprofundar.Respostas:
O problema com a segurança de threads é sobre a leitura / gravação de memória. Mesmo quando isso pode continuar em um thread diferente, nada aqui é executado simultaneamente.
fonte
i
não é declaradavolatile
nem protegida por um bloqueio, portanto, pelo que entendi, o compilador, o Jitter e o hardware (CPU) têm permissão para fazer uma otimização como esta.i
a qualquer momento.private int <i>5__2
na máquina de estado não é declaradovolatile
. Minhas preocupações não são sobre um thread que interrompe outro thread que está no meio da atualizaçãoi
. Isso é impossível de acontecer neste caso. Minhas preocupações são sobre um encadeamento usando um valor antigo dei
, armazenado em cache no cache local do núcleo da CPU, deixado lá de um loop anterior, em vez de buscar um novo valori
na RAM principal. Acessar o cache local é mais barato do que acessar a RAM principal, portanto, com otimizações ON, essas coisas são possíveis (de acordo com o que eu li).async
código lá?i
. Código bateawait
, o Thread A passa todo o estado para o Thread B e volta para o pool. Passe incrementos Bi
. Acessosawait
. O Thread B passa todo o estado para o Thread C, volta ao pool, etc. Em nenhum momento há acesso simultâneoi
, não há necessidade de segurança do thread, não importa que tenha ocorrido uma troca de thread, todos os o estado necessário é passado para o novo encadeamento executando a continuação. Não há estado compartilhado, por isso você não precisa de sincronização.Acredito que este artigo de Stephen Toub possa esclarecer isso. Em particular, esta é uma passagem relevante sobre o que acontece durante uma troca de contexto:
Vale ressaltar que os
YieldAwaitable
retornadosTask.Yield()
sempre retornamfalse
.fonte
ExecutionContext
do thread de thread para thread também servisse como um mecanismo para invalidar os caches locais do thread. Mas também não é impossível.async
ouawait
. A invalidação do cache durante uma alternância de contexto afetaria o código único e multiencadeado da mesma maneira.