Jay Sullivan
Jay Sullivan

Reputation: 18249

How to force C# asynchronous operations to run in a...synchronized manner?

I have these two classes: LineWriter and AsyncLineWriter. My goal is to allow a caller to queue up multiple pending asynchronous invocations on AsyncLineWriter’s methods, but under the covers never truly allow them to run in parallel; i.e., they must somehow queue up and wait for one another. That's it. The rest of this question gives a complete example so there's absolutely no ambiguity about what I'm really asking.

LineWriter has a single synchronous method (WriteLine) that takes about 5 seconds to run :

public class LineWriter
{
    public void WriteLine(string line)
    {
            Console.WriteLine("1:" + line);
            Thread.Sleep(1000);
            Console.WriteLine("2:" + line);
            Thread.Sleep(1000);
            Console.WriteLine("3:" + line);
            Thread.Sleep(1000);
            Console.WriteLine("4:" + line);
            Thread.Sleep(1000);
            Console.WriteLine("5:" + line);
            Thread.Sleep(1000);
    }
}

AsyncLineWriter just encapsulates LineWriter and provides an asynchronous interface (BeginWriteLine and EndWriteLine):

public class AsyncLineWriter
{
    public AsyncLineWriter()
    {
        // Async Stuff
        m_LineWriter = new LineWriter();
        DoWriteLine = new WriteLineDelegate(m_LineWriter.WriteLine);
        // Locking Stuff
        m_Lock = new object();
    }

#region Async Stuff

    private LineWriter m_LineWriter;
    private delegate void WriteLineDelegate(string line);
    private WriteLineDelegate DoWriteLine;
    public IAsyncResult BeginWriteLine(string line, AsyncCallback callback, object state)
    {
        EnterLock();
        return DoWriteLine.BeginInvoke(line, callback, state);
    }
    public void EndWriteLine(IAsyncResult result)
    {
        DoWriteLine.EndInvoke(result);
        ExitLock();
    }

#endregion

#region Locking Stuff

    private object m_Lock;
    private void EnterLock()
    {
        Monitor.Enter(m_Lock);
        Console.WriteLine("----EnterLock----");
    }
    private void ExitLock()
    {
        Console.WriteLine("----ExitLock----");
        Monitor.Exit(m_Lock);
    }

#endregion

}

As I said in the first paragraph, my goal is to only allow one pending asynchronous operation at a time. I’d really like it if I didn’t need to use locks here; i.e., if BeginWriteLine could return an IAsyncResult handle which always returns immediately, and yet retains the expected behavior, that would be great, but I can’t figure out how to do that. So the next best way to explain what I'm after seemed to be to just use locks.

Still, my “Locking Stuff” section isn’t working as expected. I would expect the above code (which runs EnterLock before an async operation begins and ExitLock after an async operation ends) to only allow one pending async operation to run at any given time.

If I run the following code:

static void Main(string[] args)
{
    AsyncLineWriter writer = new AsyncLineWriter();

    var aresult = writer.BeginWriteLine("atest", null, null);
    var bresult = writer.BeginWriteLine("btest", null, null);
    var cresult = writer.BeginWriteLine("ctest", null, null);

    writer.EndWriteLine(aresult);
    writer.EndWriteLine(bresult);
    writer.EndWriteLine(cresult);

    Console.WriteLine("----Done----");
    Console.ReadLine();
}

I expect to see the following output:

----EnterLock----
1:atest
2:atest
3:atest
4:atest
5:atest
----ExitLock----
----EnterLock----
1:btest
2:btest
3:btest
4:btest
5:btest
----ExitLock----
----EnterLock----
1:ctest
2:ctest
3:ctest
4:ctest
5:ctest
----ExitLock----
----Done----

But instead I see the following, which means they’re all just running in parallel as if the locks had no effect:

----EnterLock----
----EnterLock----
----EnterLock----
1:atest
1:btest
1:ctest
2:atest
2:btest
2:ctest
3:atest
3:btest
3:ctest
4:atest
4:btest
4:ctest
5:atest
5:btest
5:ctest
----ExitLock----
----ExitLock----
----ExitLock----
----Done----

