Усиление моделирования :: Возможность блокировки с помощью семафора, а не мьютекса (ранее называлось: Разблокирование мьютекса из другого потока)

Я использую библиотеку boost :: thread C ++, что в моем случае означает, что я использую pthreads. Официально мьютекс должен быть разблокирован из того же потока, который его блокирует, и я хочу получить эффект блокировки в одном потоке, а затем разблокировки в другом. Есть много способов добиться этого. Одна из возможностей - написать новый класс мьютекса, который допускает такое поведение.

Например:

class inter_thread_mutex{
  bool locked;
  boost::mutex mx;
  boost::condition_variable cv;
public:
  void lock(){
    boost::unique_lock<boost::mutex> lck(mx);
    while(locked) cv.wait(lck);
    locked=true;
  }

  void unlock(){
    {
      boost::lock_guard<boost::mutex> lck(mx);
      if(!locked) error();
      locked=false;
    }
    cv.notify_one();
  }
// bool try_lock(); void error(); etc.
}

Я должен указать, что приведенный выше код не гарантирует доступ к FIFO, поскольку, если один поток вызывает lock (), а другой вызывает unlock (), этот первый поток может получить блокировку раньше других ожидающих потоков. (Если задуматься, документация boost :: thread, похоже, не дает никаких явных гарантий планирования для мьютексов или условных переменных). Но давайте пока просто проигнорируем это (и любые другие ошибки).

Мой вопрос: если я решу пойти по этому пути, смогу ли я использовать такой мьютекс в качестве модели для концепции Boost Lockable. Например, что-нибудь пойдет не так, если я использую boost::unique_lock< inter_thread_mutex > для доступа в стиле RAII, а затем передам эту блокировку boost::condition_variable_any.wait() и т. Д.

С одной стороны, я не понимаю, почему бы и нет. С другой стороны, «я не понимаю, почему бы и нет» обычно очень плохой способ определить, будет ли что-то работать.

Причина, по которой я спрашиваю, заключается в том, что если окажется, что мне нужно написать классы-оболочки для блокировок RAII, переменных условий и всего остального, тогда я бы предпочел просто найти другой способ достичь того же эффекта.

РЕДАКТИРОВАТЬ: Тип поведения, который я хочу, в основном выглядит следующим образом. У меня есть объект, и его нужно блокировать всякий раз, когда он изменяется. Я хочу заблокировать объект из одного потока и поработать над ним. Затем я хочу, чтобы объект был заблокирован, пока я приказываю другому рабочему потоку завершить работу. Таким образом, первый поток может продолжить и делать что-то еще, пока рабочий поток завершается. Когда рабочий поток завершает работу, он разблокирует мьютекс.

И я хочу, чтобы переход был незаметным, чтобы никто другой не мог установить блокировку мьютекса, когда поток 1 начинает работу, а поток 2 завершает ее.

Что-то вроде inter_thread_mutex, похоже, будет работать, и это также позволит программе взаимодействовать с ним, как если бы это был обычный мьютекс. Так что это похоже на чистое решение. Если есть лучшее решение, я был бы рад услышать и это.

ИЗМЕНИТЬ СНОВА: Причина, по которой мне нужны блокировки для начала, заключается в том, что существует несколько основных потоков, и блокировки предназначены для предотвращения одновременного доступа к общим объектам недопустимыми способами. Таким образом, код уже использует безблокировочную последовательность операций на уровне цикла на уровне главного потока. Кроме того, в исходной реализации не было рабочих потоков, а мьютексы были обычными кошерными мьютексами.

Inter_thread_thingy появился как оптимизация, в первую очередь для улучшения времени отклика. Во многих случаях этого было достаточно, чтобы гарантировать, что «первая часть» операции A произойдет до «первой части» операции B. В качестве глупого примера скажем, что я пробиваю объект 1 и получаю синяк под глазом. Затем я приказываю объекту 1 изменить его внутреннюю структуру, чтобы отразить все повреждения тканей. Я не хочу ждать повреждения ткани, прежде чем перейду к удару по объекту 2. Однако я хочу, чтобы повреждение ткани произошло как часть той же операции; например, тем временем я не хочу, чтобы какой-либо другой поток изменял конфигурацию объекта таким образом, чтобы повреждение ткани стало недопустимой операцией. (да, этот пример во многих отношениях несовершенен, и нет, я не работаю над игрой)

