Чи можна чекати події замість іншого методу асинхронізації?


156

У моєму додатку метро C # / XAML є кнопка, яка починає тривалий процес. Тому, як рекомендується, я використовую async / очікую, щоб переконатися, що нитка інтерфейсу не блокується:

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;

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> + чекай .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це як 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 Я вдосконалив версію і провів короткий тест.
Фелікс Кейл

Я можу побачити додавання ідентифікатора, залежно від обставин. Крім того, щоб уникнути необхідності вводити подію двічі, ми також могли використовувати 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це метод, який продовжує отримувати результати в разі натискання кнопки. Якщо ваша кнопка не натиснута, ваш обробник подій нічого не робить.


Яка нитка асинхронізації? Немає коду, який не запускатиметься в потоці інтерфейсу користувача, як в оригінальному запитанні, так і у вашій відповіді.
svick

@svick Неправда. GetResultsповертає a 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;
}

1
Ви повністю винайшли новий механізм обробці подій. Можливо, саме таким чином перекладаються делегати в .NET, але не можна очікувати, що люди це приймуть. Наявність типу повернення для самого делегата (події) може привести людей до початку. Але добрі зусилля, дуже подобається, як добре це робиться.
nawfal

@nawfal Дякую! Я змінив його з тих пір, щоб уникнути повернення делегата. Джерело доступне тут як частина веб-двигуна Lara, альтернативи Blazor.
cat_in_hat
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.