別の非同期メソッドの代わりにイベントを待つことは可能ですか?


156

私のC#/ XAML Metroアプリには、長時間実行プロセスを開始するボタンがあります。したがって、推奨されるように、UIスレッドがブロックされないようにするために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;

ポーリングは明らかにひどい(ビジー待機/サイクルの無駄)ので、イベントベースの何かを探しています。

何か案は?

ところで、この単純化された例では、1つの解決策はもちろんGetResults()を2つの部分に分割し、最初の部分を開始ボタンから呼び出し、2番目の部分を続行ボタンから呼び出すことです。実際には、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>クラスのインスタンスを使用して、ボタンクリックの結果を表すTask <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()ブロックすると、実際にはUIスレッドがブロックされます。
svick

2
@Gabe await自体は、別のスレッドが作成されることを保証しませんが、それによって、ステートメントののすべてがTask呼び出しawaitたawaitableまたはawaitableの継続として実行されます。多くの場合、それはいくつかの IO完了、または何かすることができた、非同期操作の一種である別のスレッドで。
casperOne 2012年

16
+1。私はこれを調べなければならなかったので、他の人が興味を持っている場合に備えて、をスレッドプールスレッドにSemaphoreSlim.WaitAsyncプッシュするだけではありませんWaitSemaphoreSlimTask実装に使用されるの適切なキューがありますWaitAsync
Stephen Cleary

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は、に似async ManualResetEventていますが、イベントを「設定」できるのは1回のみで、イベントに「結果」を設定できる点が異なります(この場合、イベントを使用しないため、結果をに設定しますnull)。


5
私は「イベントを待つ」を「タスクにEAPをラップする」と基本的に同じ状況として解析するので、私は間違いなくこのアプローチを好みます。私見、それは間違いなくよりシンプルで/理由がわかりやすいコードです。
James Manning、

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か?
Denis P

@DenisPはおそらくイベントをEventAwaiterのコンストラクターに渡しますか?
CJBrew 2018年

@DenisPバージョンを改善し、短いテストを実行しました。
Felix Keil、

状況によっては、IDisposableを追加することもできます。また、イベントを2回入力する手間を省くために、Reflectionを使用してイベント名を渡すこともできるため、使用方法はさらに簡単になります。それ以外の場合は柄が好きです、ありがとうございます。
Denis P

4

理想的にはしないでください。非同期スレッドをブロックすることはできますが、これはリソースの浪費であり、理想的ではありません。

ボタンがクリックされるのを待っている間にユーザーが昼食に行くという標準的な例を考えてみましょう。

ユーザーからの入力を待機している間に非同期コードを停止した場合、そのスレッドが一時停止している間にリソースを浪費しているだけです。

つまり、非同期操作で、維持する必要のある状態を、ボタンが有効になり、クリックを「待機」している状態に設定することをお勧めします。その時点で、GetResultsメソッドは停止します。

次に、ボタンクリックすると、保存した状態に基づいて、別の非同期タスクを開始して作業を続行します。

SynchronizationContextが呼び出されるイベントハンドラーでキャプチャされるためGetResultsawait使用されているキーワードを使用した結果、UIアプリケーションでSynchronizationContext.Currentがnull以外であることをコンパイラーが行うため)、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ボタンが押された場合に結果を取得し続けるメソッドです。ボタンが押されていない場合、イベントハンドラは何もしません。


どのような非同期スレッドですか?元の質問と回答の両方に、UIスレッドで実行されないコードはありません
スビック

@svick真実ではない。 GetResultsを返しますTaskawait単に「タスクを実行し、タスクが完了したら、この後にコードを続行する」と言います。同期コンテキストがある場合、呼び出しはでキャプチャされるため、UIスレッドにマーシャリングされますawaitawaitと同じではなくTask.Wait()少なくともです。
casperOne 2012年

については何も言わなかったWait()。ただし、ここでのコードGetResults()はUIスレッドで実行され、他のスレッドはありません。言い換えれば、はい、await基本的にはあなたが言うようにタスクを実行しますが、ここではそのタスクもUIスレッドで実行されます。
14:01にスビック

@svickタスクがUIスレッドで実行されると仮定する理由はありません。なぜその仮定をするのですか?それはだ可能性が、可能性は低いです。そして、呼び出しは技術的には2つの個別のUI呼び出しであり、1つはまでawait、その後はのコードawaitであり、ブロッキングはありません。残りのコードは、継続的にマーシャリングされ、を通じてスケジュールされますSynchronizationContext
casperOne 2012年

1
もっと見たい他の人については、こちらをご覧ください:chat.stackoverflow.com/rooms/17937 - @svickと私は基本的にお互いを誤解しますが、同じことを言っていました。
casperOne 2012年

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

Nuget Package System.ReactiveでRxを追加できます

テストされたサンプル:

    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ありがとう!デリゲートを返さないように修正しました。ソースは、Blazorに代わるLara Web Engineの一部としてここで入手できます
cat_in_hat
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.