Три дня бился с зависанием, казалось бы, несложной двух-с-половиной-поточной программы.
(Один поток - UI, второй - рабочий, половинный - асинхронные колбеки непонятно откуда из MAPI).
Сперва сделал всё академически, на активных объектах,
мониторах и условных переменных.
Вкратце: монитор - это объект, методы которого охвачены критической секцией, так что одновременно там выполняется лишь один поток.
Монитор гарантирует, что в каждый момент времени объект внутренне (т.е. с точки зрения работающего с ним и в нём потока) непротиворечив. А в моменты, когда все потоки чего-то ждут - он ещё и внешне непротиворечив. Чтобы не возникали дедлоки, любое ожидание сопровождается двумя действиями:
1) поток приводит объект в непротиворечивое состояние
2) снимается блокировка критической секции
Условная переменная - это тот самый предмет ожидания. Обычно он состоит из
- собственно условия - булевой переменной или функции
- синхрообъекта-триггера, который взводится потоком, изменившим условие; в принципе, можно и без триггера, но тогда ожидание будет активным и неэффективным
// без триггера
while(!condition)
{
unlock(me); // временно вышли из крит.секции
sleep(small_timeout);
lock(me); // зашли обратно
}
// с триггером
while(!condition)
{
unlock(me);
wait(trigger);
lock(me);
reset(trigger); // сбосили триггер, уже будучи хозяином положения
}
- и, если условие отслеживают несколько потоков сразу, - планировщика, чтобы избежать голодания (когда один поток никак не успевает проснуться, пока другие сбрасывают триггер и условие)
Практикуя монитор, можно не особо заморачиваться с написанием функции рабочего потока.
Мы в самом начале входим в монитор (блокируя критическую секцию), и делаем что хотим.
Только периодически нужно вставлять рандеву: проверки условной переменной "не просят ли нас чего-нибудь, например, сдохнуть?" - в этих точках поток временно покидает критическую секцию и позволяет другому (в данном случае - интерфейсному) потоку влезть в монитор и что-нибудь поделать.
Это у нас получился активный объект.
Конечно, цена вопроса - излишняя монопольность. Рабочий поток держит блокировку не только в самые ответственные моменты (когда он читает и изменяет состояние объекта), но и во время остальных вычислений.
Это создаёт большие задержки при взаимодействии с активным объектом, что в случае UI может оказаться очень нехорошо.
Поскольку я наблюдал зависания - то предположил, что
- либо чего-то перемудрил и накосячил в реализации монитора (под виндами нет готовых средств, приходится конструировать из виндовских синхрообъектов; а boost как-бы-забанен)
- либо нарвался на непозволительно долгие вычисления
Поэтому, отчаянно скрипя сердцем, отказался от активного объекта, да и от условных переменных тоже.
Классическое и, я бы сказал, детское, решение: засунуть в критические секции только тот код, который непосредственно взаимодействует с общими данными. И никакого там ожидания, и никаких длинных операций! Всё, что нужно для работы - заранее вытаскивается в локальные копии (двойная буферизация).
Зависание продолжилось!!! Но уже не дедлок, и то приятно. То есть, проблемы рабочего потока перестали распространяться на поток UI. А проблемы рандеву с UI - перестали распространяться на ход рабочего потока. (Просто потому что рандеву исчезли).
Итог банален и прост:
- одна прогулка по памяти (неправильный формат в printf)
- и один бесконечный цикл в редко вызываемой функции
Ну и похороненный монитор. А жаль, мне он так нравился :( Но обратно переделывать я не буду! :)