Как заставить C # асинхронные операции выполняться ... синхронизированным образом? - PullRequest
2 голосов
/ 27 октября 2011

У меня есть два класса: 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();
}

I ожидаю, что увидит следующий вывод:

----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 не должен изменяться, но его внутреннее поведение каким-то образом должно ставить в очередь любые асинхронные операции, которые он выполняет в поточно-ориентированном виде. Другой недостаток заключается в том, что я не могу добавить блокировки в WriteLine LineWriter's, потому что в моем реальном случае это метод, который я не могу изменить (хотя в этом примере это действительно дает ожидаемый результат).

Ссылка на некоторый код, предназначенный для решения аналогичной проблемы, может быть достаточно хороша, чтобы направить меня на правильный путь. Или, возможно, некоторые альтернативные идеи. Спасибо.

P.S. Если вам интересно, какой вариант использования может использовать такую ​​вещь: это класс, который поддерживает сетевое соединение, в котором одновременно может быть активна только одна операция; Я использую асинхронные вызовы для каждой операции. Не существует очевидного способа, чтобы по-настоящему параллельные линии связи проходили через одно сетевое соединение, поэтому им нужно будет так или иначе ждать друг друга.

Ответы [ 2 ]

4 голосов
/ 27 октября 2011

Замки здесь не могут работать.Вы пытаетесь ввести блокировку в одном потоке (вызывающем потоке) и выйти из него в другом потоке (ThreadPool).
Поскольку блокировки .Net повторно вводятся, вводите его повторно в потоке вызывающегоне ждет(если бы он ожидал, он бы зашел в тупик, поскольку вы можете выйти из блокировки только в этом потоке)

Вместо этого вы должны создать метод, который вызывает синхронную версию в блокировке, а затем вызвать этот метод асинхронно.
Таким образом, вы вводите блокировку в асинхронном потоке, а не в потоке вызывающего, и выходите из него в том же потоке после завершения метода.


Однако это неэффективно, поскольку вы 'тратить потоки, ожидающие блокировки.
Было бы лучше создать один поток с очередью делегатов для его выполнения, чтобы при вызове асинхронного метода запускалась одна копия этого потока или добавлялась в очередьесли поток уже запущен.
Когда этот поток завершает очередь, он завершает работу, а затем перезапускается при следующем вызове асинхронного метода.Я сделал это в аналогичном контексте;см. мой предыдущий вопрос .

Обратите внимание, что вам нужно реализовать IAsyncResult самостоятельно или использовать шаблон обратного вызова (который обычно намного проще).

0 голосов
/ 28 октября 2011

Я создал следующий класс "Асинхронизатор":

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, использовать выделенный поток очереди и помещать в него действия.

...