簡體   English   中英

是否可以等待一個事件而不是另一個異步方法?

[英]Is it possible to await an event instead of another async method?

在我的 C#/XAML Metro 應用程序中,有一個按鈕可以啟動一個長時間運行的進程。 因此,按照建議,我使用 async/await 來確保 UI 線程不會被阻塞:

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 中發生的事情更復雜,執行過程中的不同點可能需要不同類型的用戶輸入。 因此,將邏輯分解為多種方法並非易事。

您可以使用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;

當您有不尋常的事情需要await時,最簡單的答案通常是TaskCompletionSource (或一些基於TaskCompletionSource啟用async原語)。

在這種情況下,您的需求非常簡單,因此您可以直接使用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 ) .

這是我使用的一個實用程序類:

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;

簡單的助手類:

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;

理想情況下,您不需要 雖然您當然可以阻塞異步線程,但這是一種資源浪費,而且並不理想。

考慮一個典型的例子,當按鈕等待被點擊時,用戶去吃午飯。

如果您在等待用戶輸入時暫停了異步代碼,那么在該線程暫停時它只是在浪費資源。

也就是說,最好在異步操作中,將需要維護的狀態設置為啟用按鈕並“等待”單擊的點。 此時,您的GetResults方法將停止

然后,單擊該按鈕時,根據您所儲存的狀態,啟動另一個異步任務,繼續工作。

因為SynchronizationContext將在調用GetResults的事件處理程序中捕獲(編譯器將這樣做,因為使用了await關鍵字,並且SynchronizationContext.Current應該是非空的,因為您在 UI 應用程序中),您可以像這樣使用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是在您的按鈕被按下時繼續獲取結果的方法。 如果您的按鈕沒有被按下,那么您的事件處理程序什么都不做。

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

使用反應式擴展 (Rx.Net)

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

var res = await eventObservable.FirstAsync();

您可以使用 Nuget 包 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");
    }

我正在使用我自己的 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;
}

這是一個包含六種方法的小工具箱,可用於將事件轉換為任務:

/// <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 ,該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);

任務完成后取消訂閱該事件。 沒有提供更早的退訂機制。

這是我用於測試的一個類,它支持 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 生成是一個昂貴的過程。 您應該避免一遍又一遍地重新生成相同的方法,而是緩存它們。

AsyncEx 為此具有AsyncManualResetEvent 你可以:

var signal = new AsyncManualResetEvent();
await signal.WaitAsync();

並通過以下方式觸發它:

signal.Set();

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM