Reputation: 1062
If the Task exposed by my TaskCompletionSource may never get called how can I deffer computation of the Result unless and until someone awaits the task?
For example, I want to block other async threads of execution until a ManualResetEvent is signaled using the following function WaitOneAsync. I complete the TaskCompleationSource in the callback of ThreadPool.RegisterWaitForSingleObject which happens when the WaitHandle is signaled. But if no one awaits the task then I don't want to RegisterWaitForSingleObject (nor do I want to RegisterWaitForSingleObject if the task is awaited after the WaitHandle is signaled).
How can I change WaitOneAsync so that the work to compute the result, to RegisterWaitForSingleObject, only happens after someone awaits the TaskCompleationSource.Task?
I believe the answer may lie in a custom TaskAwaiter as described here Implement AsyncManualResetEvent using Lazy<T> to determine if the task has been awaited by Scott Chamberlain but I can't quite get from his example to my solution... :(
public static async Task<T> WaitOneAsync<T>(this WaitHandle waitHandle, Func<T> result) {
var tcs = new TaskCompletionSource<T>();
RegisteredWaitHandle rwh = null;
rwh = ThreadPool.RegisterWaitForSingleObject(
waitObject: waitHandle,
callBack: (s, t) => {
rwh.Unregister(null);
tcs.TrySetResult(result());
},
state: null,
millisecondsTimeOutInterval: -1,
executeOnlyOnce: true
);
return await tcs.Task;
}
Upvotes: 4
Views: 289
Reputation: 244757
As usr said, it's not possible to do something in reaction to a Task
being await
ed. But if you're okay with using a custom awaitable, then you can.
An easy way to do that is to use AsyncLazy
from Stephen Cleary's AsyncEx:
private static Task<T> WaitOneAsyncImpl<T>(WaitHandle waitHandle, Func<T> result)
{
if (waitHandle.WaitOne(0))
return Task.FromResult(result());
var tcs = new TaskCompletionSource<T>();
RegisteredWaitHandle rwh = null;
rwh = ThreadPool.RegisterWaitForSingleObject(
waitObject: waitHandle,
callBack: (s, t) =>
{
rwh.Unregister(null);
tcs.TrySetResult(result());
},
state: null,
millisecondsTimeOutInterval: -1,
executeOnlyOnce: true
);
return tcs.Task;
}
public static AsyncLazy<T> WaitOneAsync<T>(this WaitHandle waitHandle, Func<T> result)
=> new AsyncLazy<T>(() => WaitOneAsyncImpl(waitHandle, result));
Upvotes: 4
Reputation: 171178
This is not possible exactly as you described your requirements. The TPL does not provide an event or a callback when someone adds a continuation or waits on a task.
So you need to structure the API so that only tasks that are needed are actually produced. What about this?
public static Func<Task<T>> CreateWaitOneAsyncFactory<T>(this WaitHandle waitHandle, Func<T> result) {
return () => WaitOneAsync(waitHandle, result);
}
This returns a task factory instead of a task. This is cheating but the only possible solutions involve cheating of this kind.
You can return a custom awaitable as well. But that would not involve tasks at all and it misses the composability features of tasks. Awaitables mostly are a C# concept. Exposing them can result in unclean APIs.
Unrelated to your question: You can remove the await tcs.Task
and return that task directly. Also, the result
function is not needed. Return a Task
that has no result. Callers can then add a result if they wish. This makes the API of WaitOneAsync
cleaner.
Upvotes: 1