是否可以等待事件而不是其他异步方法?


156

在我的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中发生的事情更加复杂,并且在执行过程中的不同点可能需要不同类型的用户输入。因此,将逻辑分解为多种方法将是不平凡的。

Answers:


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 2012年

3
@DanielHilgarth不,你不能。async并不意味着“在不同的线程上运行”或类似的东西。它仅表示“您可以使用await此方法”。在这种情况下,内部阻塞GetResults()实际上会阻塞UI线程。
svick 2012年

2
@Gabe await本身不保证会创建另一个线程,但是它将导致语句之后的其他所有内容作为Task您调用的或上的等待项的继续运行await。通常情况下,它是某种类型的异步操作的,这可能是IO完成,或东西在另一个线程。
casperOne 2012年

16
+1。我必须进行查找,以防万一其他人感兴趣:SemaphoreSlim.WaitAsync不仅将推Wait送到线程池线程上。SemaphoreSlim有适当的队列,Task用于实现WaitAsync
Stephen Cleary 2012年

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
丹尼斯·P

@DenisP也许将事件传递给EventAwaiter的构造函数?
CJBrew

@DenisP我改进了版本并进行了简短测试。
菲利克斯·基尔

根据情况,我还可以看到添加IDisposable。另外,为了避免两次键入事件,我们还可以使用Reflection传递事件名称,因此用法更加简单。否则,我喜欢这种模式,谢谢。
丹尼斯·P

4

理想情况下,您不需要。尽管您当然可以阻止异步线程,但这是浪费资源,而且不是理想的选择。

考虑一个典型的示例,其中用户在等待单击按钮时去吃午餐。

如果您在等待用户输入时暂停了异步代码,那么这只是在暂停该线程时浪费资源。

也就是说,最好在异步操作中将需要维护的状态设置为启用按钮的状态,然后单击“等待”。此时,您的GetResults方法停止了

然后,当按钮点击后,根据您所储存的状态,启动另一个异步任务,继续工作。

因为SynchronizationContext将会在调用的事件处理程序中捕获GetResults(由于使用了await关键字,所以编译器将执行此操作,并且鉴于您在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 2012年

@svick不正确。 GetResults返回Taskawait只需说“运行任务,完成任务后,再继续执行代码”即可。在存在同步上下文的情况下,调用会被封送回UI线程,因为它是在上捕获的awaitawait一样的Task.Wait(),而不是在最少。
casperOne 2012年

我什么也没说Wait()。但是其中的代码GetResults()将在此处的UI线程上运行,没有其他线程。换句话说,是await的,就像您说的那样,基本上确实可以运行任务,但是在这里,该任务也可以在UI线程上运行。
svick 2012年

@svick没有理由假设任务在UI线程上运行,为什么要这么做呢?有可能,但不太可能。并且该调用是两个单独的UI调用,从技术上讲,一次调用到await,然后是之后的代码await,没有阻塞。其余代码以连续形式编组,并通过调度SynchronizationContext
casperOne 2012年

1
对于其他想要了解更多内容的人,请参见此处:chat.stackoverflow.com/rooms/17937-@svick和我基本上互相误解了,但都在说同样的话。
casperOne 2012年

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

0

具有反应性扩展(Rx.Net)

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

var res = await eventObservable.FirstAsync();

您可以在Nuget Package System中添加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谢谢!自从避免返回委托以来,我已经对其进行了修改。该资源可在此处作为Lara Web Engine的一部分(替代Blazor)获得。
cat_in_hat
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.