186

私のC#/ XAMLメトロアプリには、長時間実行されるプロセスを開始するボタンがあります。したがって、推奨されているように、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で発生する処理はより複雑であり、実行内のさまざまなポイントでさまざまなタイプのユーザー入力が必要になる可能性があります。したがって、ロジックを複数のメソッドに分割することは簡単ではありません。

4

10 に答える 10

273

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;
于 2012-10-12T12:01:50.103 に答える
86

オンにする必要がある珍しいものがある場合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)。

于 2012-10-12T14:59:47.707 に答える
3

Stephen Toub は、この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; 
        } 
    } 
}
于 2016-02-04T21:26:25.710 に答える
0

CancellationToken をサポートする、テストに使用したクラスを次に示します。

この Test メソッドは、ClassWithEventMyEventのインスタンスが発生するのを待っていることを示しています。:

    public async Task TestEventAwaiter()
    {
        var cls = new ClassWithEvent();

        Task<bool> isRaisedTask = EventAwaiter<ClassWithEvent>.RunAsync(
            cls, 
            nameof(ClassWithEvent.MyMethodEvent), 
            TimeSpan.FromSeconds(3));

        cls.Raise();
        Assert.IsTrue(await isRaisedTask);
        isRaisedTask = EventAwaiter<ClassWithEvent>.RunAsync(
            cls, 
            nameof(ClassWithEvent.MyMethodEvent), 
            TimeSpan.FromSeconds(1));

        System.Threading.Thread.Sleep(2000);

        Assert.IsFalse(await isRaisedTask);
    }

これがイベント待機クラスです。

public class EventAwaiter<TOwner>
{
    private readonly TOwner_owner;
    private readonly string _eventName;
    private readonly TaskCompletionSource<bool> _taskCompletionSource;
    private readonly CancellationTokenSource _elapsedCancellationTokenSource;
    private readonly CancellationTokenSource _linkedCancellationTokenSource;
    private readonly CancellationToken _activeCancellationToken;
    private Delegate _localHookDelegate;
    private EventInfo _eventInfo;

    public static Task<bool> RunAsync(
        TOwner owner,
        string eventName,
        TimeSpan timeout,
        CancellationToken? cancellationToken = null)
    {
        return (new EventAwaiter<TOwner>(owner, eventName, timeout, cancellationToken)).RunAsync(timeout);
    }
    private EventAwaiter(
        TOwner owner,
        string eventName,
        TimeSpan timeout,
        CancellationToken? cancellationToken = null)
    {
        if (owner == null) throw new TypeInitializationException(this.GetType().FullName, new ArgumentNullException(nameof(owner)));
        if (eventName == null) throw new TypeInitializationException(this.GetType().FullName, new ArgumentNullException(nameof(eventName)));

        _owner = owner;
        _eventName = eventName;
        _taskCompletionSource = new TaskCompletionSource<bool>();
        _elapsedCancellationTokenSource = new CancellationTokenSource();
        _linkedCancellationTokenSource =
            cancellationToken == null
                ? null
                : CancellationTokenSource.CreateLinkedTokenSource(_elapsedCancellationTokenSource.Token, cancellationToken.Value);
        _activeCancellationToken = (_linkedCancellationTokenSource ?? _elapsedCancellationTokenSource).Token;

        _eventInfo = typeof(TOwner).GetEvent(_eventName);
        Type eventHandlerType = _eventInfo.EventHandlerType;
        MethodInfo invokeMethodInfo = eventHandlerType.GetMethod("Invoke");
        var parameterTypes = Enumerable.Repeat(this.GetType(),1).Concat(invokeMethodInfo.GetParameters().Select(p => p.ParameterType)).ToArray();
        DynamicMethod eventRedirectorMethod = new DynamicMethod("EventRedirect", typeof(void), parameterTypes);
        ILGenerator generator = eventRedirectorMethod.GetILGenerator();
        generator.Emit(OpCodes.Nop);
        generator.Emit(OpCodes.Ldarg_0);
        generator.EmitCall(OpCodes.Call, this.GetType().GetMethod(nameof(OnEventRaised),BindingFlags.Public | BindingFlags.Instance), null);
        generator.Emit(OpCodes.Ret);
        _localHookDelegate = eventRedirectorMethod.CreateDelegate(eventHandlerType,this);
    }
    private void AddHandler()
    {
        _eventInfo.AddEventHandler(_owner, _localHookDelegate);
    }
    private void RemoveHandler()
    {
        _eventInfo.RemoveEventHandler(_owner, _localHookDelegate);
    }
    private Task<bool> RunAsync(TimeSpan timeout)
    {
        AddHandler();
        Task.Delay(timeout, _activeCancellationToken).
            ContinueWith(TimeOutTaskCompleted);

        return _taskCompletionSource.Task;
    }