Итак, мы внесли изменение в модель, в которой право собственности на объект может быть передано рабочему потоку для завершения операции, и на самом деле это работает довольно хорошо; каждый главный поток может выполнить гораздо больше операций, потому что ему не нужно ждать их завершения. И поскольку последовательность событий на уровне главного потока по-прежнему основана на циклах, легко написать высокоуровневые операции главного потока, поскольку они могут быть основаны на предположении, что операция завершена (точнее, критический " первая часть ", от которой зависит логика последовательности, завершена), когда возвращается соответствующий вызов функции.

Наконец, я подумал, что было бы неплохо использовать мьютексы / семафоры inter_thread с использованием RAII с блокировками ускорения, чтобы инкапсулировать необходимую синхронизацию, которая требуется для того, чтобы все это работало.


person dan    schedule 02.05.2010    source источник
comment
Ваш код не будет работать, вы все еще блокируете разблокировку mx из разных потоков, что приведет к неопределенному поведению. Но семафор сделает то, что вы хотите, поищите его.   -  person Gunther Piez    schedule 03.05.2010
comment
На самом деле я думаю, что код будет работать. Блокировки ускорения имеют ограниченную область видимости, что означает, что они снимают блокировку мьютекса при разрушении. Таким образом, boost :: mutex освобождается тем же потоком, который его заблокировал. Назначение внутреннего мьютекса - просто заблокировать доступ к заблокированному логическому объекту. Однако я согласен с тем, что мне нужен семафор. По сути, этот inter_thread_mutex больше похож на двоичный семафор, который я называю мьютексом.   -  person dan    schedule 03.05.2010
comment
Этот код очень похож на этот код picturel.com/ucr/node32.html для реализация семафора с использованием мьютекса, который был связан с этим замечательным SO-ответом stackoverflow.com/questions/62814/ об отношении семафоров к мьютексам. Вопрос OP заключается в том, работает ли unique_lock и т. Д. С семафорами, а также с мьютексами.   -  person Potatoswatter    schedule 03.05.2010


Ответы (6)


man pthread_unlock (это в OS X, аналогичная формулировка в Linux) дает ответ:

NAME
     pthread_mutex_unlock -- unlock a mutex

SYNOPSIS
     #include <pthread.h>

     int
     pthread_mutex_unlock(pthread_mutex_t *mutex);

DESCRIPTION
     If the current thread holds the lock on mutex, then the
     pthread_mutex_unlock() function unlocks mutex.

     Calling pthread_mutex_unlock() with a mutex that the
     calling thread does not hold will result in
     undefined behavior.

     ...

Мой встречный вопрос - какую проблему синхронизации вы пытаетесь решить этим? Скорее всего, есть более простое решение.

Ни pthreads, ни boost::thread (построенные поверх него) не гарантируют какой-либо порядок, в котором конкурирующий мьютекс захватывается конкурирующими потоками.

person Nikolai Fetissov    schedule 02.05.2010
comment
Это я поняла. Вот почему я подумываю написать новый класс мьютекса, который можно разблокировать из другого потока. Код, который я написал в вопросе, сделал это. Мой вопрос в том, можно ли использовать описанный выше inter_thread_mutex в качестве концепции Lockable с другими объектами boost :: thread, в частности блокировками RAII и переменными состояния. - person dan; 03.05.2010
comment
@dan: то, что вы ищете, не мьютекс. Вы ищете что-то вроде семафора или условной переменной. - person jalf; 03.05.2010
comment
@jalf: да, я думаю, ты прав, и мне действительно нужен семафор. - person dan; 03.05.2010
comment
Я почти уверен, что эффективная реализация позволит pthread_mutex_unlock из любого потока. (Но проверить не повредит.) - person curiousguy; 24.10.2011

Простите, но я не понимаю. каково будет состояние вашего мьютекса в строке [1] следующего кода, если другой поток сможет его разблокировать?

inter_thread_mutex m;

{
  m.lock();
  // [1]
  m.unlock();
}

Это не имеет смысла.

