Усиление моделирования :: Блокируется семафором, а не мьютексом (ранее назывался: разблокировка мьютекса из другого потока) - PullRequest
2 голосов
/ 03 мая 2010

Я использую библиотеку C ++ boost :: thread, что в моем случае означает, что я использую 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 не дает никаких явных гарантий планирования для мьютексов или условных переменных). Но давайте просто пока проигнорируем это (и любые другие ошибки).

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

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

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

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

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

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

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

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

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

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

Ответы [ 6 ]

3 голосов
/ 03 мая 2010

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.

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

     ...

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

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

0 голосов
/ 04 мая 2010

Не думаю, что будет хорошей идеей сказать, что ваш inter_thread_mutex (binary_semaphore) можно рассматривать как модель Lockable.Основная проблема в том, что главная особенность вашего inter_thread_mutex побеждает концепцию Locakble.Если inter_thread_mutex была моделью с блокировкой, в In [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 не используют блокировку / разблокировку для именования функций, а для ожидания / уведомления.Любопытно, что это те же имена, которые используются условиями:)

0 голосов
/ 03 мая 2010

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

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

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

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

0 голосов
/ 03 мая 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()

  ...

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

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

0 голосов
/ 03 мая 2010

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

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

0 голосов
/ 03 мая 2010

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

inter_thread_mutex m;

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

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

...