    private void TimeOutTaskCompleted(Task tsk)
    {
        RemoveHandler();
        if (_elapsedCancellationTokenSource.IsCancellationRequested) return;

        if (_linkedCancellationTokenSource?.IsCancellationRequested == true)
            SetResult(TaskResult.Cancelled);
        else if (!_taskCompletionSource.Task.IsCompleted)
            SetResult(TaskResult.Failed);

    }

    public void OnEventRaised()
    {
        RemoveHandler();
        if (_taskCompletionSource.Task.IsCompleted)
        {
            if (!_elapsedCancellationTokenSource.IsCancellationRequested)
                _elapsedCancellationTokenSource?.Cancel(false);
        }
        else
        {
            if (!_elapsedCancellationTokenSource.IsCancellationRequested)
                _elapsedCancellationTokenSource?.Cancel(false);
            SetResult(TaskResult.Success);
        }
    }
    enum TaskResult { Failed, Success, Cancelled }
    private void SetResult(TaskResult result)
    {
        if (result == TaskResult.Success)
            _taskCompletionSource.SetResult(true);
        else if (result == TaskResult.Failed)
            _taskCompletionSource.SetResult(false);
        else if (result == TaskResult.Cancelled)
            _taskCompletionSource.SetCanceled();
        Dispose();

    }
    public void Dispose()
    {
        RemoveHandler();
        _elapsedCancellationTokenSource?.Dispose();
        _linkedCancellationTokenSource?.Dispose();
    }
}

基本的にCancellationTokenSourceに依存して結果を報告します。IL インジェクションを使用して、イベントの署名に一致するデリゲートを作成します。そのデリゲートは、何らかのリフレクションを使用して、そのイベントのハンドラーとして追加されます。generate メソッドの本体は、 EventAwaiter クラスの別の関数を呼び出すだけで、CancellationTokenSourceを使用して成功を報告します。

そのまま製品に使用しないでください。これは実用的な例です。

たとえば、IL の生成はコストのかかるプロセスです。同じメソッドを何度も再生成することは避け、代わりにこれらをキャッシュする必要があります。

于 2021-08-04T22:34:19.123 に答える
0

イベントをタスクに変換するために使用できる 6 つのメソッドの小さなツールボックスを次に示します。

/// <summary>Converts a .NET event, conforming to the standard .NET event pattern
/// based on <see cref="EventHandler"/>, to a Task.</summary>
public static Task EventToAsync(
    Action<EventHandler> addHandler,
    Action<EventHandler> removeHandler)
{
    var tcs = new TaskCompletionSource<object>();
    addHandler(Handler);
    return tcs.Task;

    void Handler(object sender, EventArgs e)
    {
        removeHandler(Handler);
        tcs.SetResult(null);
    }
}

/// <summary>Converts a .NET event, conforming to the standard .NET event pattern
/// based on <see cref="EventHandler{TEventArgs}"/>, to a Task.</summary>
public static Task<TEventArgs> EventToAsync<TEventArgs>(
    Action<EventHandler<TEventArgs>> addHandler,
    Action<EventHandler<TEventArgs>> removeHandler)
{
    var tcs = new TaskCompletionSource<TEventArgs>();
    addHandler(Handler);
    return tcs.Task;

    void Handler(object sender, TEventArgs e)
    {
        removeHandler(Handler);
        tcs.SetResult(e);
    }
}

