Como o suporte a assinaturas C # 5 ajuda a solucionar problemas de sincronização de threads da interface do usuário?

16

Ouvi em algum lugar que o C # 5 async-waitit será tão incrível que você não precisará se preocupar em fazer isso:

if (InvokeRequired)
{
    BeginInvoke(...);
    return;
}
// do your stuff here

Parece que o retorno de chamada de uma operação de espera ocorrerá no encadeamento original do chamador. Eric Lippert e Anders Hejlsberg afirmaram várias vezes que esse recurso se originou da necessidade de tornar as interfaces de usuário (principalmente as de dispositivo de toque) mais responsivas.

Eu acho que um uso comum desse recurso seria algo como isto:

public class Form1 : Form
{
    // ...
    async void GetFurtherInfo()
    {
        var temperature = await GetCurrentTemperatureAsync();
        label1.Text = temperature;
    }
}

Se apenas um retorno de chamada for usado, a configuração do texto do rótulo gerará uma exceção, pois não está sendo executado no encadeamento da interface do usuário.

Até agora, não consegui encontrar nenhum recurso confirmando que esse seja o caso. Alguém sabe sobre isso? Existem documentos explicando tecnicamente como isso funcionará?

Por favor, forneça um link de uma fonte confiável, não basta responder "sim".

Alex
fonte
Isso parece altamente improvável, pelo menos no que diz respeito à awaitfuncionalidade. É apenas muito açúcar sintático para a passagem contínua . Possivelmente, existem outras melhorias não relacionadas ao WinForms que devem ajudar? Isso seria abrangido pelo próprio framework .NET, e não pelo C # especificamente.
Aaronaught 16/10
@Aaronaught Eu concordo, é por isso que estou fazendo a pergunta com precisão. Editei a pergunta para deixar claro de onde estou vindo. Parece estranho que eles criem esse recurso e ainda exijam o uso do infame estilo de código InvokeRequired.
18711 Alex

Respostas:

17

Acho que você está confundindo algumas coisas aqui. O que você está pedindo é possível usando System.Threading.Tasks, o asynce awaitem C # 5 está indo só para proporcionar um pouco de açúcar sintático mais agradável para o mesmo recurso.

Vamos usar um exemplo do Winforms - solte um botão e uma caixa de texto no formulário e use este código:

private void button1_Click(object sender, EventArgs e)
{
    Task.Factory.StartNew<int>(() => DelayedAdd(5, 10))
        .ContinueWith(t => DelayedAdd(t.Result, 20))
        .ContinueWith(t => DelayedAdd(t.Result, 30))
        .ContinueWith(t => DelayedAdd(t.Result, 50))
        .ContinueWith(t => textBox1.Text = t.Result.ToString(),
            TaskScheduler.FromCurrentSynchronizationContext());
}

private int DelayedAdd(int a, int b)
{
    Thread.Sleep(500);
    return a + b;
}

Execute-o e você verá que (a) ele não bloqueia o encadeamento da interface do usuário e (b) você não recebe o erro "operação de encadeamento inválida" usual - a menos que você remova o TaskSchedulerargumento do último ContinueWith, em Nesse caso, você irá.

Este é o padrão do pântano estilo de passagem de continuação . A mágica acontece na TaskSchedulerclasse e, especificamente, na instância recuperada por FromCurrentSynchronizationContext. Passe isso para qualquer continuação e você diz que a continuação deve ser executada em qualquer thread chamado FromCurrentSynchronizationContextmétodo - nesse caso, o thread da interface do usuário.

Os garçons são um pouco mais sofisticados, no sentido de saberem em qual encadeamento iniciaram e em qual encadeamento a continuação precisa ocorrer. Portanto, o código acima pode ser escrito um pouco mais naturalmente:

private async void button1_Click(object sender, EventArgs e)
{
    int a = await DelayedAddAsync(5, 10);
    int b = await DelayedAddAsync(a, 20);
    int c = await DelayedAddAsync(b, 30);
    int d = await DelayedAddAsync(c, 50);
    textBox1.Text = d.ToString();
}

private async Task<int> DelayedAddAsync(int a, int b)
{
    Thread.Sleep(500);
    return a + b;
}

Esses dois devem parecer muito semelhantes e, de fato, são muito semelhantes. oDelayedAddAsync método agora retorna um em Task<int>vez de um inte, portanto, awaitestá apenas colocando continuações em cada uma delas. A principal diferença é que ela está transmitindo o contexto de sincronização em cada linha, para que você não precise fazer isso explicitamente, como fizemos no último exemplo.

Em teoria, as diferenças são muito mais significativas. No segundo exemplo, todas as linhas do button1_Clickmétodo são realmente executadas no thread da interface do usuário, mas a própria tarefa ( DelayedAddAsync) é executada em segundo plano. No primeiro exemplo, tudo é executado em segundo plano , exceto a atribuição à textBox1.Textqual anexamos explicitamente o contexto de sincronização do thread da interface do usuário.

É disso que realmente interessa await- o fato de um garçom ser capaz de entrar e sair do mesmo método sem nenhuma chamada de bloqueio. Você liga await, o encadeamento atual volta ao processamento de mensagens e, quando terminar, o garçom retoma exatamente onde parou, no mesmo encadeamento em que parou. Mas, em termos de seu Invoke/ BeginInvokecontraste na pergunta, eu ' Lamento dizer que você deveria ter parado de fazer isso há muito tempo.

Aaronaught
fonte
Isso é muito interessante @Aaronaught. Eu estava ciente do estilo de passagem de continuação, mas não estava ciente de toda essa coisa do "contexto de sincronização". Existe algum documento vinculando esse contexto de sincronização ao C # 5 async-waitit? Entendo que é um recurso existente, mas o fato de usá-lo por padrão parece muito importante, especialmente porque deve ter alguns impactos importantes no desempenho, certo? Mais algum comentário sobre isso? Obrigado pela sua resposta, a propósito.
Alex
1
@Alex: Para obter respostas para todas essas perguntas de acompanhamento, sugiro que você leia Desempenho assíncrono: Compreendendo os custos do Async e do Aguardar . A seção "Importar-se com o contexto" explica como tudo se relaciona com o contexto de sincronização.
Aaronaught 16/10
(By the way, contextos de sincronização não são novas; eles estiveram no quadro desde 2.0 O TPL acaba de fazer-lhes muito mais fácil de usar..)
Aaronaught
2
Pergunto-me por que ainda há muitas discussões sobre o uso do estilo InvokeRequired e a maioria dos threads que eu vi nem menciona contextos de sincronização. Teria me poupado tempo para colocar essa questão ... #
1616 Alex
2
@ Alex: Eu acho que você não estava procurando nos lugares certos . Não sei o que te dizer; existem grandes partes da comunidade .NET que levam muito tempo para serem atualizadas. Inferno, ainda vejo alguns codificadores usando a ArrayListclasse no novo código. Eu ainda quase não tenho nenhuma experiência com o RX. As pessoas aprendem o que precisam saber e compartilham o que já sabem, mesmo que o que já sabem esteja desatualizado. Essa resposta pode estar desatualizada em alguns anos.
Aaronaught 16/10
4

Sim, para o encadeamento da interface do usuário, o retorno de chamada de uma operação de espera ocorrerá no encadeamento original do chamador.

Eric Lippert escreveu uma série de 8 partes sobre o assunto há um ano: Fabulous Adventures In Coding

EDIT: e aqui está o // build / apresentação de Anders : channel9

BTW, você notou que, se você virar "// build /" de cabeça para baixo, terá "/ plinq //" ;-)

Nicholas Butler
fonte