- PVSM.RU - https://www.pvsm.ru -
В Википедии дается следующее определение взаимной блокировки: «Взаимная блокировка (англ. deadlock) — ситуация в многозадачной среде или СУБД, при которой несколько процессов находятся в состоянии бесконечного ожидания ресурсов, занятых самими этими процессами».
Взаимные блокировки носят, как правило, динамический характер: их проявление зависит от таких факторов, как действия пользователя, доступность сетевых сервисов, позиционирование головки жесткого диска, переключение задач в системе с вытесняющей многозадачностью и т.п.
Классический пример взаимной блокировки: первый поток (A) захватывает мьютекс M1 и следом мьютекс M2. Второй поток (B) захватывает мьютекс M2, а уже после этого – мьютекс M1. Взаимная блокировка этих двух потоков может произойти следующим образом: поток A захватывает M1, поток B захватывает M2, после этого оба потока «обречены»: ни поток A не может захватить M2, ни поток B не может захватить M1; попытки захвата мьютексов заблокируют оба потока.
Описанная взаимная блокировка произойдет только в том случае, если оба потока успеют захватить ровно по одному мьютексу. В противном случае потоки продолжат свое выполнение.
Данная ситуация очень распространена в сложных многопоточных системах. Как правило, мьютексы-участники расположены далеко друг от друга (в различных компонентах системы), и выявить участников взаимной блокировки оказывается достаточно сложно.
Один из частных случаев возникновения описанной выше взаимной блокировки выглядит следующим образом:
Чем опасна описанная выше ситуация? Тем, что на этапе разработки объекта Worker разработчик еще не знал, какие именно функции системы будут вызваны через интерфейс обратного вызова. Он только предъявил требования к интерфейсу: функция должна иметь такие-то параметры, через которые будут передаваться такие-то данные. И этот вызов «в неизвестном направлении» делается при захваченном мьютексе.
Достаточно к описанной картине добавить несколько штрихов (в сложных системах так и происходит):
Всё. Получилась ситуация, при которой возможна взаимная блокировка. Она не будет происходить в 100% случаев (необходима определенная динамика, чтобы каждый из потоков-участников успел захватить только один мьютекс), и это существенно осложняет поиск таких ошибок.
Далее предлагаются два способа решения этой проблемы.
Объект Worker предоставит отдельные функции для блокирования и разблокирования своего внутреннего мьютекса, а потребитель будет регистрироваться следующим образом:
Недостатки этого способа очевидны:
Этот способ звучит многообещающе: если передача данных из внутреннего потока объекта Worker потребителям будет происходить при не заблокированных мьютексах, это исправит все возможные в дальнейшем взаимные блокировки при работе с объектом Worker.
Почему бы просто не делать обратный вызов при не захваченном мьютексе? Потому что поток объекта Worker должен пройтись по списку зарегистрированных потребителей и вызвать функцию интерфейса каждого потребителя. Если список не будет защищен мьютексом, и содержимое списка изменится в ходе этого цикла, вероятно зацикливание или даже аварийное завершение программы из-за некорректного обращения к памяти.
Почему бы не сделать копию списка потребителей (при создании копии захватывать мьютекс), а дальше пройти циклом по копии? Потому что нужно гарантировать потребителю, что после вызова unregisterCallback ему не будут переданы данные. Если потребитель вызывает unregisterCallback из своего деструктора, последующая передача данных в интерфейс обратного вызова этого потребителя приведет к аварийному завершению программы.
Таким образом, мы почти пришли к решению:
Вот и готовое решение. Для его реализации потребуется еще один объект синхронизации — «условная переменная» (англ. condition variable):
Важное замечание: если unregisterCallback может быть вызван из интерфейса обратного вызова, реализованного потребителем, то описанный алгоритм приведет к 100-процентному зависанию внутри unregisterCallback. Это легко решается: если unregisterCallback вызван в контексте внутреннего потока объекта Worker, проверять флаг и ожидать изменения условной переменной не нужно.
Заголовочный файл:
class ICallback
{
public:
virtual void dataReady(QByteArray data) = 0;
};
class Worker : public QThread
{
public:
Worker();
void registerCallback(ICallback *callback);
void unregisterCallback(ICallback *callback);
protected:
virtual void run();
private:
QMutex _mutex;
QWaitCondition _wait;
bool _callingNow;
QLinkedList<ICallback *> _callbacks;
};
Реализация:
Worker::Worker() :
QThread(),
_mutex(QMutex::NonRecursive),
_callingNow(false)
{
...
}
void Worker::registerCallback(ICallback *callback)
{
QMutexLocker locker(&_mutex);
_callbacks.append(callback);
}
void Worker::unregisterCallback(ICallback *callback)
{
QMutexLocker locker(&_mutex);
_callbacks.removeOne(callback);
if(QThread::currentThread()!=this)
{
while(_callingNow)
_wait.wait(&_mutex);
}
}
void Worker::run()
{
while(...)
{
QByteArray data;
...
QLinkedList<ICallback *> callbacksCopy;
_mutex.lock();
_callingNow=true;
callbacksCopy=_callbacks;
_mutex.unlock();
for(QLinkedList<Callback *>::const_iterator it=callbacksCopy.begin();
it!=callbacksCopy.end();
++it)
{
(*it)->dataReady(data);
}
_mutex.lock();
_callingNow=false;
_wait.wakeAll();
_mutex.unlock();
}
}
Автор: Nordavind
Источник [1]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/proektirovanie-i-refaktoring/31183
Ссылки в тексте:
[1] Источник: http://habrahabr.ru/post/175401/
Нажмите здесь для печати.