У меня следующая проблема: у меня есть класс, который должен быть защищен от одновременного доступа из разных потоков. У класса есть два метода: lock () и unlock () using (g_mutex_lock
/ g_mutex_unlock
с каждого объекта GMutex
). Теперь метод блокировки выглядит так:
void Object::method()
{
lock();
// do stuff modifying the object
unlock();
}
Теперь давайте предположим, что у меня есть два метода этого типа, method1()
а также method2()
который я называю один за другим:
object.method1();
// but what if some other thread modifies object in between
object.method2();
Я попытался заблокировать объект перед этим блоком и снова разблокировать его, но в этом случае
есть тупик даже с одной нитью, потому что GMutex
не знает, что он уже заблокирован тем же потоком. Решение было бы изменить метод, чтобы принять дополнительный bool
определить, заблокирован ли объект. Но есть ли более элегантная концепция? Или это недостаток концепции дизайна в целом?
Решение рекурсивного мьютекса, упомянутое в других ответах и комментариях, будет работать просто отлично, но по моему опыту оно приводит к тому, что код сложнее поддерживать, потому что, как только вы переключаетесь на рекурсивный мьютекс, слишком легко злоупотреблять им и блокировать все место.
Вместо этого я предпочитаю реорганизовать код так, чтобы однократной блокировки было достаточно. В вашем примере я бы определил класс следующим образом:
class Object {
public:
void method1() {
GMutexLock scopeLock(&lock);
method1_locked();
}
void method2() {
GMutexLock scopeLock(&lock);
method2_locked();
}
void method1_and_2() {
GMutexLock scopeLock(&lock);
method1_locked();
method2_locked();
}
private:
void method1_locked();
void method2_locked();
GMutex lock;
};
«Закрытые» версии ваших методов являются частными, поэтому они доступны только внутри класса. Класс берет на себя ответственность за то, чтобы никогда не вызывать их без снятой блокировки.
Снаружи у вас есть три варианта вызова методов, в зависимости от того, какие методы вы хотите запустить.
Обратите внимание, что еще одно улучшение, которое я сделал, это не использование явных блокирующих примитивов, а косвенное использование области видимости для блокировки и разблокировки. Это то, что GMutexLock
делает. Пример реализации для этого класса ниже:
class GMutexLock {
private:
GMutex* m;
GMutexLock(const GMutexLock &mlock); // not allowed
GMutexLock &operator=(const GMutexLock &); // not allowed
public:
GMutexLock(GMutex* mutex) {
m = mutex;
g_mutex_lock(m);
}
~GMutexLock() {
g_mutex_unlock(m);
}
};
Посмотрите «рекурсивный мьютекс» или «реентерабельный мьютекс«(по сравнению с нерекурсивным мьютексом, который вы используете сейчас). Они включают то, что вы хотите. Некоторые люди не являются поклонниками рекурсивных мьютексов и чувствуют, что они допускают грязный дизайн.
Обратите внимание, что рекурсивный мьютекс не может быть заблокирован в одном потоке и разблокирован в другом.
Лично я бы никогда не использовал рекурсивные мьютексы (особенно как таковые).
Я бы сделал некоторые частные функции, которые не блокируют мьютексы и не блокируют их в реализациях открытых функций.