🎉 Celebrating 25 Years of GameDev.net! 🎉

Not many can claim 25 years on the Internet! Join us in celebrating this milestone. Learn more about our history, and thank you for being a part of our community!

Questions about WaitHandles in C#

Started by
3 comments, last by hplus0603 5 years, 4 months ago

I am using C#.
I have a UDPServer class with methods called BeginWork and EndWork.
When BeginWork is called, I want it to detect type A work that can be completed immediately, type B work that can be completed after a timeout, and type C work that can be completed when a WaitHandle is set (the wait handle is retrieved from a built in .NET BeginReceive call on a socket.)
I am trying to avoid using an additional thread.
I have a main game server thread that wants to wait for network related work that needs to be done, in which case it would call EndWork, or just wait for its own timeout value to elapse and then call its own Update method.
I would like to have BeginWork return a WaitHandle so that the main thread can call WaitOne or WaitAny, passing in its own timeout.
How can I combine the check for types A, B, and C work into a single WaitHandle that can be returned to the main thread?

A few more details just in case they are helpful:
My UDPServer class is implementing reliable UDP.
The type A work is detecting out of order packets that can now be processed because the in order packets leading up to them were finally received and processed.
The type B work is needing to send ack packets when we have waited long enough to group a few acks, but not so long that the other side will think it needs to resend an unacknowledged packet.
The type C work is just waiting for additional packets to come in.

If I create a MyWaitHandle class to wrap the logic of waiting on the network timeout and receive WaitHandle, then the main thread can not combine that with any other wait handles.
It might work for this particular scenario, but if the main thread ever needs to wait on something else at the same time then it won't work.

I would rather not expose multiple WaitHandles and a timeout value to my caller, because that complicates their logic for something that should be self contained.
They should only need a WaitHandle that they can wait on to know if any work is ready to be done in UDPServer or not.

I might be able to make it work by passing callbacks to BeginReceive and a System.Threading.Timer.
Then having those callbacks set a ManualResetEvent that corresponds to the WaitHandle I return, along with some internal state to note which work is ready.
This does introduce some ThreadPool threads, which is not as bad as me creating a dedicated thread, but the synchronization logic still seems to get hairy.

I guess I wanted something like this to exist:
var waitHandle = WaitHandle.CreateWaitAny(waitHandleArray, timeout);
But then I'd still need to internally determine which of the waitHandles was ready, or if the timeout occurred, so maybe that isn't really the right solution either.

Thanks for any help.

 

Advertisement

You can avoid type A scheduling altogether, just by doing all the processing enabled after each packet you receive. No need to "schedule" that work; that will just add overhead that reduces throughput.

To combine waiting for a timeout, and waiting for a number of other operations, you can simply keep the timeout as separate accounting in your program (typically as a priority queue) and use WaitAny() with a timeout in your main thread to expire the waiting when the deadline arrives.

enum Bool { True, False, FileNotFound };

Thanks for the advice.
My code evolved around using the WaitHandle from BeginReceive and EndReceive, and I think that sent me down a confusing road.

I've reworked things and I think the new setup will solve my important issues and be a lot cleaner.
I'm now using WaitForWork and DoWork methods.

Here is the rough code for the main thread:


var server = new UDPServer(port);
byte[] buffer = new byte[UDPServer.MAX_BUFFER_SIZE];
while (listening)
{
    if (!server.WaitForWork(timeUntilNextTick))
        OnTick();
    else
    {
        IPEndPoint remoteEP;
        ConnectionOperation op;
        int read;
        while (server.DoWork(buffer, out read, out remoteEP, out op))
        {
            if (op != ConnectionOperation.Receive)
                continue; // we could alternatively log a count of acks, keep alive, duplicate packets, etc.
            // otherwise we can read the payload
            // implementation elided
        }
    }
}

DoWork returns a bool telling me if there is more work because I only pass in one buffer, so it can only process one packet for me at a time, even if the first one it processes is an in order packet that makes previously cached out of order packets now ready for processing as well.
Here are the rough implementations:


public bool WaitForWork(int callerTimeout)
{ // returns true if work is available, false if the caller's timeout elapsed.
    int ackTimeout = GetTimeUntilEndOfAckGroupingWindow();
    int timeout = Math.Min(ackTimeout, callerTimeout);
    bool usingAckTimeout = ackTimeout <= callerTimeout;
	    if (mPendingReceiveAsyncResult == null)
    {
        var remoteEP = mAnyEP;
        mPendingReceiveAsyncResult = mSocket.BeginReceiveFrom(mReceiveBuffer, 0, mReceiveBuffer.Length, SocketFlags.None, ref remoteEP, null, null);
    }
    bool signalled = mPendingReceiveAsyncResult.AsyncWaitHandle.WaitOne(timeout);
    if (!signalled && usingAckTimeout)
        mTimeToSendAcks = true;
    // there is work to do if receive signalled, or the ack timeout elapsed.
    return signalled || usingAckTimeout;
}
public bool DoWork(byte[] writeBuffer, out int size, out IPEndPoint remoteEP, out ConnectionOperation op)
{ // returns true if more work can be done on a subsequent call.
    // attempt to handle any out of order packets that can now be processed
    if (mOutOfOrderPacketsCanBeProcessed)
    { // implementation elided
    }
	    if (mTimeToSendAcks)
    { // implementation elided
    }
	    if (mPendingReceiveAsyncResult.IsCompleted)
    {
        var tempRemoteEP = mAnyEP;
        int read = mSocket.EndReceiveFrom(mPendingReceiveAsyncResult, ref tempRemoteEP);
        mPendingReceiveAsyncResult = null;
        remoteEP = (IPEndPoint)tempRemoteEP;
        // process my reliable UDP headers, write the rest of the payload to the writeBuffer, and set mOutOfOrderPacketsCanBeProcessed and related member variables as needed.
        // implementation elided
    }
    // otherwise we were called when there was no work to be done, which is an invalid operation since you always want to be calling WaitForWork until it is true before calling DoWork.
    throw new InvalidOperationException("DoWork was called when there was no work to be done.");
}
public enum ConnectionOperation
{ // this is returned to callers of DoWork
    Connect,
    Disconnect,
    Receive,
    KeepAlive,
    Ack,
    OutOfOrder,
    Duplicate,
    Invalid,
}

I think that is going to work pretty well for me, and feels a lot cleaner than the previous direction I was headed.
Thanks again for the feedback. I've been following your posts for a long time and they have taught me a lot.

Edit: I'm blind and using a screen reader, and from what I can tell my code tags didn't work correctly.
Sorry if the formatting is bad for you. I'll try to figure out what I did wrong.
Edit2: Ah, I was supposed to use square brackets, not angle brackets.

 

Well, the brackets worked fine now ?

enum Bool { True, False, FileNotFound };

This topic is closed to new replies.

Advertisement