Можно ли ожидать событие вместо другого асинхронного метода?

156

В моем приложении C # / XAML metro есть кнопка, которая запускает длительный процесс. Итак, как рекомендовано, я использую async / await, чтобы убедиться, что поток пользовательского интерфейса не заблокирован:

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)
  ...
}

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

Мой вопрос: как я могу приостановить выполнение GetResults таким образом, чтобы он ожидал события, такого как нажатие другой кнопки?

Вот ужасный способ добиться того, что я ищу: обработчик события для кнопки "продолжить" устанавливает флаг ...

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

... и GetResults периодически опрашивает его:

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

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

Любые идеи?

Кстати, в этом упрощенном примере одним из решений, конечно, было бы разделение GetResults () на две части, вызов первой части с помощью кнопки «Пуск» и второй части с помощью кнопки «Продолжить». В действительности, вещи, происходящие в GetResults, являются более сложными, и в разных точках выполнения могут потребоваться разные типы пользовательского ввода. Так что разбить логику на несколько методов было бы нетривиально.

Макс
источник

Ответы:

225

Вы можете использовать экземпляр класса SemaphoreSlim в качестве сигнала:

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

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

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

Кроме того, вы можете использовать экземпляр класса TaskCompletionSource <T>, чтобы создать задачу <T>, которая представляет результат нажатия кнопки:

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

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

// wait for task somewhere else
await tcs.Task;
DTB
источник
7
@DanielHilgarth ManualResetEvent(Slim), похоже, не поддерживает WaitAsync().
svick
3
@DanielHilgarth Нет, ты не мог. asyncне означает «работает в другом потоке» или что-то в этом роде. Это просто означает «вы можете использовать awaitв этом методе». И в этом случае блокировка внутри GetResults()фактически заблокирует поток пользовательского интерфейса.
svick
2
@Gabe awaitсам по себе не гарантирует создания другого потока, но заставляет все остальное после оператора выполняться как продолжение Taskили ожидание, которое вы вызываете await. Чаще всего, это какой - то вид асинхронной операции, которые могут быть IO завершение, или что - то , что находится в другом потоке.
casperOne
16
+1. Я должен был это найти, так что на случай, если другие заинтересуются: SemaphoreSlim.WaitAsyncне просто вставить Waitпоток пула потоков. SemaphoreSlimимеет правильную очередь Tasks, которые используются для реализации WaitAsync.
Стивен Клири
14
TaskCompletionSource <T> + await .Task + .SetResult () оказывается идеальным решением для моего сценария - спасибо! :-)
Макс
75

Если у вас есть необычная вещь, на которую вам нужно awaitвключить, часто самый простой ответ TaskCompletionSource(или какой-нибудь asyncвключенный примитив на основе TaskCompletionSource).

В этом случае ваша потребность довольно проста, поэтому вы можете просто использовать TaskCompletionSourceнапрямую:

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);
}

Логически TaskCompletionSourceэто похоже на an async ManualResetEvent, за исключением того, что вы можете «установить» событие только один раз, и событие может иметь «результат» (в этом случае мы его не используем, поэтому мы просто устанавливаем результат null).

Стивен Клири
источник
5
Поскольку я анализирую «жду событие», как в основном ту же ситуацию, что и «завернуть EAP в задачу», я бы определенно предпочел такой подход. ИМХО, это определенно более простой / понятный код.
Джеймс Мэннинг
8

Вот полезный класс, который я использую:

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; }
}

И вот как я это использую:

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

// ... make it change ...

await itChanged.Successfully;
someObject.PropertyChanged -= itChanged.Listen;
Андерс Сковборг
источник
1
Я не знаю, как это работает. Как метод Listen асинхронно выполняет мой пользовательский обработчик? Не new Task(() => { });будет мгновенно завершено?
Nawfal
5

Простой хелпер класс:

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);
        };
}

Использование:

var valueChangedEventAwaiter = new EventAwaiter<YourEventArgs>(
                            h => example.YourEvent += h,
                            h => example.YourEvent -= h);
