[英]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;
}
}
}
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 方法向我们展示了等待引发ClassWithEvent的MyEvent的实例。 :
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.