В моей новой команде, которой я управляю, большая часть нашего кода - это платформа, сокет TCP и код сети http. Все на С ++. Большая часть этого произошла от других разработчиков, которые покинули команду. Нынешние разработчики в команде очень умные, но в основном младшие с точки зрения опыта.
Наша самая большая проблема: многопоточные ошибки параллелизма. Большинство наших библиотек классов написаны как асинхронные с использованием некоторых классов пула потоков. Методы в библиотеках классов часто ставят в очередь долго выполняющиеся такты в пул потоков из одного потока, а затем методы обратного вызова этого класса вызываются в другом потоке. В результате у нас есть много ошибок в крайнем случае, связанных с неверными предположениями о потоке. Это приводит к тонким ошибкам, которые выходят за рамки просто наличия критических секций и блокировок для защиты от проблем параллелизма.
Что делает эти проблемы еще сложнее, так это то, что попытки исправить их часто неверны. Некоторые ошибки, которые я наблюдал при попытке команды (или внутри самого унаследованного кода), включают в себя что-то вроде следующего:
Распространенная ошибка № 1 - Устранение проблемы параллелизма, просто установив блокировку общих данных, но забывая о том, что происходит, когда методы не вызываются в ожидаемом порядке. Вот очень простой пример:
void Foo::OnHttpRequestComplete(statuscode status)
{
m_pBar->DoSomethingImportant(status);
}
void Foo::Shutdown()
{
m_pBar->Cleanup();
delete m_pBar;
m_pBar=nullptr;
}
Так что теперь у нас есть ошибка, из-за которой Shutdown может вызываться, пока происходит OnHttpNetworkRequestComplete. Тестировщик находит ошибку, фиксирует аварийный дамп и назначает ошибку разработчику. Он в свою очередь исправляет ошибку, как это.
void Foo::OnHttpRequestComplete(statuscode status)
{
AutoLock lock(m_cs);
m_pBar->DoSomethingImportant(status);
}
void Foo::Shutdown()
{
AutoLock lock(m_cs);
m_pBar->Cleanup();
delete m_pBar;
m_pBar=nullptr;
}
Вышеуказанное исправление выглядит хорошо, пока вы не поймете, что есть еще более тонкий случай. Что произойдет, если Shutdown вызывается до того, как OnHttpRequestComplete будет вызван обратно? Примеры из реальной жизни, которые есть у моей команды, еще сложнее, а крайние случаи еще сложнее обнаружить в процессе проверки кода.
Распространенная ошибка № 2 - устранение проблем взаимоблокировки путем слепого выхода из блокировки, ожидания завершения другого потока, а затем повторного входа в блокировку - но без обработки случая, когда объект только что обновился другим потоком!
Распространенная ошибка № 3 - Несмотря на то, что объекты подсчитаны, последовательность выключения «освобождает» свой указатель. Но забывает ждать, пока поток, который все еще работает, освобождает свой экземпляр. Таким образом, компоненты полностью отключаются, а затем ложные или поздние обратные вызовы вызываются для объекта в состоянии, не ожидающем больше вызовов.
Есть и другие крайние случаи, но суть заключается в следующем:
Многопоточное программирование просто сложно, даже для умных людей.
Когда я улавливаю эти ошибки, я трачу время на обсуждение ошибок с каждым разработчиком для разработки более подходящего исправления. Но я подозреваю, что они часто путаются в том, как решить каждую проблему из-за огромного количества унаследованного кода, который «правильное» исправление будет включать в себя касание.
Мы скоро будем в продаже, и я уверен, что патчи, которые мы применяем, сохранятся для предстоящего релиза. После этого у нас будет время улучшить базу кода и рефакторинг, где это необходимо. У нас не будет времени просто переписать все. И большая часть кода не так уж и плоха. Но я пытаюсь реорганизовать код так, чтобы можно было избежать проблем с многопоточностью.
Один подход, который я рассматриваю, заключается в следующем. Для каждой важной функции платформы выделите отдельный поток, в который будут перенаправляться все события и сетевые обратные вызовы. Похоже на многопоточность COM-квартир в Windows с использованием цикла сообщений. Длинные операции блокировки могут все еще передаваться потоку рабочего пула, но обратный вызов завершения вызывается в потоке компонента. Компоненты могут даже иметь общий поток. Тогда все библиотеки классов, работающие внутри потока, могут быть написаны в предположении о едином поточном мире.
Прежде чем идти по этому пути, мне также очень интересно, есть ли другие стандартные методы или шаблоны проектирования для решения многопоточных проблем. И я должен подчеркнуть - нечто большее, чем книга, которая описывает основы мьютексов и семафоров. Что вы думаете?
Я также заинтересован в любых других подходах к процессу рефакторинга. Включая любое из следующего:
Литература или статьи о дизайне узоров вокруг ниток. Что-то за пределами введения в мьютексы и семафоры. Нам не нужен массивный параллелизм либо, только пути для разработки объектной модели таким образом , чтобы обрабатывать асинхронные события из других потоков правильно .
Способы составления схемы потоков различных компонентов, чтобы было легко изучать и разрабатывать решения. (То есть UML-эквивалент для обсуждения потоков между объектами и классами)
Обучение вашей команды разработчиков по вопросам с многопоточным кодом.
Чтобы ты делал?