2012-01-06 4 views
1

Вот что такое: есть массив с поплавком float bucket[5] и 2 потока, например thread1 и thread2., когда использовать мьютекс

Thread1 отвечает за заправку bucket, присваивая каждому элементу bucket случайное число. Когда ковш заправляется, thread2 будет получать доступ к bucket и читать его элементы.

Вот как я делаю работу:

float bucket[5]; 
pthread_mutex_t mu = PTHREAD_MUTEX_INITIALIZER; 
pthread_t thread1, thread2; 

void* thread_1_proc(void*); //thread1's startup routine, tank up the bucket 
void* thread_2_proc(void*); //thread2's startup routine, read the bucket 

int main() 
{ 
    pthread_create(&thread1, NULL, thread_1_proc, NULL); 
    pthread_create(&thread2, NULL, thread_2_proc, NULL); 
    pthread_join(thread1); 
    pthread_join(thread2); 
} 

Ниже моя реализация для thread_x_proc:

void* thread_1_proc(void*) 
{ 
    while(1) { //make it work forever 
     pthread_mutex_lock(&mu); //lock the mutex, right? 

     cout << "tanking\n"; 
     for(int i=0; i<5; i++) 
      bucket[i] = rand(); //actually, rand() returns int, doesn't matter 

     pthread_mutex_unlock(&mu); //bucket tanked, unlock the mutex, right? 

     //sleep(1); /* this line is commented */ 
    } 
} 

void* thread_2_proc(void*) 
{ 
    while(1) { 
     pthread_mutex_lock(&mu); 

     cout << "reading\n"; 
     for(int i=0; i<5; i++) 
      cout << bucket[i] << " "; //read each element in the bucket 

     pthread_mutex_unlock(&mu); //reading done, unlock the mutex, right? 

     //sleep(1); /* this line is commented */ 
    } 
} 

Вопрос

мое право реализации? Потому что результат не такой, какой я ожидал.

... 
reading 
5.09434e+08 6.58441e+08 1.2288e+08 8.16198e+07 4.66482e+07 7.08736e+08 1.33455e+09 
reading 
5.09434e+08 6.58441e+08 1.2288e+08 8.16198e+07 4.66482e+07 7.08736e+08 1.33455e+09 
reading 
5.09434e+08 6.58441e+08 1.2288e+08 8.16198e+07 4.66482e+07 7.08736e+08 1.33455e+09 
reading 
tanking 
tanking 
tanking 
tanking 
... 

Но если я раскомментировать sleep(1); в каждой функции thread_x_proc, выход правильно, tanking и reading следуют друг за другом, как это:

... 
tanking 
reading 
1.80429e+09 8.46931e+08 1.68169e+09 1.71464e+09 1.95775e+09 4.24238e+08 7.19885e+08 
tanking 
reading 
1.64976e+09 5.96517e+08 1.18964e+09 1.0252e+09 1.35049e+09 7.83369e+08 1.10252e+09 
tanking 
reading 
2.0449e+09 1.96751e+09 1.36518e+09 1.54038e+09 3.04089e+08 1.30346e+09 3.50052e+07 
... 

Почему? Должен ли я использовать sleep() при использовании mutex?

ответ

4

Ваш код технически правильный, но он не имеет большого смысла, и он не делает то, что вы предполагаете.

Что ваш код делает, он обновляет раздел данных атомарно и читает из этого раздела атомарно. Однако вы не знаете, в каком порядке это происходит, и как часто данные записываются до чтения (или, если вообще!).

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

В общем, ответ на вопрос «когда я должен использовать мьютекс» «никогда, если вы можете помочь». Потоки должны отправлять сообщения, а не предоставлять общий доступ. Это делает ненужным мьютекс и предлагает параллелизм (который является основным стимулом для использования потоков в первую очередь).

Мьютекс заставляет ваши потоки запускаться затвором, поэтому вы можете просто запустить только в одном потоке.

+0

Это не технически корректно, так как его поведение не определено (он может танковать два раза подряд, например, «не должен»). – Valmond

+0

Вы правы, спасибо. – Alcott

+0

Я бы сказал, ответ на вопрос «когда использовать мьютексы?» «только тогда, когда вам нужно защитить приоритет инверсии». Я думаю, что во всех остальных случаях есть лучшие способы оценки проблемы. – elmo

1

Если у вас есть две функции, которые должны выполняться последовательно, то есть F1 должен завершиться до начала F2, тогда вы не должны использовать два потока. Запустите F2 в том же потоке, что и F1, после того, как F1 вернется.

Без потоков вам также не нужны мьютексы.

+0

ну, но я хочу, чтобы f1 работал навсегда, более того, f1 и f2 обмениваются данными, правильно? – Alcott

+0

Ни «вечно», ни «общие данные» действительно не имеют значения: 'for (;;) {f1 (data); f2 (данные); } '. Или в стиле OO: 'for (;;) {object.f1(); object.f2(); } '. Все еще нет мьютекса. – MSalters

+0

да, это так. – Alcott

0

На самом деле это не проблема.

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

мьютекс для защиты данных, поэтому два потока не: а) писать одновременно б) один пишет, когда другой читает

