É possível aguardar um evento em vez de outro método assíncrono?


156

No meu aplicativo de metrô C # / XAML, há um botão que inicia um processo de longa duração. Portanto, como recomendado, estou usando async / waitit para garantir que o thread da interface do usuário não seja bloqueado:

private async void Button_Click_1(object sender, RoutedEventArgs e) 
{
     await GetResults();
}

private async Task GetResults()
{ 
     // Do lot of complex stuff that takes a long time
     // (e.g. contact some web services)
  ...
}

Ocasionalmente, as coisas que acontecem no GetResults exigem entrada adicional do usuário antes que ele possa continuar. Para simplificar, digamos que o usuário apenas tenha que clicar no botão "continuar".

Minha pergunta é: como posso suspender a execução de GetResults de forma que aguarde um evento como o clique de outro botão?

Aqui está uma maneira feia de conseguir o que estou procurando: o manipulador de eventos para o botão continuar "define um sinalizador ...

private bool _continue = false;
private void buttonContinue_Click(object sender, RoutedEventArgs e)
{
    _continue = true;
}

... e o GetResults realiza pesquisas periodicamente:

 buttonContinue.Visibility = Visibility.Visible;
 while (!_continue) await Task.Delay(100);  // poll _continue every 100ms
 buttonContinue.Visibility = Visibility.Collapsed;

A pesquisa é claramente terrível (espera ocupada / desperdício de ciclos) e estou procurando algo baseado em eventos.

Alguma ideia?

Entre neste exemplo simplificado, uma solução seria, obviamente, dividir GetResults () em duas partes, invocar a primeira parte do botão Iniciar e a segunda parte do botão Continuar. Na realidade, as coisas que acontecem no GetResults são mais complexas e diferentes tipos de entrada do usuário podem ser necessários em diferentes pontos da execução. Portanto, dividir a lógica em vários métodos não seria trivial.

Respostas:


225

Você pode usar uma instância da classe SemaphoreSlim como um sinal:

private SemaphoreSlim signal = new SemaphoreSlim(0, 1);

// set signal in event
signal.Release();

// wait for signal somewhere else
await signal.WaitAsync();

Como alternativa, você pode usar uma instância da TaskCompletionSource <T> Class para criar uma tarefa <T> que represente o resultado do clique no botão:

private TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();

// complete task in event
tcs.SetResult(true);

// wait for task somewhere else
await tcs.Task;

7
@DanielHilgarth ManualResetEvent(Slim)não parece apoiar WaitAsync().
svick

3
@DanielHilgarth Não, você não poderia. asyncnão significa "roda em um segmento diferente", ou algo assim. Significa apenas "você pode usar awaitneste método". E, nesse caso, o bloqueio interno GetResults()realmente bloqueará o thread da interface do usuário.
svick

2
O @Gabe awaitpor si só não garante a criação de outro encadeamento, mas faz com que todo o resto após a instrução seja executado como uma continuação Taskou aguardável que você chamar await. Na maioria das vezes, é algum tipo de operação assíncrona, que pode ser a conclusão de E / S ou algo que esteja em outro encadeamento.
casperOne

16
+1. Eu tive que procurar isso, para o caso de outras pessoas estarem interessadas: SemaphoreSlim.WaitAsyncnão basta inserir o Waitthread em um pool de threads. SemaphoreSlimpossui uma fila adequada de Tasks que são usadas para implementar WaitAsync.
Stephen Cleary

14
TaskCompletionSource <T> + aguarda .Task + .SetResult () acaba por ser a solução perfeita para o meu cenário - obrigado! :-)
Max

75

Quando você precisa fazer algo incomum await, a resposta mais fácil é frequentemente TaskCompletionSource(ou alguma asyncprimitiva ativada com base em TaskCompletionSource).

Nesse caso, sua necessidade é bastante simples, então você pode simplesmente usar TaskCompletionSourcediretamente:

private TaskCompletionSource<object> continueClicked;

private async void Button_Click_1(object sender, RoutedEventArgs e) 
{
  // Note: You probably want to disable this button while "in progress" so the
  //  user can't click it twice.
  await GetResults();
  // And re-enable the button here, possibly in a finally block.
}

private async Task GetResults()
{ 
  // Do lot of complex stuff that takes a long time
  // (e.g. contact some web services)

  // Wait for the user to click Continue.
  continueClicked = new TaskCompletionSource<object>();
  buttonContinue.Visibility = Visibility.Visible;
  await continueClicked.Task;
  buttonContinue.Visibility = Visibility.Collapsed;

  // More work...
}

private void buttonContinue_Click(object sender, RoutedEventArgs e)
{
  if (continueClicked != null)
    continueClicked.TrySetResult(null);
}

Logicamente, TaskCompletionSourceé como um async ManualResetEvent, exceto que você só pode "definir" o evento uma vez e o evento pode ter um "resultado" (nesse caso, não o estamos usando, apenas definimos o resultado como null).