I’m assuming the locks are “ignored” because (and correct me if I’m wrong) I’m locking them all from the same thread. My question is: how can I get my expected behavior? And “don’t use asynchronous operations” is not an acceptable answer.

There is a bigger, more complex, real life use case for this, using asynchronous operations, where sometimes it’s not possible to have multiple truly parallel operations, but I need to at least emulate the behavior even if it means queuing the operations and running them one after another. Specifically, the interface to AsyncLineWriter shouldn’t change, but it’s internal behavior somehow needs to queue up any async operations it’s given in a thread-safe manner. Another gotcha is that I can't add locks into LineWriter's WriteLine, because in my real case, this is a method which I can't change (although, in this example, doing so actually does give me the expected output).

A link to some code designed to solve a similar issue might be good enough to get me on the right path. Or perhaps some alternative ideas. Thanks.

P.S. If you're wondering what kind of use case could possibly use such a thing: it's a class which maintains a network connection, upon which only one operation can be active at a time; I'm using asynchronous calls for each operation. There's no obvious way to have truly parallel lines of communication go on over a single network connection, so they will need to wait for each other in one way or another.

Upvotes: 2

Views: 3382

Answers (2)

Jay Sullivan
Jay Sullivan

Reputation: 18249

I created the following "Asynchronizer" class:

public class Asynchronizer
{
    public readonly Action<Action> DoAction;

    public Asynchronizer()
    {
        m_Lock = new object();
        DoAction = new Action<Action>(ActionWrapper);
    }
    private object m_Lock;
    private void ActionWrapper(Action action)
    {
        lock (m_Lock)
        {
            action();
        }
    }
}

You can use it like this:

public IAsyncResult BeginWriteLine(string line, AsyncCallback callback, object state)
{
    Action action = () => m_LineWriter.WriteLine(line);
    return m_Asynchronizer.DoAction.BeginInvoke(action, callback, state);
}
public void EndWriteLine(IAsyncResult result)
{
    m_Asynchronizer.DoAction.EndInvoke(result);
}

This way it's easy to synchronize multiple asynchronous operations even if they have different method signatures. E.g., we could also do this:

public IAsyncResult BeginWriteLine2(string line1, string line2, AsyncCallback callback, object state)
{
    Action action = () => m_LineWriter.WriteLine(line1, line2);
    return m_Asynchronizer.DoAction.BeginInvoke(action, callback, state);
}
public void EndWriteLine2(IAsyncResult result)
{
    m_Asynchronizer.DoAction.EndInvoke(result);
}

A user can queue up as many BeginWriteLine/EndWriteLine and BeginWriteLine2/EndWriteLine2 as they want, and they will be called in a staggered, synchronized, fashion (although you will have as many threads open as operations are pending, so only practical if you know you'll only ever have a asynchronous operations pending at a time). A better, more complex, solution is, like SLaks pointed out, to use a dedicated queue-thread and queue the actions into it.

Upvotes: 0

SLaks
SLaks

Reputation: 887413

Locks cannot possibly work here. You're trying to enter the lock on one thread (the calling thread) and exit from it on a different thread (the ThreadPool).
Since .Net locks are re-entrant, entering it a second time on the caller thread doesn't wait. (had it waited, it would deadlock, since you can only exit the lock on that thread)

Instead, you should create a method that calls the synchronous version in a lock, then call that method asynchronously.
This way, you're entering the lock on the async thread rather than the caller thread, and exiting it on the same thread after the method finishes.


However, this is inefficient,since you're wasting threads waiting on the lock.
It would be better to make a single thread with a queue of delegates to execute on it, so that calling the async method would start one copy of that thread, or add it to the queue if the thread is already running.
When this thread finishes the queue, it would exit, and then be restarted next time you call an async method. I've done this in a similar context; see my earlier question.

Note that you'll need to implement IAsyncResult yourself, or use a callback pattern (which is generally much simpler).

Upvotes: 4

Related Questions