person Vicente Botet Escriba    schedule 02.05.2010
comment
Если другой поток разблокирует его, это ошибка в программе. Идея состоит в том, чтобы иметь возможность заблокировать мьютекс в одном потоке, а затем передать блокировку другому потоку, который затем его разблокирует. - person dan; 03.05.2010
comment
@dan Можете ли вы привести пример того, как передача блокировки другому потоку для разблокировки полезна? И что именно повлечет за собой переход? - person Logan Capaldo; 03.05.2010
comment
@Logan: Идея передачи блокировки заключается в том, чтобы подать сигнал второму потоку, чтобы он начал делать что-то, что требует владения мьютексом. Как только второй поток запускается, он отправляет ответный сигнал, и первый поток освобождает владение мьютексом, не разблокируя его. Затем второй поток разблокирует его, когда это будет сделано. Но начинает казаться, что мьютекс не подходит для этого, как указывали другие. - person dan; 03.05.2010
comment
@dan Полагаю, что первый поток будет ждать сигнала от второго. Тогда второму потоку не нужно разблокировать мьютекс первого потока, поэтому нет необходимости в inter_thread_mutex. - person Vicente Botet Escriba; 03.05.2010

Есть несколько способов подойти к этому. Оба из них, которые я собираюсь предложить, будут включать добавление дополнительной информации к объекту, а не добавление механизма для разблокировки потока из потока, отличного от того, которому он принадлежит.

1) вы можете добавить некоторую информацию для обозначения состояния объекта:

enum modification_state { consistent, // ready to be examined or to start being modified
                          phase1_complete, // ready for the second thread to finish the work
                        };

// first worker thread
lock();
do_init_work(object);
object.mod_state = phase1_complete;
unlock();
signal();
do_other_stuff();

// second worker thread
lock()
while( object.mod_state != phase1_complete )
  wait()
do_final_work(obj)
object.mod_state = consistent;
unlock()
signal()

// some other thread that needs to read the data
lock()
while( object.mod_state != consistent )
  wait();
read_data(obj)
unlock()

Прекрасно работает с условными переменными, потому что, очевидно, вы не пишете свою собственную блокировку.

2) Если у вас есть конкретная тема, вы можете передать объекту владельца.

  // first worker
  lock();
  while( obj.owner != this_thread() ) wait();
  do_initial_work(obj);
  obj.owner = second_thread_id;
  unlock()
  signal()

  ...

Это почти то же решение, что и мое первое решение, но более гибкое при добавлении / удалении фаз и менее гибкое при добавлении / удалении потоков.

Честно говоря, я не уверен, как межпоточный мьютекс может вам здесь помочь. Вам все равно понадобится семафор или переменная условия, чтобы сигнализировать о передаче работы второму потоку.

person Logan Capaldo    schedule 02.05.2010
comment
Спасибо за это. Текущая реализация, которую я использую, аналогична (1) выше. Я просто подумал, может быть, это можно упростить чем-нибудь вроде inter_thread_mutex. Но похоже, что ответ, вероятно, отрицательный. Фактически, класс inter_thread_mutex действительно ближе к двоичному семафору, чем к мьютексу. Кроме того, да, я использую условную переменную для сигнализации, поэтому я, конечно, не пытаюсь избежать cvs. Просто ищу чистый способ смоделировать идею передачи владения заблокированным ресурсом от одного потока к другому. - person dan; 03.05.2010

Небольшая модификация того, что у вас уже есть: как насчет сохранения идентификатора потока, который вы хотите взять на себя блокировку, в вашем inter_thread_whatever? Затем разблокируйте его и отправьте сообщение в этот поток, в котором говорится: «Я хочу, чтобы вы выполнили любую процедуру, которая пытается получить эту блокировку».

Тогда условие в lock становится while(locked || (desired_locker != thisthread && desired_locker != 0)). Технически вы «сняли блокировку» в первом потоке и «сняли ее снова» во втором потоке, но никакой другой поток не может захватить ее между ними, так что это как если бы вы передали ее прямо из один к другому.

