Christopher King Christopher King - 1 month ago 9
C# Question

How can I implement a lazy TaskCompletionSource?

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

Answer

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));
Comments