【问题标题】:await and event handler等待和事件处理程序
【发布时间】:2021-08-27 10:26:12
【问题描述】:

是否允许将通常的事件处理程序从 void 转换为基于 Task 并像下面这样等待它?

Something.PropertyChanged += async (o, args) => await IsButtonVisible_PropertyChanged(o, args);  
Something.PropertyChanged -= async (o, args) => await IsButtonVisible_PropertyChanged(o, args);  

private Task IsButtonVisible_PropertyChanged(object sender,PropertyChangedEventArgs e)
{
   if (IsSomthingEnabled)
   {
       return SomeService.ExecuteAsync(...);
   }

   return Task.CompletedTask;
}

还是这样?

Something.PropertyChanged += IsButtonVisible_PropertyChanged;  
Something.PropertyChanged -= IsButtonVisible_PropertyChanged;  

private void IsButtonVisible_PropertyChanged(object sender,PropertyChangedEventArgs e)
{
   if (IsSomthingEnabled)
   {
       _ = SomeService.ExecuteAsync(...);
   }
}

更新: 或者这个,我知道应该禁止使用 Task void ,因为异常它没有被捕获,但也许对于 Eventhandler 的情况是可以的,因为 Eventhandler 没有返回。

Something.PropertyChanged += IsButtonVisible_PropertyChanged;  
Something.PropertyChanged -= IsButtonVisible_PropertyChanged;  

private async void IsButtonVisible_PropertyChanged(object sender,PropertyChangedEventArgs e)
{
   if (IsSomthingEnabled)
   {
       await = SomeService.ExecuteAsync(...);
   }
}

【问题讨论】:

  • 事件处理程序不会返回,因此无需等待,如果您需要来自事件的响应,那么您应该创建一个具有可等待值的事件 arg,(很可能是偶数就其本身而言)
  • 这对我来说很有意义,谢谢。
  • 正确的语法是第二个async void。这是唯一应该使用async void 的情况
  • 可能正在寻找TaskCompletionSource 来触发方法;并等待一个事件;给await
  • 在 await 和正在等待的项目之间不需要 =

标签: c# asynchronous async-await eventhandler


【解决方案1】:

异步事件处理程序的语法是:

Something.PropertyChanged += IsButtonVisible_PropertyChanged;  
... 

private async void IsButtonVisible_PropertyChanged(object sender,
                                                   PropertyChangedEventArgs e)
{
   if (IsSomethingEnabled)
   {
       await SomeService.ExecuteAsync(...);
   }
}

这允许在事件处理程序中等待异步操作,而不会阻塞 UI 线程。但是,这不能用于在其他方法中等待事件。

等待单个事件

如果您希望其他代码等待事件完成,您需要一个 TaskCompletionSource。这在Tasks and the Event-based Asynchronous Pattern (EAP) 中有解释。

public Task<string> OnPropChangeAsync(Something x)
{
     var options=TaskCreationOptions.RunContinuationsAsynchronously;
     var tcs = new TaskCompletionSource<string>(options);
     x.OnPropertyChanged += onChanged;
     return tcs.Task;

     void onChanged(object sender,PropertyChangedEventArgs e)
     {
         tcs.TrySetResult(e.PropertyName);
         x.OnPropertyChanged -= onChanged;
     }
     
}

....

async Task MyAsyncMethod()
{
    var sth=new Something();
    ....
    var propName=await OnPropertyChangeAsync(sth);
   
    if (propName=="Enabled" && IsSomethingEnabled)
    {
        await SomeService.ExecuteAsync(...);
    }

}

这在两个地方与示例不同:

  1. 事件触发后,事件处理程序委托被取消注册。否则,与 Something 一样,委托将保留在内存中。
  2. TaskCreationOptions.RunContinuationsAsynchronously 确保任何延续都将在单独的线程上运行。默认是在设置结果的同一线程上运行它们

此方法将只等待一个事件。循环调用每次都会创建一个新的TCS,很浪费。

等待事件流

在 C# 8 中引入 IAsyncEnumerable 之前,不可能轻松地 await 多个事件。使用 IAsyncEnumerable&lt;T&gt;Channel,可以创建一个发送通知流的方法:

