У моїй новій команді, якою я керую, більшість нашого коду - це платформа, TCP-сокет і http-мережний код. Усі C ++. Більшість його походить від інших розробників, які покинули команду. Нинішні розробники в команді дуже розумні, але в основному молодші за рівнем досвіду.
Наша найбільша проблема: багатопотокові помилки одночасності. Більшість наших бібліотек класів написані асинхронними за допомогою деяких класів пулу потоків. Методи в бібліотеках класів часто вимагають довгих запущених дотиків до пулу потоків з одного потоку, а потім методи зворотного виклику цього класу викликаються в іншому потоці. Як результат, у нас є багато помилок у кращих випадках, пов’язаних з неправильними припущеннями про нарізку. Це призводить до тонких помилок, які виходять за рамки лише критичних розділів і блокувань, щоб захистити від проблем з одночасністю.
Що робить ці проблеми ще складнішими, це те, що спроби виправити часто неправильні. Деякі помилки, які я спостерігав, як команда намагалася (або в самому застарілому коді), включають щось таке:
Поширена помилка №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 для обговорення потоків між об'єктами та класами)
Навчання своєї команди розробників з питань багатопотокового коду.
Що б ти зробив?