How to make a nonblocking wait handle?
Asked Answered
I

3

5

Essentially, what I'm doing is creating a web server to handle an API call, and then when it's done continue the method execution, so essentially:

new WebServer(myAutoResetEvent);
myAutoResetEvent.WaitOne();

However, this blocks the thread until then. Is there any way to make this async? Is it fine just to wrap it in a await Task.Run() call, i.e. await Task.Run(() => myAutoResetEvent.WaitOne())?

Thanks!

Inspirit answered 31/12, 2015 at 15:59 Comment(8)
Can you give more details? Is this code on the server side or the client side?Vietnam
@YacoubMassad This is a wholly client side app. Basically I'm making a reddit oauth2 request which then redirects to 127.0.0.1:someport, which I then intercept to get the query strings so that I can ask for a reddit API token, but I need to wait for that to happen before I can carry on the execution.Rigdon
Which line is blocking the thread? I imagine you are calling myAutoResetEvent.WaitOne() for this thread to be blocked. If the constructor new WebServer(myAutoResetEvent) is blocking the thread, why do you need the myAutoResetEvent.WaitOne()?Heliocentric
@Heliocentric The webserver is waiting for its first request, which I will then take the query params from. When this first request happens, myAutoResetEvent's Set method will be called.Rigdon
Can you add sample code showing which line is blocking. I understand the premise, but the devil is in the implementation details, right :) ?!?!Heliocentric
@Heliocentric The WaitOne line is blocking, waiting for the ARE to be Set()Rigdon
OK. I assumed that is your intention. myAutoResetEvent.WaitOne() tells the system to block the current thread till the myAutoResetEvent.Set() is called. myAutoResetEvent.WaitOne() serves no other purpose, or does it? What I mean to say is, if myAutoResetEvent.WaitOne() is blocking and you don't want it to block, you should simply delete that line. Am I missing something here?Heliocentric
@Heliocentric the asker wants the remainder of the method to continue after Set() is called but without blocking the thread.Nehru
W
5

Normally, the WebServer ctor should not do anything interesting. There should be a Task WebServer.RunAsync function that runs the server. You can then use the resulting task to synchronize and coordinate.

If you don't want that you can use a TaskCompletionSource<object> as a one-shot async-ready event.

I believe the ThreadPool class has a way to efficiently wait for a WaitHandle to be set but that's a worse solution.

Westhead answered 31/12, 2015 at 16:20 Comment(2)
It's for a very lightweight application so I'm just gonna use this: codehosting.net/blog/BlogEngine/post/Simple-C-Web-Server.aspx - there's no RunAsync method. A TaskCompletionSource seems like a good idea, though! Let me try that out :DRigdon
Yep, the TaskCompletionSource works :D Thanks a lot!Rigdon
N
4

You should not block ThreadPool threads, this is a quick way to lead to ThreadPool starvation, instead there is a provided method to asynchronously wait for WaitHandle instances, this is called ThreadPool.RegisterWaitForSingleObject.

By using ThreadPool.RegisterWaitForSingleObject a callback is registered to be invoked when the WaitHandle is available, unfortunately this is not async/await compatible out of the box, a full implementation which makes this async/await compatible is as follows:

public static class WaitHandleExtensions
{
    public static Task WaitOneAsync(this WaitHandle waitHandle, CancellationToken cancellationToken)
    {    
        return WaitOneAsync(waitHandle, Timeout.Infinite, cancellationToken);
    }

    public static async Task<bool> WaitOneAsync(this WaitHandle waitHandle, int timeout, CancellationToken cancellationToken)
    {    
        // A Mutex can't use RegisterWaitForSingleObject as a Mutex requires the wait and release to be on the same thread
        // but RegisterWaitForSingleObject acquires the Mutex on a ThreadPool thread.
        if (waitHandle is Mutex)
            throw new ArgumentException(StringResources.MutexMayNotBeUsedWithWaitOneAsyncAsThreadIdentityIsEnforced, nameof(waitHandle));

        cancellationToken.ThrowIfCancellationRequested();

        var tcs = new TaskCompletionSource<bool>();
        var rwh = ThreadPool.RegisterWaitForSingleObject(waitHandle, OnWaitOrTimerCallback, tcs, timeout, true);

        var cancellationCallback = BuildCancellationCallback(rwh, tcs);

        using (cancellationToken.Register(cancellationCallback))
        {
            try
            {
                return await tcs.Task.ConfigureAwait(false);
            }
            finally
            {
                rwh.Unregister(null);
            }
        }
    }

    private static Action BuildCancellationCallback(RegisteredWaitHandle rwh, TaskCompletionSource<bool> tcs)
    {    
        return () =>
        {
            if (rwh.Unregister(null))
            {
                tcs.SetCanceled();
            }
        };
    }

    private static void OnWaitOrTimerCallback(object state, bool timedOut)
    {    
        var taskCompletionSource = (TaskCompletionSource<bool>)state;

        taskCompletionSource.SetResult(!timedOut);
    }
}

The only limitation is that this cannot be used with a Mutex.

This can be used like so:

await myAutoResetEvent.WaitOneAsync(cancellationToken).ConfigureAwait(false);
Nehru answered 31/12, 2015 at 16:27 Comment(4)
Not bad. If the TCS does not work for him this would be a very good solution.Westhead
It seems it is better to use simply AsyncAutoResetEvent variations, which use Task underneath and as the name suggests are fully async/awaitable: blogs.msdn.microsoft.com/pfxteam/2012/02/11/…Gati
Static typing, cleaner code, and my personal preference actually -- no IDisposable. No big deals, just "it seems it is better..." :-) On related note -- you introduced tiny bugs in the code, you should use TrySetXXX, not SetXXX.Gati
The blog post you linked has no cancellation support, no timeout support and requires you to reimplement each of the coordination types which is a lot more work. If you've seen a bug happen, some source demonstrating would be great.Nehru
C
0

Another approach to consider would be to use HttpSelfHostServer (System.Web.Http.SelfHost.dll) and leaving all of the threading details to its implementation.

var config = new HttpSelfHostConfiguration("http://localhost:9999");
var tcs = new TaskCompletionSource<Uri>();

using (var server = new HttpSelfHostServer(config, new MessageHandler(tcs)))
{
    await server.OpenAsync();

    await tcs.Task;

    await server.CloseAsync();
}

return tcs.Task.Result;

class MessageHandler : HttpMessageHandler
{
    private readonly TaskCompletionSource<Uri> _task;

    public MessageHandler(TaskCompletionSource<Uri> task)
    {
        _task = task;
    }

    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        _task.SetResult(request.RequestUri);
        return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK));
    }
}
Cartilaginous answered 31/12, 2015 at 16:55 Comment(2)
This might be a better webserver implementation for me to use. Thanks for the advice!Rigdon
@It'sNotALie You're welcome! Please upvote answers if you find them useful.Cartilaginous

© 2022 - 2024 — McMap. All rights reserved.