Нужно ли мне синхронизировать клиентов TCP / UDP в асинхронном обратном вызове BeginReceive

У меня есть многопоточное сетевое приложение, использующее UdpClient, TcpClient, TcpListener и обрабатывающее полученные соединения и полученные данные с использованием, например, BeginReceive() EndReceive() шаблон обратного вызова.

Используя UdpClient в качестве примера, в этом шаблоне общий поток работы, который я использую, следующий:

  1. Позвоните UdpClient.BeginReceive()
  2. Обратный вызов приема выполняется при получении дейтаграммы.
  3. Вызовите UdpClient.EndReceive(), чтобы получить дейтаграмму.
  4. Вызовите UdpClient.BeginReceive() еще раз, чтобы подготовиться к приему другой дейтаграммы.
  5. Обработайте дейтаграмму, полученную в (3).
  6. Повторите 2–5 по мере получения новых дейтаграмм.

В: Поскольку существует только один объект UdpClient, и из-за того, что всегда вызывается EndReceive() перед следующим BeginReceive(), необходимо ли блокировать / синхронизировать доступ к объекту UdpClient для этих вызовов?

Мне кажется, что другой поток не сможет вмешаться в этот рабочий процесс или сделать эти вызовы неатомарными. Шаблон для TcpClient.BeginReceive() и TcpListener.BeginAcceptTcpClient() очень похож.

Бонус Q: нужно ли объявить единственный UdpClient объект staticstatic заблокировать object, если это необходимо)?

Примечание. Я не спрашиваю, нужно ли выполнять какие-либо блокировки во время, например, обработка дейтаграмм. Только относительно этого паттерна и объектов UdpClient TcpClient TcpListener.


ИЗМЕНИТЬ

В качестве пояснения (без учета обработки исключений) следующий код:

private void InitUDP()
{
    udpclient = new UdpClient(new IPEndPoint(IPAddress.Any, Settings.Port));

    udpclient.BeginReceive(new AsyncCallback(receiveCallback), udpclient);
}

private void receiveCallback(IAsyncResult ar)
{
    UdpClient client = (UdpClient)ar.AsyncState;

    IPEndPoint ep = new IPEndPoint(IPAddress.Any, 0);

    byte[] datagram = client.EndReceive(ar, ref ep);

    udpclient.BeginReceive(new AsyncCallback(receiveCallback), udpclient);

    processDatagram();
}

Практически другой или менее защищающий, чем этот код:

private void InitUDP()
{
    udpclient = new UdpClient(new IPEndPoint(IPAddress.Any, Settings.Port));

    udpclient.BeginReceive(new AsyncCallback(receiveCallback), udpclient);
}

private void receiveCallback(IAsyncResult ar)
{
    UdpClient client = (UdpClient)ar.AsyncState;

    IPEndPoint ep = new IPEndPoint(IPAddress.Any, 0);

    lock(_lock)
    {
        byte[] datagram = client.EndReceive(ar, ref ep);

        udpclient.BeginReceive(new AsyncCallback(receiveCallback), udpclient);
    }

    processDatagram();
}

person khargoosh    schedule 14.10.2015    source источник
comment
UdpClient функции-члены не являются потокобезопасными, поэтому, если несколько потоков обращаются к одному и тому же экземпляру, у вас есть проблема. Я не уверен, был ли это ваш вопрос.   -  person MicroVirus    schedule 14.10.2015
comment
Но вот в чем вопрос - может ли другой поток получить доступ к тому же экземпляру через эти обратные вызовы, если EndReceive() всегда вызывается перед следующим BeginReceive()? Мне нужно заблокировать эти звонки?   -  person khargoosh    schedule 14.10.2015
comment
Вы контролируете потоки, поэтому вы решаете, какой поток к чему имеет доступ. Так возможно ли это? Да, если обратные вызовы могут легально получить доступ к экземпляру, например, когда он является общедоступным или обратные вызовы являются членами одного и того же экземпляра - в некотором смысле, если вы можете написать код для доступа к экземпляру в обратном вызове, и он затем компилируется, да, вы можете получить к нему доступ. Но если я не неправильно понял, это не тот вопрос, на который вы действительно хотите получить ответ, потому что, как я уже сказал, вы решаете, что делает каждый обратный вызов / поток, верно?   -  person MicroVirus    schedule 14.10.2015
comment
Вот еще один ответ SO, показывающий, как можно использовать BeginReceive и EndReceive. В нем также говорится об использовании задач как способе упрощения кода, что также дает понять, что в вашем коде / обратных вызовах нет потоковой передачи.   -  person MicroVirus    schedule 14.10.2015


