Ручное снятие блокировок?

Ради изучения комбинаторики boost::thread я реализую простой барьер (BR) для потоков, которые блокируют общий мьютекс (M). Однако, насколько я понимаю, при переходе к BR.wait() блокировки на мьютексе не снимаются, поэтому для того, чтобы все потоки достигли BR, необходимо выполнить ручное снятие блокировки на M. Итак, у меня есть следующий код:

boost::barrier BR(3);
boost::mutex M;

void THfoo(int m){
    cout<<"TH"<<m<<" started and attempts locking M\n";
    boost::lock_guard<boost::mutex> ownlock(M);

    cout<<"TH"<<m<<" locked mutex\n";
    Wait_(15); //simple wait for few milliseconds

    M.unlock(); //probably bad idea
    //boost::lock_guard<boost::mutex> ~ownlock(M);
    // this TH needs to unlock the mutex before going to barrier BR

    cout<<"TH"<<m<<" unlocked mutex\n";
    cout<<"TH"<<m<<" going to BR\n";
    BR.wait();
    cout<<"TH"<<m<<" let loose from BR\n";
}

int main()  
{  
    boost::thread TH1(THfoo,1);
    boost::thread TH2(THfoo,2);
    boost::thread TH3(THfoo,3);

    TH2.join(); //but TH2 might end before TH1, and so destroy BR and M
    cout<<"exiting main TH \n";

    return 0;  
}

Принимая во внимание, что M.unlock() явно плохое решение (без использования блокировки); так как (просто) снять блокировку? Также: как мне (правильно) ждать в main() завершения всех потоков? (TH2.join() не работает, потому что TH2 может завершиться первым...);

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


person P Marecki    schedule 06.03.2012    source источник


Ответы (5)


Что-то типа:

void THfoo(int m){
  // use a scope here, this means that the lock_guard will be destroyed (and therefore mutex unlocked on exiting this scope
  {
    cout<<"TH"<<m<<" started and attempts locking M\n";
    boost::lock_guard<boost::mutex> ownlock(M);

    cout<<"TH"<<m<<" locked mutex\n";
    Wait_(15); //simple wait for few milliseconds

  }
  // This is outside of the lock
  cout<<"TH"<<m<<" unlocked mutex\n";
  cout<<"TH"<<m<<" going to BR\n";
  BR.wait();
  cout<<"TH"<<m<<" let loose from BR\n";
}

Что касается ожидания, просто вызовите соединение для всех дескрипторов потока (если они уже завершились, функция немедленно вернется)

TH1.join();
TH2.join();
TH3.join();
person Nim    schedule 06.03.2012

В дополнение к области видимости boost::lock_guard в блоке вы также можете использовать boost::unique_lock, который может быть явно указан unlock():

boost::unique_lock<boost::mutex> ownlock(M);

cout<<"TH"<<m<<" locked mutex\n";
Wait_(15); //simple wait for few milliseconds

ownlock.unlock();

Это полезно, если вам нужно освободить мьютекс, прежде чем повторно захватить его позже.

Что касается объединения, просто вызовите join() для всех дескрипторов потоков по очереди.

person liwp    schedule 06.03.2012

Пусть это выходит за рамки:

void THfoo(int m){
    cout<<"TH"<<m<<" started and attempts locking M\n";
    {
       boost::lock_guard<boost::mutex> ownlock(M);

       cout<<"TH"<<m<<" locked mutex\n";
       Wait_(15); //simple wait for few milliseconds
    }

    // this TH needs to unlock the mutex before going to barrier BR

    cout<<"TH"<<m<<" unlocked mutex\n";
    cout<<"TH"<<m<<" going to BR\n";
    BR.wait();
    cout<<"TH"<<m<<" let loose from BR\n";
}
person Lightness Races in Orbit    schedule 06.03.2012

Если вы используете boost::mutex::scoped_lock вместо boost::lock_guard, у него есть метод unlock(). Если вы назовете это, то блокировка не будет пытаться повторно разблокировать в своем деструкторе. Таким образом, мне больше нравится код, чем размещение блокировки в отдельном блоке.

person RobH    schedule 06.03.2012
comment
Чтобы добавить немного: если вы вызываете unlock() для базового мьютекса, как вы это делаете в своем вопросе, unlock() будет вызываться снова, когда lock_guard выходит за рамки. Однако, если вы используете scoped_lock (или аналогичный) и вызываете для этого unlock(), он не будет пытаться разблокировать второй раз, когда выйдет за пределы области действия. - person RobH; 06.03.2012
comment
Я нахожу блок (и сопровождающий его отступ) намного более четким, чем чередующиеся вызовы функций - person Lightness Races in Orbit; 06.03.2012

cout<<"TH"<<m<<" started and attempts locking M\n";
{
    boost::lock_guard<boost::mutex> ownlock(M);

    cout<<"TH"<<m<<" locked mutex\n";
    Wait_(15); //simple wait for few milliseconds

} //boost::lock_guard<boost::mutex> ~ownlock(M);
// this TH needs to unlock the mutex before going to barrier BR

cout<<"TH"<<m<<" unlocked mutex\n";

Пока вы выполняете join все потоки, единственная проблема, когда TH2 завершается первым, заключается в том, что TH1 должен завершить работу до того, как TH2 сможет быть «пожинать» join, а любые оставшиеся ресурсы, такие как возвращаемое значение, будут освобождены. Не стоит беспокоиться о 3 потоках. Если это использование памяти было проблемой, вы могли бы использовать timed_join для многократного повторения всех потоков по очереди.

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

person Steve Jessop    schedule 06.03.2012
comment
Меня больше беспокоят M и BR, которые не будут доступны для остальных TH после выхода из main(). Итак, проблема в том, как сделать это правильно, чтобы никогда не было риска сбоя — и я полагаю, что ваш timed_join во всех потоках является ответом, верно? - person P Marecki; 06.03.2012
comment
@PMarecki: Если вы не можете придумать конкретную причину не делать этого, просто сделайте TH1.join(); TH2.join(); TH3.join();. Тогда main не вернется, пока работает какой-либо из потоков (хотя может кинуть). Вы должны планировать либо присоединение, либо отсоединение каждого потока, который вы запускаете. Деструктор boost::thread в любом случае отсоединит их, что, я думаю, означает, что они должны быть завершены при выходе до того, как глобальные объекты будут уничтожены. Но если вы видите иначе, возможно, я ошибаюсь. - person Steve Jessop; 06.03.2012