#c #multithreading #mutex
#c #многопоточность #мьютекс
Вопрос:
Предположим, sharedFnc
это функция, которая используется между несколькими потоками:
void sharedFnc(){
// do some thread safe work here
}
Какой из них является правильным способом использования мьютекса здесь?
A)
void sharedFnc(){
// do some thread safe work here
}
int main(){
...
pthread_mutex_lock(amp;lock);
sharedFnc();
pthread_mutex_unlock(amp;lock);
...
}
Или B)
void sharedFnc(){
pthread_mutex_lock(amp;lock);
// do some thread safe work here
pthread_mutex_unlock(amp;lock);
}
int main(){
...
sharedFnc();
...
}
Комментарии:
1. Оба будут работать. Вопрос в том, имеет ли смысл вызывать эту функцию без блокировки мьютекса?
2. Обычно это происходит внутри функции. Таким образом, вызывающему не нужно беспокоиться о блокировке.
3. Концепция «совместно используемой функции» обычно неверна. Это данные , которые потенциально являются общими и должны быть синхронизированы между потоками, обращающимися к ним, а не к функциям .
4. Оба варианта подходят. Если бы
sharedFnc
это была единственная функция, я мог бы поместить блокировки мьютекса внутрь нее. Но, если бы существовало более одной функции [которую также необходимо было защитить] (например,sharedFnc2
andsharedFnc3
), я бы сделал:LOCK; sharedFnc(); sharedFnc2(); sharedFnc3(); UNLOCK
. Но я мог бы обернуть эту последовательность в [еще] другую функцию [которая является опцией B]
Ответ №1:
Давайте рассмотрим две крайности:
В первом случае вы даже не можете сказать, какую блокировку вам нужно получить, пока не окажетесь внутри функции. Возможно, функция находит объект и работает с ним, и блокировка выполняется для каждого объекта. Итак, как вызывающий может узнать, какую блокировку удерживать?
И, возможно, коду нужно выполнить некоторую работу, удерживая блокировку, и некоторую работу, не удерживая блокировку. Возможно, ему нужно снять блокировку во время ожидания чего-либо.
В этом случае блокировка должна быть получена и снята внутри функции.
В противоположном случае функция может даже не иметь представления о том, что она используется несколькими потоками. Возможно, он понятия не имеет, с какой блокировкой связаны его данные. Возможно, она вызывается для разных данных в разное время и эти данные защищены разными блокировками.
Возможно, ее вызывающей стороне необходимо вызвать несколько разных функций, удерживая одну и ту же блокировку. Возможно, эта функция сообщает некоторую информацию, на основании которой поток решит вызвать какую-либо другую функцию, и крайне важно, чтобы состояние не было изменено другим потоком между этими двумя функциями.
В этом случае вызывающий должен получить и снять блокировку.
Между этими двумя крайностями это вызов суждения, основанный на том, к какой крайности ситуация ближе. Кроме того, это не единственные два доступных варианта. Также существуют промежуточные опции.
Ответ №2:
Есть кое-что, что можно сказать об этом шаблоне:
// Only call this with `lock` locked.
//
static sometype foofunc_locked(...) {
...
}
sometype foofunc(...) {
pthread_mutex_lock(amp;lock);
sometype rVal = foofunc_locked(...);
pthread_mutex_unlock(amp;lock);
return rVal;
}
Это отделяет ответственность за блокировку и разблокировку мьютекса от любых других обязанностей, воплощенных в foofunc_locked(...)
.
Одна из причин, по которой вы хотели бы это сделать, заключается в том, что очень легко увидеть, разблокирует ли каждый возможный вызов foofunc()
lock
перед его возвратом. Это могло бы быть не так, если бы блокировка и разблокировка были смешаны с циклами, switch
операторами и вложенными if
операторами и returns
из середины и т.д.
Ответ №3:
Если блокировка находится внутри функции, вам лучше чертовски убедиться, что здесь не задействована рекурсия, особенно косвенная рекурсия.
Другая проблема, связанная с блокировкой внутри функции, — это циклы, где у вас есть две большие проблемы:
-
Производительность. Каждый цикл вы освобождаете и повторно запрашиваете свои блокировки. Это может быть дорогостоящим, особенно в ОС, подобных Linux, которые не имеют легких блокировок, таких как критические разделы.
-
Семантика блокировки. Если есть работа, которую нужно выполнить внутри цикла, но за пределами вашей функции, вы не сможете получить блокировку один раз за цикл, потому что это приведет к полной блокировке вашей функции. Таким образом, вам придется еще больше усложнять цикл цикла, вызывая вашу функцию (acquire-release), затем вручную получать блокировку, выполнять дополнительную работу и вручную снимать ее до завершения цикла. И у вас нет абсолютно никакой гарантии того, что произойдет между тем, как ваша функция выпустит ее, и тем, как вы ее получите.
Комментарии:
1. Повторно, «… убедитесь, черт возьми … » Вы можете сделать это, создав объект «reentrant lock». (поскольку библиотека потоков posix еще не предоставляет ее.) Просто еще один инструмент в наборе.