Ответы (1)


необходимо ли блокировать / синхронизировать доступ к объекту UdpClient для этих вызовов?

Нет, не совсем, но, возможно, не по той причине, о которой вы думаете.

Если вы вызываете BeginReceiveFrom() (или просто BeginReceive()) до того, как закончите обработку текущей дейтаграммы, фактически возможно одновременное выполнение одного и того же обратного вызова. Произойдет ли это на самом деле , зависит от многих вещей, включая планирование потоков, количество потоков IOCP, доступных в настоящее время в пуле потоков, и, конечно, есть ли даже дейтаграмма для приема.

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

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

Но что касается самой дейтаграммы, сетевые объекты являются потокобезопасными в том смысле, что вы не повредите объект, используя его одновременно, это все еще зависит от вас, чтобы убедиться, что вы используете их согласованным образом. Но с протоколом UDP, в частности, это проще, чем с TCP.

UDP ненадежен. У него отсутствие трех очень важных гарантий:

  1. Нет никакой гарантии, что дейтаграмма будет доставлена.
  2. Нет гарантии, что дейтаграмма не будет доставлена ​​более одного раза.
  3. Нет гарантии, что дейтаграмма будет доставлена ​​в том же порядке, что и другие дейтаграммы, в которых она была отправлена.

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


С TCP все по-другому. У вас снова возникает та же проблема, что если вы начали операцию ввода-вывода, она наверняка может завершиться до того, как вы закончите обработку текущей операции ввода-вывода. Но, в отличие от UDP, у вас есть некоторые гарантии с TCP, включая то, что данные, полученные в сокете, будут получены в том же порядке, в котором они были отправлены.

Итак, пока вы не вызываете BeginReceive(), пока вы полностью не закончите обработку текущей завершенной операции ввода-вывода, все в порядке. Ваш код видит данные в правильном порядке. Но если вы вызовете BeginReceive() раньше, тогда ваш текущий поток может быть освобожден до того, как он завершит обработку текущей операции ввода-вывода, а другой поток может завершить обработку недавно завершенной операции ввода-вывода.

Если вы не выполнили какую-либо синхронизацию или упорядочение полученных данных, чтобы учесть возможность обработки неупорядоченных операций ввода-вывода, это повредит ваши данные. Нехорошо.

Есть веские причины для одновременного выполнения нескольких операций приема. Но обычно они связаны с потребностью в высокомасштабируемом сервере. Есть также недостатки, связанные с выполнением нескольких одновременных операций приема, в том числе дополнительная сложность обеспечения обработки данных в правильном порядке, а также накладные расходы, связанные с наличием нескольких фиксированных / закрепленных буферов в вашей куче (хотя это можно уменьшить в различные способы, такие как выделение достаточно больших буферов, чтобы они находились в куче больших объектов).

Я бы не стал реализовывать код таким образом, если только у вас нет конкретной проблемы с производительностью, которую вам нужно решить. Даже при работе с UDP, но особенно при работе с TCP. И если вы действительно реализуете код таким образом, делайте это с большой осторожностью.

Нужно ли объявлять единственный объект UdpClient статическим (и объект статической блокировки, если он требуется)?

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

Все, что делает что-то static, - это меняет способ доступа к этому члену. Если не static, вам нужно указать ссылку на экземпляр, в котором находится элемент; если это static, вам просто нужно указать тип. Это все. Это не имеет ничего общего с потокобезопасностью как таковой.


Наконец, что касается двух ваших примеров кода, они функционально эквивалентны. Нет необходимости защищать вызовы EndReceive() и BeginReceive(), и ваш lock не включает в себя какую-либо другую часть этих методов (например, фактическую обработку дейтаграммы), поэтому на самом деле он ничего не выполняет (кроме возможного увеличения накладных расходов переключателей контекста).