public IAsyncEnumerable<string> OnPropChangeAsync(Something x,CancellationToken token)
{
     var channel=Channel.CreateUnbounded<string>();
     //Finish on cancellation
     token.Register(()=>channel.Writer.TryComplete());
     x.OnPropertyChanged += onChanged;
     
     return channel.Reader.ReadAllAsync();

     async void onChanged(object sender,PropertyChangedEventArgs e)
     {
         channel.Writer.SendAsync(e.PropertyName);
     }
     
}

....

async Task MyAsyncMethod(CancellationToken token)
{  
    var sth=new Something();
    ....
    await foreach(var prop in OnPropertyChangeAsync(sth),token)
    {
   
        if (propName=="Enabled" && IsSomethingEnabled)
        {
           await SomeService.ExecuteAsync(...);
        }
    }

}

在这种情况下,只需要一个事件处理程序。每次发生事件时,命名的属性都会被推送到ChannelChannel.Reader.ReadAllAsync() 用于返回可用于异步循环的IAsyncEnumerable&lt;string&gt;。循环将一直运行直到CancellationToken 发出信号,在这种情况下,编写器将进入Completed 状态,IAsyncEnumerable&lt;T&gt; 将终止。

【讨论】:

  • Plus 1,总是很好的答案并涵盖所有基础
【解决方案2】:

引用微软文章Async/Await - Best Practices in Asynchronous Programming,特别是Avoid async void部分:

返回无效的异步方法有一个特定的目的:使异步事件处理程序成为可能。 [...] 事件处理程序自然返回 void,因此异步方法返回 void 以便您可以拥有异步事件处理程序。

基于此,您的第三种方法是正确的:

private async void IsButtonVisible_PropertyChanged(object sender,
    PropertyChangedEventArgs e)
{
   if (IsSomethingEnabled)
   {
       await SomeService.ExecuteAsync();
   }
}

您的第一种方法 (+= async (o, args) =&gt; await) 在技术上是等效的,但不建议这样做,因为它是惯用的,可能会给未来的维护者造成混淆。

您的第二种方法 (_ = SomeService.ExecuteAsync() 以即发即弃的方式启动异步操作,这不是一个好主意,因为您的应用程序完全忘记了该任务。它也是elides async and await,它打开了另一个蠕虫罐。

【讨论】:

    【解决方案3】:

    异步事件处理程序的语法是

    async void handler(object sender,EventArgs args){}
    

    而且由于事件没有返回,所以没有什么可以等待的,所以等待它们是没有意义的

    但是,如果您需要来自事件的响应,那么您可以使用 EventsArgs 类来提供响应,例如

    class FeedbackEventArgs:EventArgs
    {
        event EventHandler Completed;
        Complete(){
            this.Completed(this,EventArgs.Empty);
        }
    }
    

    那么你就可以把它当做

    event EventHandler<FeedbackEventArgs> myFeedbackEvent;
    
    args = new FeedbackEventArgs();
    args.Completed += OnCompleted;
    this.myFeedbackEvent(this,args)
    

    请注意,如果您的处理程序不是异步的,那么您可以假设您的代码在事件发生时已暂停,在这种情况下,您可以从 eventArg 中读取一个属性,而不必触发事件

    class FeedbackEventArgs:EventArgs
    {
        int result{get;set;}
    }
    
    event EventHandler<FeedbackEventArgs> myFeedbackEvent;
    
    this.myFeedbackEvent(this,args)
    args.result //this will be the result set in the sync handler
    

    正如@Panagiotis 所指出的,这是一个概念性示例,而不是一个工作示例

    【讨论】:

    • 第一部分正确,第二部分错误。虽然该代码可以运行(有几个修复),但一切都在主线程上运行。要真正等待一个事件,您需要一个 TaskCompletionSource
    • 您不需要作为 TaskCompletionSource ,但这是避免跨线程问题的简单方法
    猜你喜欢
    • 2015-04-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-07-29
    • 2016-08-17
    • 2013-11-08
    相关资源
    最近更新 更多