Используется ли блокировка мьютекса внутри совместно используемой функции или вне ее

#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 and sharedFnc3 ), я бы сделал: 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:

Если блокировка находится внутри функции, вам лучше чертовски убедиться, что здесь не задействована рекурсия, особенно косвенная рекурсия.

Другая проблема, связанная с блокировкой внутри функции, — это циклы, где у вас есть две большие проблемы:

  1. Производительность. Каждый цикл вы освобождаете и повторно запрашиваете свои блокировки. Это может быть дорогостоящим, особенно в ОС, подобных Linux, которые не имеют легких блокировок, таких как критические разделы.

  2. Семантика блокировки. Если есть работа, которую нужно выполнить внутри цикла, но за пределами вашей функции, вы не сможете получить блокировку один раз за цикл, потому что это приведет к полной блокировке вашей функции. Таким образом, вам придется еще больше усложнять цикл цикла, вызывая вашу функцию (acquire-release), затем вручную получать блокировку, выполнять дополнительную работу и вручную снимать ее до завершения цикла. И у вас нет абсолютно никакой гарантии того, что произойдет между тем, как ваша функция выпустит ее, и тем, как вы ее получите.

Комментарии:

1. Повторно, «… убедитесь, черт возьми … » Вы можете сделать это, создав объект «reentrant lock». (поскольку библиотека потоков posix еще не предоставляет ее.) Просто еще один инструмент в наборе.