4

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;
}
Community
  • 1
  • 1
Christopher King
  • 1,034
  • 1
  • 8
  • 21

2 Answers2

4

As usr said, it's not possible to do something in reaction to a Task being awaited. 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));
svick
  • 236,525
  • 50
  • 385
  • 514
  • 1
    @ChristopherKing Also check out [`AsyncFactory.FromWaitHandle`](http://dotnetapis.com/pkg/Nito.AsyncEx/3.0.1/net45/doc/Nito.AsyncEx.AsyncFactory/FromWaitHandle(System.Threading.WaitHandle)) – Stephen Cleary Oct 31 '16 at 12:58
1

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.

usr
  • 168,620
  • 35
  • 240
  • 369
  • Usr, good to know TPL doesn't provide this out of the box so I'm not missing something obvious. Is there a way to extend (aka "hack") the TPL to add this functionality? Looks to me that Scott is using ICriticalNotifyCompletion.[Unsafe]OnCompleted to intercept continuation attach events. If so, then I was hoping someone with some experience with these undocumented TPL plumbing APIs would think this a worthy general scenario and be able to cook a solution.. – Christopher King Oct 29 '16 at 18:45
  • What he does is fully documented and conceptually not difficult. He does the custom awaitable thing. It's just no longer a task. You can only await this thing, but not use it with task APIs. – usr Oct 29 '16 at 18:51
  • Ah. Well then that would be why I couldn't understand how to turn his work into a task! I'll go what you suggest except I'll use a `Lazy>` to offload the work of ensuring just one TaskCompletionSource gets created. Thanks! – Christopher King Oct 29 '16 at 19:48