5
Como eu analiso "aguardar um evento" como basicamente a mesma situação que 'agrupar o EAP em uma tarefa', eu definitivamente preferiria essa abordagem. IMHO, é definitivamente um código mais simples / mais fácil de raciocinar.
James Manning

8

Aqui está uma classe de utilitário que eu uso:

public class AsyncEventListener
{
    private readonly Func<bool> _predicate;

    public AsyncEventListener() : this(() => true)
    {

    }

    public AsyncEventListener(Func<bool> predicate)
    {
        _predicate = predicate;
        Successfully = new Task(() => { });
    }

    public void Listen(object sender, EventArgs eventArgs)
    {
        if (!Successfully.IsCompleted && _predicate.Invoke())
        {
            Successfully.RunSynchronously();
        }
    }

    public Task Successfully { get; }
}

E aqui está como eu o uso:

var itChanged = new AsyncEventListener();
someObject.PropertyChanged += itChanged.Listen;

// ... make it change ...

await itChanged.Successfully;
someObject.PropertyChanged -= itChanged.Listen;

1
Não sei como isso funciona. Como o método Listen está executando assincronamente meu manipulador personalizado? Não new Task(() => { });seria concluído instantaneamente?
Nawfal 6/12/19

5

Classe Auxiliar Simples:

public class EventAwaiter<TEventArgs>
{
    private readonly TaskCompletionSource<TEventArgs> _eventArrived = new TaskCompletionSource<TEventArgs>();

    private readonly Action<EventHandler<TEventArgs>> _unsubscribe;

    public EventAwaiter(Action<EventHandler<TEventArgs>> subscribe, Action<EventHandler<TEventArgs>> unsubscribe)
    {
        subscribe(Subscription);
        _unsubscribe = unsubscribe;
    }

    public Task<TEventArgs> Task => _eventArrived.Task;

    private EventHandler<TEventArgs> Subscription => (s, e) =>
        {
            _eventArrived.TrySetResult(e);
            _unsubscribe(Subscription);
        };
}

Uso:

var valueChangedEventAwaiter = new EventAwaiter<YourEventArgs>(
                            h => example.YourEvent += h,
                            h => example.YourEvent -= h);
await valueChangedEventAwaiter.Task;

1
Como você limparia a assinatura example.YourEvent?
Denis P #

@DenisP talvez passe o evento para o construtor EventAwaiter?
CJBrew

@DenisP Melhorei a versão e fiz um pequeno teste.
Felix Keil #:

Pude ver a adição de IDisposable também, dependendo das circunstâncias. Além disso, para evitar a digitação do evento duas vezes, também podemos usar o Reflection para passar o nome do evento, para que o uso seja ainda mais simples. Caso contrário, eu gosto do padrão, obrigado.
Denis P

4

Idealmente, você não . Embora você certamente possa bloquear o encadeamento assíncrono, isso é um desperdício de recursos, e não é o ideal.

Considere o exemplo canônico em que o usuário vai almoçar enquanto o botão está aguardando para ser clicado.

Se você interrompeu seu código assíncrono enquanto aguarda a entrada do usuário, estará desperdiçando recursos enquanto o encadeamento estiver em pausa.

Dito isso, é melhor que, em sua operação assíncrona, você defina o estado que precisa manter no ponto em que o botão está ativado e você estará "esperando" com um clique. Nesse ponto, seu GetResultsmétodo para .

Em seguida, quando o botão é clicado, com base no estado que você armazenou, você inicia outra tarefa assíncrona para continuar o trabalho.

Como o SynchronizationContextserá capturado no manipulador de eventos que chama GetResults(o compilador fará isso como resultado do uso da awaitpalavra - chave usada e o fato de que SynchronizationContext.Current deve ser não nulo, desde que você esteja em um aplicativo de interface do usuário), você pode usar async/await assim:

private async void Button_Click_1(object sender, RoutedEventArgs e) 
{
     await GetResults();

     // Show dialog/UI element.  This code has been marshaled
     // back to the UI thread because the SynchronizationContext
     // was captured behind the scenes when
     // await was called on the previous line.
     ...

     // Check continue, if true, then continue with another async task.
     if (_continue) await ContinueToGetResultsAsync();
}

private bool _continue = false;
private void buttonContinue_Click(object sender, RoutedEventArgs e)
{
    _continue = true;
}

private async Task GetResults()
{ 
     // Do lot of complex stuff that takes a long time
     // (e.g. contact some web services)
  ...
}

ContinueToGetResultsAsyncé o método que continua a obter os resultados caso o botão seja pressionado. Se o botão não for pressionado, o manipulador de eventos não fará nada.


Qual thread assíncrono? Não há código que não seja executado no thread da interface do usuário, tanto na pergunta original quanto na sua resposta.
svick

@svick Não é verdade. GetResultsretorna a Task. awaitsimplesmente diz "execute a tarefa e, quando a tarefa estiver concluída, continue o código depois disso". Dado que existe um contexto de sincronização, a chamada é empacotada de volta para o thread da interface do usuário, conforme é capturada no await. nãoawait é o mesmo que , nem um pouco. Task.Wait()
casperOne