/// <summary>Converts a .NET event, conforming to the standard .NET event pattern
/// based on a supplied event delegate type, to a Task.</summary>
public static Task<TEventArgs> EventToAsync<TDelegate, TEventArgs>(
    Action<TDelegate> addHandler, Action<TDelegate> removeHandler)
{
    var tcs = new TaskCompletionSource<TEventArgs>();
    TDelegate handler = default;
    Action<object, TEventArgs> genericHandler = (sender, e) =>
    {
        removeHandler(handler);
        tcs.SetResult(e);
    };
    handler = (TDelegate)(object)genericHandler.GetType().GetMethod("Invoke")
        .CreateDelegate(typeof(TDelegate), genericHandler);
    addHandler(handler);
    return tcs.Task;
}

/// <summary>Converts a named .NET event, conforming to the standard .NET event
/// pattern based on <see cref="EventHandler"/>, to a Task.</summary>
public static Task EventToAsync(object target, string eventName)
{
    var type = target.GetType();
    var eventInfo = type.GetEvent(eventName);
    if (eventInfo == null) throw new InvalidOperationException("Event not found.");
    var tcs = new TaskCompletionSource<object>();
    EventHandler handler = default;
    handler = new EventHandler((sender, e) =>
    {
        eventInfo.RemoveEventHandler(target, handler);
        tcs.SetResult(null);
    });
    eventInfo.AddEventHandler(target, handler);
    return tcs.Task;
}

/// <summary>Converts a named .NET event, conforming to the standard .NET event
/// pattern based on <see cref="EventHandler{TEventArgs}"/>, to a Task.</summary>
public static Task<TEventArgs> EventToAsync<TEventArgs>(
    object target, string eventName)
{
    var type = target.GetType();
    var eventInfo = type.GetEvent(eventName);
    if (eventInfo == null) throw new InvalidOperationException("Event not found.");
    var tcs = new TaskCompletionSource<TEventArgs>();
    EventHandler<TEventArgs> handler = default;
    handler = new EventHandler<TEventArgs>((sender, e) =>
    {
        eventInfo.RemoveEventHandler(target, handler);
        tcs.SetResult(e);
    });
    eventInfo.AddEventHandler(target, handler);
    return tcs.Task;
}

/// <summary>Converts a generic Action-based .NET event to a Task.</summary>
public static Task<TArgument> EventActionToAsync<TArgument>(
    Action<Action<TArgument>> addHandler,
    Action<Action<TArgument>> removeHandler)
{
    var tcs = new TaskCompletionSource<TArgument>();
    addHandler(Handler);
    return tcs.Task;

    void Handler(TArgument arg)
    {
        removeHandler(Handler);
        tcs.SetResult(arg);
    }
}

これらのメソッドはすべてTask、関連付けられたイベントの次の呼び出しで完了する を作成しています。このタスクは失敗したりキャンセルされたりすることはなく、正常に完了するだけです。

標準イベント(Progress<T>.ProgressChanged)での使用例:

var p = new Progress<int>();

//...

int result = await EventToAsync<int>(
    h => p.ProgressChanged += h, h => p.ProgressChanged -= h);

// ...or...

int result = await EventToAsync<EventHandler<int>, int>(
    h => p.ProgressChanged += h, h => p.ProgressChanged -= h);

// ...or...

int result = await EventToAsync<int>(p, "ProgressChanged");

非標準イベントでの使用例:

public static event Action<int> MyEvent;

//...

int result = await EventActionToAsync<int>(h => MyEvent += h, h => MyEvent -= h);

タスクが完了すると、イベントは登録解除されます。それより前に購読を解除するためのメカニズムは提供されていません。

于 2020-12-16T14:18:32.147 に答える