Это не делает нити работать в определенном порядке (если вы хотите эту функциональность, зацепите резьбовой подход или используйте флаг, чтобы сказать, что «бак» заполнен, например).

+0

Любой другой способ без использования потоков? – Alcott

+1

Без потоков вы просто: while (true) {Fill_Bucket(); Empty_Bucket();} – Valmond

2

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

Дело, которое вы представили, относится скорее к семафору, который «отправляется» с каждым добавленным элементом.

Однако, если он всегда должен быть как:

  • записи 5 элементов
  • чтения 5 элементов

вы должны иметь два семафоры:

  • один, что производитель блоков пока потребитель не закончит
  • тот, который b замки потребителя до производителя закончил

Так что код должен выглядеть примерно так:

Producer: 
    while(true){ 
     lock(&write_mutex) 
     [insert data] 
     unlock(&read_mutex) 
    } 

Consumer: 
    while(true){ 
     lock(&read_mutex) 
     [insert data] 
     unlock(&write_mutex) 
    } 

write_mutex Первоначально должен быть разблокирован и read_mutex запертой.

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

2

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

Почему не следует запускать thread2 перед thread1? И почему каждая нить не должна заканчивать цикл несколько раз, прежде чем другой поток получит шанс запустить до линии, где он получает мьютекс?

Если вы хотите, чтобы выполнение переключалось между двумя потоками предсказуемым образом, вам нужно использовать семафор, переменную условия или другой механизм обмена сообщениями между двумя потоками. sleep, похоже, приводит к тому, что вы хотите по этому поводу, но даже со сном вы сделали недостаточно, чтобы гарантировать, что они будут чередоваться. И я понятия не имею, почему sleep имеет значение для того, какой поток запускается для запуска в первую очередь - совместим ли это с несколькими прогонами?

+0

После нескольких тестов это согласовано, но ваше объяснение, по-видимому, является правильным. Upvoted. – Alcott

+0

@Alcott: может быть, ваше первоначальное недоразумение состояло в том, чтобы подумать, что 'pthread_mutex_unlock' означает« дать мьютекс другой теме ». Это не так, он просто выпускает мьютексы, чтобы этот поток или любой другой мог его приобрести в будущем. Пока другой поток не получит шанс запустить, он будет таким, поэтому разница, которую делает 'sleep', - это просто позволить запустить что-то еще. С помощью сон потоки будут получать мьютекс попеременно при условии, что они будут пробуждаться поочередно, что, скорее всего, не гарантируется, так как 'sleep (1)' не обязательно ровно 1 секунда каждый раз. –

+0

Да, спасибо,:) – Alcott

0

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

/* A flag that indicates whose turn it is. */ 
char tanked = 0; 

void* thread_1_proc(void*) 
{ 
    while(1) { //make it work forever 
     pthread_mutex_lock(&mu); //lock the mutex 
     if(!tanked) { // is it my turn? 
      cout << "tanking\n"; 
      for(int i=0; i<5; i++) 
       bucket[i] = rand(); //actually, rand() returns int, doesn't matter 
      tanked = 1; 
     } 
     pthread_mutex_unlock(&mu); // unlock the mutex 
    } 
} 

void* thread_2_proc(void*) 
{ 
    while(1) { 
     pthread_mutex_lock(&mu); 
     if(tanked) { // is it my turn? 
      cout << "reading\n"; 
      for(int i=0; i<5; i++) 
       cout << bucket[i] << " "; //read each element in the bucket 
      tanked = 0; 
     } 
     pthread_mutex_unlock(&mu); // unlock the mutex 
    } 
} 

Вышеприведенный код должен работать должным образом. Однако, как указывали другие, результат был бы лучше выполнен с одним из этих двух других вариантов:

  • Последовательное. Поскольку производитель и потребитель должны чередоваться, вам не нужны два потока. Одного цикла, который танков, а затем будет читать, будет достаточно. Это решение также позволит избежать оживленного ожидания, которое происходит в коде выше.
  • Использование семафоров. Это было бы решением, если бы продюсер мог работать несколько раз подряд, накапливая элементы в ведро (не в случае с исходным кодом). http://en.wikipedia.org/wiki/Producer-consumer_problem#Using_semaphores
+0

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

+0

@SteveJessop Это формально неверно, именно потому, что ожидание оживает. Но я согласен, что блокировка с двойным проверкой не улучшает код здесь (из-за оживленного ожидания и потому, что есть только один производитель и один потребитель), поэтому я удалил его. – igorrs

+0

Это формально некорректно, потому что нет гарантии, что запись «забита» в одном потоке * всегда видна потоком, который выполняет 'while (1) if (tanked) {anything; } '. «ничего» никогда не выполняется и, следовательно, никогда не вводит какой-либо барьер памяти. На практике большинство архитектур распространяют записи в любом случае (через когерентность кэш-памяти или в худшем случае, когда поток выполняется), но AFAIK не гарантирует в C++ 11 или Posix, что это будет, или что оптимизатор не сделает что-то, что делает его не имеет значения, что делает оборудование. –

Смежные вопросы