Construct Task from WaitHandle.Wait

2019-04-09 21:20发布

问题:

I chose to return Task<T> and Task from my objects methods to provide easy consumation by the gui. Some of the methods simply wait for mutex of other kind of waithandles . Is there a way to construct Task from WaitHandle.Wait() so that I don't have to block one treadpool thread for that.

回答1:

There is a way to do this: you can subscribe to WaitHandle using ThreadPool.RegisterWaitForSingleObject method and wrap it via TaskCompletionSource class:

public static class WaitHandleEx
{
    public static Task ToTask(this WaitHandle waitHandle)
    {
        var tcs = new TaskCompletionSource<object>();

        // Registering callback to wait till WaitHandle changes its state

        ThreadPool.RegisterWaitForSingleObject(
            waitObject: waitHandle,
            callBack:(o, timeout) => { tcs.SetResult(null); }, 
            state: null, 
            timeout: TimeSpan.MaxValue, 
            executeOnlyOnce: true);

        return tcs.Task;
    }
}

Usage:

WaitHandle wh = new AutoResetEvent(true);
var task = wh.ToTask();
task.Wait();


回答2:

As noted by @gordy in the comments of the accepted answer of Sergey Teplyakov, MSDN proposes an implementation with unsubscription of the registered WaitHandle.

I slightly modified it here to support the result of the callback: if the registration has timed out, the task return false. If the signal has been received, the task return true:

public static class ExtensionMethods
{
    public static Task<bool> WaitOneAsync(this WaitHandle waitHandle, int timeoutMs)
    {
        if (waitHandle == null)
            throw new ArgumentNullException(nameof(waitHandle));

        var tcs = new TaskCompletionSource<bool>();

        RegisteredWaitHandle registeredWaitHandle = ThreadPool.RegisterWaitForSingleObject(
            waitHandle,
            callBack: (state, timedOut) => { tcs.TrySetResult(!timedOut); }, 
            state: null,
            millisecondsTimeOutInterval: timeoutMs, 
            executeOnlyOnce: true);

        return tcs.Task.ContinueWith((antecedent) =>
        {
            registeredWaitHandle.Unregister(waitObject: null);
            try
            {
                return antecedent.Result;
            }
            catch 
            {
                return false;
                throw;
            }
        });
    }
}

Usage is same as the original answer:

WaitHandle signal = new AutoResetEvent(initialState: false);

bool signaled = await signal.WaitOneAsync(1000);
if (signaled)
{
    Console.WriteLine("Signal received");
}
else 
{
    Console.WriteLine("Waiting signal timed out");
}