Как сохранить асинхронный параллельный программный код управляемым (например, в C++)
В настоящее время я работаю над серверным приложением, которое должно управлять устройствами сбора данных по сети. Из-за этого нам нужно много параллельного программирования. Со временем я узнал, что существует три подхода к взаимодействию между объектами обработки (потоками / процессами / приложениями). К сожалению, все три подхода имеют свои недостатки.
А) Вы можете сделать синхронный запрос (синхронный вызов функции). В этом случае вызывающая сторона ожидает, пока функция не будет обработана и ответ не будет получен. Например:
const bool convertedSuccessfully = Sync_ConvertMovie(params);
Проблема в том, что звонящий не работает. Иногда это просто не вариант. Например, если вызов был выполнен потоком пользовательского интерфейса, может показаться, что приложение заблокировано до получения ответа, что может занять много времени.
Б) Вы можете сделать асинхронный запрос и дождаться обратного вызова. Код клиента может продолжаться с тем, что должно быть сделано.
Async_ConvertMovie(params, TheFunctionToCallWhenTheResponseArrives);
Это решение имеет большое неудобство, что функция обратного вызова обязательно выполняется в отдельном потоке. Проблема в том, что теперь трудно вернуть ответ вызывающей стороне. Например, вы нажали кнопку в диалоговом окне, которое вызывало службу асинхронно, но диалоговое окно было долго закрыто, когда поступил обратный вызов.
void TheFunctionToCallWhenTheResponseArrives()
{
//Difficulty 1: how to get to the dialog instance?
//Difficulty 2: how to guarantee in a thread-safe manner that
// the dialog instance is still valid?
}
Само по себе это не такая большая проблема. Однако, когда вы хотите сделать более одного из таких звонков, и все они зависят от ответа предыдущего, в моем опыте это становится неуправляемо сложным.
C) Последний вариант, который я вижу, - сделать асинхронный запрос и продолжать опрос до получения ответа. В промежутке между проверками "есть ответ", вы можете сделать что-то полезное. Это лучшее из известных мне решений для решения случая, когда необходимо выполнить последовательность асинхронных вызовов функций. Это потому, что у него есть большое преимущество, что у вас все еще есть весь контекст вызывающего, когда приходит ответ. Кроме того, логическая последовательность вызовов остается достаточно ясной. Например:
const CallHandle c1 = Sync_ConvertMovie(sourceFile, destFile);
while(!c1.ResponseHasArrived())
{
//... do something in the meanwhile
}
if (!c1.IsSuccessful())
return;
const CallHandle c2 = Sync_CopyFile(destFile, otherLocation);
while(!c1.ResponseHasArrived())
{
//... do something in the meanwhile
}
if (c1.IsSuccessful())
//show a success dialog
Проблема с этим третьим решением состоит в том, что вы не можете вернуться из функции вызывающей стороны. Это делает его неподходящим, если работа, которую вы хотите выполнить между ними, не имеет ничего общего с работой, которую вы выполняете асинхронно. В течение долгого времени мне было интересно, есть ли какая-то другая возможность вызывать функции асинхронно, у которой нет недостатков перечисленных выше опций. У кого-нибудь есть идея, возможно, какая-нибудь хитрая уловка?
Примечание: приведенный пример - C++- подобный псевдокод. Тем не менее, я думаю, что этот вопрос в равной степени относится к C# и Java, и, возможно, ко многим другим языкам.
3 ответа
Вы могли бы рассмотреть явный "цикл обработки событий" или "цикл обработки сообщений", не слишком отличающийся от классических подходов, таких как select
цикл для асинхронных сетевых задач или цикл сообщений для оконной системы. События, которые поступают, могут при необходимости отправляться в функцию обратного вызова, например, в вашем примере B, но в некоторых случаях они также могут отслеживаться по-разному, например, чтобы вызывать транзакции в конечном автомате. FSM - это прекрасный способ управлять сложностью взаимодействия по протоколу, который, в конце концов, требует много шагов!
Один из подходов к систематизации этих соображений начинается с шаблона проектирования реактора.
Работа ACE Шмидта является хорошей отправной точкой для решения этих проблем, если вы пришли из C++; Twisted также весьма полезен, на фоне Python; и я уверен, что аналогичные рамки и наборы технических документов существуют, как вы говорите, для "многих других языков" (URL-адрес Википедии, который я дал, указывает на реализации Reactor для других языков, кроме ACE и Twisted).
Я склонен идти с B, но вместо того, чтобы звонить вперед и назад, я бы делал всю обработку, включая последующие действия в отдельном потоке. Тем временем основной поток может обновлять графический интерфейс и либо активно ждать завершения потока (т. Е. Показать диалоговое окно с индикатором выполнения), либо просто позволить ему выполнить свою работу в фоновом режиме и получить уведомление, когда это будет сделано. Пока никаких проблем со сложностью, поскольку вся обработка на самом деле является синхронной с точки зрения потока обработки. С точки зрения графического интерфейса, он асинхронный.
Кроме того, в.NET нет проблем переключиться на поток GUI. Класс BackgroundWorker и ThreadPool также упрощают эту задачу (я использовал ThreadPool, если я правильно помню). В Qt, например, остаться с C++ тоже довольно просто.
Я использовал этот подход в нашем последнем крупном приложении и очень доволен им.
Как сказал Алекс, посмотрите на Proactor и Reactor, описанные Дагом Шмидтом в разделе "Шаблоны архитектуры программного обеспечения".
Есть конкретные реализации их для разных платформ в ACE.