В параллельном сценарии возможно, что первый поток будет освобожден до выхода из lock, но после вызова BeginReceive(). Это может привести к пробуждению второго потока для обработки обратного вызова для второго завершения ввода-вывода. Затем этот второй поток попадет в lock и остановится, позволяя первому потоку возобновить выполнение и покинуть lock. Но все, что делает эта синхронизация, - это замедляет работу. Это не предотвращает одновременный доступ к самим данным дейтаграммы, что (возможно) является важной частью.

person Peter Duniho    schedule 14.10.2015
comment
Спасибо @Peter, очень исчерпывающий ответ и обсуждение. Это принесло мне много пользы. Если я правильно понимаю вашу точку зрения, если мои требования к производительности не высоки, кажется, что вы рекомендовали бы переместить processDatagram() после EndReceive() вызова и перед последующим BeginReceive() вызовом для всех протоколов. И для моего конкретного приложения это будет нормально. Что касается самой обработки дейтаграммы, я в настоящее время ставлю в очередь и блокирую очередь, и это обрабатывается в другом месте. - person khargoosh; 14.10.2015
comment
Чтобы уточнить: что касается самой обработки дейтаграммы, я в настоящее время помещаю эти данные в очередь и блокирую очередь для доступа на чтение и запись, и это обрабатывается в другом месте. Сами данные являются общими и потокобезопасными. - person khargoosh; 14.10.2015
comment
Да, если у вас нет конкретной наблюдаемой и поддающейся количественной оценке проблемы с производительностью, которую необходимо решить, я бы позаботился о том, чтобы ваш вызов processDatagram() был выполнен до вашего следующего вызова BeginReceive(). Это упростит задачу. - person Peter Duniho; 14.10.2015
comment
Я не уверен в этом на 100%, поэтому, если я ошибаюсь, поправьте меня: асинхронный шаблон не вызывает обработчики в другом потоке. Вся эта блокировка актуальна, когда есть несколько потоков, но сокеты вызывают обработчики завершения в том же потоке, что и BeginReceive-call. Следовательно, единственная проблема, с которой вы должны справиться, - это то, что вы не знаете порядок, в котором вызываются обратные вызовы, но все эти вызовы являются синхронными! - person MicroVirus; 14.10.2015
comment
Они могут «перекрываться» (несколько обратных вызовов действуют одновременно) в определенных ситуациях, но тогда они по-прежнему обрабатываются однопоточным способом, точно так же, как при обработке одного события в форме могут запускаться последующие события, которые затем обрабатывается до завершения первого события. Так что либо я что-то упустил, либо этот разговор о тредах здесь не актуален. Суть вашего ответа, порядок получения сообщений - это единственное, о чем нужно беспокоиться @khargoosh. - person MicroVirus; 14.10.2015
comment
@MicroVirus: Асинхронный шаблон не вызывает обработчики в другом потоке - вы определенно ошиблись в этом. В .NET асинхронные шаблоны почти всегда используют разные потоки для завершения. Очевидными исключениями являются Control.BeginInvoke() и Dispatcher.BeginInvoke() (они специально разработаны для работы в определенном потоке). Сетевой ввод-вывод, в частности, обрабатывается пулом потоков IOCP, а для обработки завершения ввода-вывода используются произвольные потоки. - person Peter Duniho; 14.10.2015
comment
@MicroVirus: поскольку используется пул потоков, при низкой нагрузке вы можете обнаружить, что в пуле есть только один поток, и это может создать впечатление, что вы всегда получаете завершение в одном и том же потоке. Но на самом деле все, что там происходит, - это недостаточная активность, чтобы заставить пул потоков создавать новые потоки. Под нагрузкой это произойдет, и эти дополнительные потоки используются для обработки завершений ввода-вывода по мере их возникновения, независимо от того, где был инициирован ввод-вывод. - person Peter Duniho; 14.10.2015
comment
Спасибо за ответ, это кое-что прояснило. Надеюсь, мои предыдущие комментарии не смущают читателей. - person MicroVirus; 14.10.2015
comment
@MicroVirus: рад помочь ... если вас беспокоит путаница, у вас есть возможность удалить свои комментарии. При этом следует учитывать вероятность того, что какие-либо неправильные представления, которые могли быть у вас, могут быть у кого-то еще, и он может извлечь выгоду из ваших комментариев и моих ответов. Но я не думаю, что большой вред будет нанесен, если вы действительно захотите удалить комментарии. - person Peter Duniho; 14.10.2015