Christopher King
Christopher King

Reputation: 1062

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

Upvotes: 4

Views: 289

Answers (2)

svick
svick

Reputation: 244757

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

Upvotes: 4

usr
usr

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

Related Questions