2012-10-12 26 views
108

在我的C#/ XAML metro应用程序中,有一个按钮可以启动长时间运行的进程。因此,推荐,我使用异步/等待以确保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中发生的事情更复杂,并且在执行过程中的不同点可能需要不同类型的用户输入。因此,将逻辑分解为多种方法将是不平凡的。

回答

150

可以使用SemaphoreSlim Class的实例作为一个信号:

private SemaphoreSlim signal = new SemaphoreSlim(0, 1); 

// set signal in event 
signal.Release(); 

// wait for signal somewhere else 
await signal.WaitAsync(); 

或者,您可以使用TaskCompletionSource<T> Class的一个实例来创建Task<T>表示该按钮点击的结果:

private TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>(); 

// complete task in event 
tcs.SetResult(true); 

// wait for task somewhere else 
await tcs.Task; 
+0

我会使用'ManualResetEvent'。使用“SemaphoreSlim”有没有优势?或者你可以使用其中一种吗? –

+5

@DanielHilgarth'ManualResetEvent(Slim)'似乎不支持'WaitAsync()'。 – svick

+0

@svick:好点。但是,由于'GetResult'已经是'async',所以你可以在这个方法中不会出现任何问题,不是吗? –

4

理想情况下,你不要。虽然你当然可以阻止异步线程,但这是浪费资源,并不理想。

考虑用户在按钮等待点击时去午餐的规范示例。

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

也就是说,如果在异步操作中设置了需要保持启用按钮的位置并且您正在“等待”点击的状态,则会更好。此时,您的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是继续得到您的按钮被按下的事件结果的方法。如果你的按钮是而不是推送,那么你的事件处理程序什么都不做。

+0

什么是异步线程?在原始问题和答案中都没有*代码不会在UI线程上运行。 – svick

+0

@svick不正确。 'GetResults'返回一个'Task'。 “等待”只是说“运行任务,当任务完成时,在此之后继续执行代码”。考虑到有一个同步上下文,调用会被封送回UI线程,因为它在'await'上被捕获。 'await'与* Task.Wait()'不同*,完全没有。 – casperOne

+0

我没有说'Wait()'什么。但'GetResults()'中的代码将在这里的UI线程上运行,没有其他线程。换句话说,是的,'await'基本上可以执行任务,就像你说的那样,但是在这里,这个任务也在UI线程上运行。 – svick

54

当你有你需要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就像一个asyncManualResetEvent,但你只能“集”的事件一次,该事件可以有一个“结果”(在这种情况下,我们没有使用它,所以我们只是将结果设置为null)。

+5

因为我解析“等待事件”与“在任务中包装EAP”基本相同的情况,所以我肯定会更喜欢这种方法。恕我直言,这绝对是更简单/更容易推理的代码。 –

2

Stephen Toub发布了这个AsyncManualResetEventon his blog

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

这里是我使用的工具类:

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

简单的辅助类:

public class EventAwaiter<TEventArgs> 
{ 
    #region Fields 

    private TaskCompletionSource<TEventArgs> _eventArrived = new TaskCompletionSource<TEventArgs>(); 

    #endregion Fields 

    #region Properties 

    public Task<TEventArgs> Task { get; set; } 

    public EventHandler<TEventArgs> Subscription => (s, e) => _eventArrived.TrySetResult(e); 

    #endregion Properties 
} 

用法:

var valueChangedEventAwaiter = new EventAwaiter<YourEventArgs>(); 
example.YourEvent += valueChangedEventAwaiter.Subscription; 
await valueChangedEventAwaiter.Task; 
+0

你将如何清理对'example.YourEvent'的订阅? –