Существует потенциальная проблема: если поток завершается или завершается, когда это желаемый шкафчик вашей блокировки, то этот поток блокируется. Но вы уже говорили о том, что первый поток ожидает сообщения от второго потока, чтобы сказать, что он успешно получил блокировку, поэтому, по-видимому, у вас уже есть план того, что произойдет, если это сообщение никогда не будет получено. К этому плану добавьте «сбросить поле желаемого_блокировщика на inter_thread_whatever».

Это все очень непросто, но я не уверен, что то, что я предложил, правильно. Есть ли способ, которым «главный» поток (тот, который управляет всеми этими помощниками) может просто убедиться, что он не приказывает выполнять какие-либо дополнительные операции над тем, что защищено этой блокировкой, до тех пор, пока первая операция не будет завершена ( или происходит сбой, и вас уведомляет какая-то вещь RAII)? Вам не нужны блокировки как таковые, если вы можете справиться с этим на уровне цикла сообщений.

person Steve Jessop    schedule 03.05.2010
comment
Спасибо за комментарии. (1) Я думал о сохранении идентификатора потока, но решил (возможно, неправильно), что было бы проще логически управлять владением. В мысленных экспериментах казалось, что явное использование идентификаторов потоков в конечном итоге приведет к дополнительным сложностям (например, при обработке ошибок, о которой вы говорите). (2) Да, проблема в том, что никогда не приходит подтверждение; вкратце, в этом случае отправляется сообщение nack или ack-fail, которое восстанавливается и снимает блокировку. - person dan; 03.05.2010
comment
... (3) отредактировал исходное сообщение для более подробного описания того, как в целом работает блокировка главного потока. - person dan; 04.05.2010

Я не думаю, что будет хорошей идеей сказать, что ваш inter_thread_mutex (binary_semaphore) может рассматриваться как модель Lockable. Основная проблема в том, что главная черта вашего inter_thread_mutex побеждает концепцию Locakble. Если inter_thread_mutex был моделью lockable, вы ожидаете в [1], что inter_thread_mutex m заблокирован.

// thread T1
inter_thread_mutex m;

{
  unique_lock<inter_thread_mutex> lk(m);
  // [1]
}

Но поскольку другой поток T2 может делать m.unlock(), пока T1 находится в [1], гарантия нарушена.

Двоичные семафоры могут использоваться как Lockables, если каждый поток пытается заблокировать перед разблокировкой. Но основная цель вашего класса как раз наоборот.

Это одна из причин, по которой семафоры в Boost.Interprocess не используют блокировку / разблокировку для именования функций, а ждут / уведомляют. Любопытно, что это те же имена, которые используются в условиях :)

person Vicente Botet Escriba    schedule 04.05.2010
comment
В [1] вы ожидаете, что inter_thread_mutex m заблокирован Это будет, если вы не передадите lk в какой-то другой поток! Я не понимаю, в чем проблема. Дизайн простой, правильный, очевидный, и любое альтернативное решение, вероятно, будет намного сложнее. - person curiousguy; 24.10.2011

Мьютекс - это механизм для описания взаимоисключающих блоков кода. Для этих блоков кода не имеет смысла пересекать границы потоков. Попытка использовать такую ​​концепцию таким противоречивым интуитивным способом может привести только к проблемам в будущем.

Похоже, вы ищете другую концепцию многопоточности, но без более подробной информации трудно понять, что именно.

person Adam Bowen    schedule 02.05.2010
comment
Мьютекс - это механизм для описания взаимоисключающих блоков кода. Вовсе нет. взаимоисключающие блоки кода - это определение критического раздела. Мьютекс предназначен для монопольного доступа к данным. - person curiousguy; 24.10.2011
comment
Поскольку вы почувствовали необходимость проголосовать против второстепенной семантики: критический раздел сам по себе не является механизмом или алгоритмом для взаимного исключения. (en.wikipedia.org/wiki/Mutex). Мьютекс - это механизм, с помощью которого вы предотвращаете одновременное выполнение критических разделов; почти всегда это происходит потому, что они обращаются к одному и тому же ресурсу. В любом случае критические разделы и взаимное исключение - это тесно взаимосвязанные концепции, и точная семантика не совсем меняет мою точку зрения, которую я чувствовал, что первоначальный автор искал другой примитив. - person Adam Bowen; 24.10.2011