Eu não disse nada sobre Wait(). Mas o código GetResults()será executado no thread da interface do usuário aqui, não há outro thread. Em outras palavras, sim, awaitbasicamente executa a tarefa, como você diz, mas aqui, essa tarefa também é executada no thread da interface do usuário.
21712 #

@svick Não há razão para supor que a tarefa seja executada no thread da interface do usuário, por que você faz essa suposição? É possível , mas improvável. E a chamada são duas chamadas de interface do usuário separadas, tecnicamente, uma até o awaite depois o código depois await, não há bloqueio. O resto do código é empacotado volta na continuação e programado através do SynchronizationContext.
casperOne

1
Para outras pessoas que querem ver mais, consulte aqui: chat.stackoverflow.com/rooms/17937 - @svick e eu basicamente nos entendemos mal, mas estávamos dizendo a mesma coisa.
casperOne

3

Stephen Toub publicou esta AsyncManualResetEventaula em seu blog .

public class AsyncManualResetEvent 
{ 
    private volatile TaskCompletionSource<bool> m_tcs = new TaskCompletionSource<bool>();

    public Task WaitAsync() { return m_tcs.Task; } 

    public void Set() 
    { 
        var tcs = m_tcs; 
        Task.Factory.StartNew(s => ((TaskCompletionSource<bool>)s).TrySetResult(true), 
            tcs, CancellationToken.None, TaskCreationOptions.PreferFairness, TaskScheduler.Default); 
        tcs.Task.Wait(); 
    }

    public void Reset() 
    { 
        while (true) 
        { 
            var tcs = m_tcs; 
            if (!tcs.Task.IsCompleted || 
                Interlocked.CompareExchange(ref m_tcs, new TaskCompletionSource<bool>(), tcs) == tcs) 
                return; 
        } 
    } 
}

0

Com extensões reativas (Rx.Net)

var eventObservable = Observable
            .FromEventPattern<EventArgs>(
                h => example.YourEvent += h,
                h => example.YourEvent -= h);

var res = await eventObservable.FirstAsync();

Você pode adicionar o Rx com o Nuget Package System.Reactive

Amostra testada:

    private static event EventHandler<EventArgs> _testEvent;

    private static async Task Main()
    {
        var eventObservable = Observable
            .FromEventPattern<EventArgs>(
                h => _testEvent += h,
                h => _testEvent -= h);

        Task.Delay(5000).ContinueWith(_ => _testEvent?.Invoke(null, new EventArgs()));

        var res = await eventObservable.FirstAsync();

        Console.WriteLine("Event got fired");
    }

0

Estou usando minha própria classe AsyncEvent para eventos aguardáveis.

public delegate Task AsyncEventHandler<T>(object sender, T args) where T : EventArgs;

public class AsyncEvent : AsyncEvent<EventArgs>
{
    public AsyncEvent() : base()
    {
    }
}

public class AsyncEvent<T> where T : EventArgs
{
    private readonly HashSet<AsyncEventHandler<T>> _handlers;

    public AsyncEvent()
    {
        _handlers = new HashSet<AsyncEventHandler<T>>();
    }

    public void Add(AsyncEventHandler<T> handler)
    {
        _handlers.Add(handler);
    }

    public void Remove(AsyncEventHandler<T> handler)
    {
        _handlers.Remove(handler);
    }

    public async Task InvokeAsync(object sender, T args)
    {
        foreach (var handler in _handlers)
        {
            await handler(sender, args);
        }
    }

    public static AsyncEvent<T> operator+(AsyncEvent<T> left, AsyncEventHandler<T> right)
    {
        var result = left ?? new AsyncEvent<T>();
        result.Add(right);
        return result;
    }

    public static AsyncEvent<T> operator-(AsyncEvent<T> left, AsyncEventHandler<T> right)
    {
        left.Remove(right);
        return left;
    }
}

Para declarar um evento na classe que gera eventos:

public AsyncEvent MyNormalEvent;
public AsyncEvent<ProgressEventArgs> MyCustomEvent;

Para aumentar os eventos:

if (MyNormalEvent != null) await MyNormalEvent.InvokeAsync(this, new EventArgs());
if (MyCustomEvent != null) await MyCustomEvent.InvokeAsync(this, new ProgressEventArgs());

Para se inscrever nos eventos:

MyControl.Click += async (sender, args) => {
    // await...
}

MyControl.Click += (sender, args) => {
    // synchronous code
    return Task.CompletedTask;
}

1
Você inventou completamente um novo mecanismo de manipulador de eventos. Talvez seja para isso que os representantes do .NET sejam traduzidos, mas não espere que as pessoas adotem isso. Ter um tipo de retorno para o delegado (do evento) em si pode colocar as pessoas em primeiro lugar. Mas bom esforço, realmente gosto de como é feito.
Nawfal #

@nawfal Thanks! Eu o modifiquei para evitar o retorno de um delegado. A fonte está disponível aqui como parte do Lara Web Engine, uma alternativa ao Blazor.
cat_in_hat 5/12/19
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.