await valueChangedEventAwaiter.Task;
Феликс Кейл
источник
1
Как бы вы очистили подписку на example.YourEvent?
Денис П
@DenisP, возможно, передать событие в конструктор для EventAwaiter?
CJBrew
@DenisP Я улучшил версию и провел короткий тест.
Феликс Кейл
Я мог видеть добавление IDisposable также, в зависимости от обстоятельств. Кроме того, чтобы избежать необходимости вводить событие дважды, мы могли бы также использовать Reflection для передачи имени события, поэтому использование будет еще проще. В противном случае мне нравится шаблон, спасибо.
Денис П
4

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

Рассмотрим канонический пример, когда пользователь идет на обед, пока кнопка ожидает нажатия.

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

Тем не менее, лучше, если в вашей асинхронной операции вы устанавливаете состояние, которое вам нужно поддерживать, до точки, когда кнопка включена, и вы «ждете» щелчка. В этот момент ваш GetResultsметод останавливается .

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

Поскольку это SynchronizationContextбудет записано в вызывающем обработчике событий GetResults(компилятор сделает это в результате использования используемого awaitключевого слова и того факта, что SynchronizationContext.Current должен быть ненулевым, если вы находитесь в приложении пользовательского интерфейса), вы можно использовать async/await нравится так:

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это метод, который продолжает получать результаты в случае нажатия вашей кнопки. Если ваша кнопка не нажата, то ваш обработчик событий ничего не делает.

casperOne
источник
Какой асинхронный поток? Нет кода, который не будет выполняться в потоке пользовательского интерфейса, как в исходном вопросе, так и в вашем ответе.
svick
@svick Не правда. GetResultsвозвращает Task. awaitпросто говорит «запустите задачу, и когда задача будет выполнена, продолжайте код после этого». Учитывая наличие контекста синхронизации, вызов перенаправляется обратно в поток пользовательского интерфейса, поскольку он фиксируется в await. awaitэто не то же самое Task.Wait(), что ни в коем случае.
casperOne
Я ничего не говорил о Wait(). Но здесь GetResults()будет выполняться код в потоке пользовательского интерфейса, другого потока нет. Другими словами, да, в awaitпринципе, задача выполняется, как вы говорите, но здесь эта задача также выполняется в потоке пользовательского интерфейса.
svick
@svick Нет оснований предполагать, что задача выполняется в потоке пользовательского интерфейса. Почему вы делаете это предположение? Это возможно , но вряд ли. И вызов - это два отдельных вызова пользовательского интерфейса, технически, один до, awaitа затем код после await, блокировки нет. Остальная часть кода отправляется обратно в продолжение и планируется через SynchronizationContext.
casperOne
1
Для других, которые хотят видеть больше, см. Здесь: chat.stackoverflow.com/rooms/17937 - @svick и я в основном неправильно поняли друг друга, но говорили то же самое.
casperOne
3

Стивен Туб опубликовал этот AsyncManualResetEventкласс в своем блоге .

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

С реактивными расширениями (Rx.Net)

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

var res = await eventObservable.FirstAsync();

Вы можете добавить Rx с помощью Nuget Package System.Reactive

Протестированный образец:

    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

Я использую свой собственный класс AsyncEvent для ожидаемых событий.

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;
    }
}

Чтобы объявить событие в классе, который вызывает события:

public AsyncEvent MyNormalEvent;
public AsyncEvent<ProgressEventArgs> MyCustomEvent;

Чтобы поднять события:

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

Чтобы подписаться на события:

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

MyControl.Click += (sender, args) => {
    // synchronous code
    return Task.CompletedTask;
}
cat_in_hat
источник
1
Вы полностью изобрели новый механизм обработчика событий. Возможно, это то, к чему в конечном итоге переводятся делегаты .NET, но не стоит ожидать, что люди это примут. Наличие типа возврата для самого делегата (события) может с самого начала оттолкнуть людей. Но хорошие усилия, очень нравится, как хорошо это сделано.
Nawfal
@nawfal Спасибо! С тех пор я изменил его, чтобы избежать возврата делегата. Источник доступен здесь как часть Lara Web Engine, альтернативы Blazor.
cat_in_hat