如何强制C#异步操作以...同步方式运行?

时间:2011-10-27 17:42:05

标签: c# .net asynchronous locking

我有这两个类:LineWriter和AsyncLineWriter。我的目标是允许调用者在AsyncLineWriter的方法上排队多个挂起的异步调用,但是在封面下永远不会真正允许它们并行运行;也就是说,他们必须以某种方式排队并等待彼此。而已。这个问题的其余部分给出了一个完整的例子,因此对于我真正要求的内容绝对没有歧义。

LineWriter有一个同步方法(WriteLine),大约需要5秒才能运行:

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只是封装了LineWriter并提供了一个异步接口(BeginWriteLine和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

}

正如我在第一段中所说,我的目标是一次只允许一个挂起的异步操作。如果我不需要在这里使用锁,我真的喜欢它;即,如果BeginWriteLine可以返回一个总是立即返回的IAsyncResult句柄,但仍然保留了预期的行为,那就太好了,但我无法弄清楚如何做到这一点。因此,解释我所追求的内容的下一个最佳方式似乎只是使用锁。

但是,我的“锁定东西”部分没有按预期工作。我希望上面的代码(在异步操作开始之前运行EnterLock并在异步操作结束后运行ExitLock)只允许在任何给定时间运行一个挂起的异步操作。

如果我运行以下代码:

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

期待看到以下输出:

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

但相反,我看到以下内容,这意味着它们只是并行运行,就像锁没有效果一样:

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

我假设锁被“忽略”,因为(如果我错了,请纠正我)我将它们全部锁定在同一个线程中。我的问题是:如何我得到了预期的行为?并且“不使用异步操作”不是一个可接受的答案。

有一个更大,更复杂的现实生活用例,使用异步操作,有时候不可能有多个真正的并行操作,但我需要至少模拟行为,即使它意味着排队操作并一个接一个地运行它们。具体来说,AsyncLineWriter的接口不应该改变,但它的内部行为需要以线程安全的方式排队任何异步操作。另一个问题是我无法在LineWriter的WriteLine中添加锁,因为在我的实际情况中,这是一个我无法改变的方法(尽管在这个例子中,这样做实际上确实给了我预期的输出)。

指向一些旨在解决类似问题的代码的链接可能足以让我走上正确的道路。或者也许是一些替代想法。感谢。

P.S。如果您想知道哪种用例可能会使用这样的东西:它是一个维护网络连接的类,一次只能激活一个操作;我正在为每个操作使用异步调用。没有明显的方法可以通过单一网络连接进行真正的并行通信,因此他们需要以某种方式等待彼此。

2 个答案:

答案 0 :(得分:4)

锁在这里不可行。您正试图在一个线程(调用线程)上输入锁定并在另一个线程(ThreadPool)上退出。 由于.Net锁是可重入的,因此在调用者线程上第二次输入它不会等待。 (如果它等了,它会死锁,因为你只能退出该线程上的锁)

相反,您应该创建一个在锁中调用同步版本的方法,然后异步调用该方法 这样,您将在异步线程而不是调用程序线程上输入锁定,并在方法完成后在同一线程上退出。


然而,这是低效的,因为你浪费了等待锁定的线程 最好使用一个委托队列来创建一个单独的线程,以便调用异步方法将启动该线程的一个副本,或者如果线程已在运行,则将其添加到队列中。 当此线程完成队列时,它将退出,然后在下次调用异步方法时重新启动。 我在类似的背景下做到了这一点;见my earlier question

请注意,您需要自己实现IAsyncResult,或使用回调模式(通常更简单)。

答案 1 :(得分:0)

我创建了以下“Asynchronizer”类:

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

你可以像这样使用它:

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

这样,即使它们具有不同的方法签名,也可以轻松地同步多个异步操作。例如,我们也可以这样做:

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

用户可以根据需要排队尽可能多的BeginWriteLine / EndWriteLine和BeginWriteLine2 / EndWriteLine2,并且它们将以交错,同步的方式进行调用(尽管您将拥有尽可能多的线程,因为操作正在等待,所以只有实用如果你知道你一次只有一个待定的异步操作)。像SLaks指出的那样,更好,更复杂的解决方案是使用专用的队列线程并将操作排入其中。