#c #mutex
#c #мьютекс
Вопрос:
Я пытаюсь изучить основы многопоточности. Приведенный ниже пример взят из книги «Искусство многопроцессорного программирования», которая имеет (на Java):
class Counter
{
private long value;
private Lock lock;
public long getAndIncrement()
{
lock.lock();
try
{
long temp = value;
value = temp 1;
return temp;
}
finally
{
lock.unlock();
}
}
}
конец кода java. В книге говорится, что «блокировка () и разблокировка ()», показанные выше, добавят взаимное исключение к этой реализации общего счетчика. (Я не могу сам запустить этот Java-код, а также считаю, что «значение» должно быть возвращено, а не «temp».)
когда я попробовал этот простой пример (на C , который я могу попробовать), мой мьютекс ничего не делает. Я могу удалить его, результаты будут теми же. код на c :
#include <iostream>
#include <thread>
#include <mutex>
class Counter
{
public:
Counter(int v)
: value(v)
{}
int getAndIncrement()
{
std::lock_guard<std::mutex> lk(m);
return value ;
}
void show() const
{
std::cout << "value = " << value << 'n';
}
private:
int value;
std::mutex m;
};
int main()
{
Counter c1(1);
std::thread t1(amp;Counter::getAndIncrement, amp;c1);
t1.detach();
c1.show(); // 1 or 2
std::thread t2(amp;Counter::getAndIncrement, amp;c1);
t2.detach();
c1.show(); // 1 or 2
//int n = c1.getAndIncrement();
//std::cout << n << 'n'; // 1 or 2 or 3. never 4
c1.getAndIncrement();
c1.show(); //2, 3 and sometimes 4
}
Комментарии показывают мои результаты, когда я запускаю код. Мой мьютекс не имеет никакого эффекта.
Любое руководство будет оценено.
Спасибо.
Комментарии:
1. какой результат вы ожидали и почему? Я не понимаю, какой эффект мьютекса вы ожидаете увидеть в этом коде
2. Я ожидаю 4. Потому что я думаю, что каждый поток, который вводит «getAndIncrement ()», должен сначала заблокировать (и заблокировать другие потоки, входящие одновременно), выполнить операцию , затем разблокировать ее, чтобы следующий поток мог сделать то же самое.
3. для запуска потоков требуется время. Вполне возможно, что вы вызываете
show
его еще до запуска потока.4. @mhm Нет никакой гарантии, что эти потоки получат мьютекс в каком-либо определенном порядке. Все гарантии мьютекса заключаются в том, что никакие два потока не будут выполняться
value
и вычислять возвращаемые значения одновременно или перекрываться.5. Похоже, он работает отлично. Какое влияние вы ожидали, что этого не происходит? Основываясь на вашем комментарии, вы ожидаете
main()
, что поток прочитает значение последним (после выполнения всех приращений). Но вы ничего не сделали, чтобы предотвратить запуск основного потока и вывести значения до того, как другие потоки что-либо сделают. Потоки могут увеличить счетчик некоторое время спустя. Вполне возможно, что этот код будет печататься0
каждый раз.
Ответ №1:
Эффект мьютекса должен заключаться в том, чтобы убедиться, что никакие два потока одновременно не читают или не записывают в элемент (нормально только чтение). В вашем коде это не так, потому что в то же время один поток заблокировал мьютекс и записывает в элемент, который может вызвать основной поток show
. Вам необходимо защитить любой доступ:
void show() const
{
std::lock_guard<std::mutex> lk(m);
std::cout << "value = " << value << 'n';
}
Тогда ваш код в порядке, и результат, который вы наблюдаете, следует ожидать.
Вы запускаете 2 потока и не можете знать, сначала ли эти потоки записывают или сначала основные чтения из члена.
Возможная последовательность:
thread t1 writes
main calls show 2
main calls show 2
main increments
main calls show 3
thread t2 writes
другая возможная последовательность:
main calls show 1
main calls show 1
thread t2 writes
thread t1 writes
main increments
main calls show 4
Мьютекс защищает только одновременный доступ к члену. Вы можете быть уверены, что это произойдет в порядке:
main calls show
main calls show
main increments
main calls show
Если вы хотите, чтобы что-то происходило в определенном порядке также между разными потоками, вам нужно добавить какую-то синхронизацию (например. переменная условия). Хотя для вашего простого примера это в основном соответствовало бы отсутствию параллелизма, а добавление потоков просто добавило бы накладных расходов.
Таким образом, конечное значение «value» не должно быть равно 4
Давайте удалим detach
буквы s из вашего кода и добавим некоторые join
буквы s:
int main()
{
Counter c1(1);
std::thread t1(amp;Counter::getAndIncrement, amp;c1);
c1.show(); // 1 or 2
std::thread t2(amp;Counter::getAndIncrement, amp;c1);
c1.show(); // 1 or 2 or 3
c1.getAndIncrement();
c1.show(); // 2, 3 or 4
t1.join();
t2.join();
c1.show(); // definitely 4 !!!
}
join
ожидает завершения потока. То есть: мы можем быть уверены, что к моменту join
возврата поток уже вернулся из getAndIncrement
. Последний show
определенно печатает 4
. В более простом примере:
int main()
{
Counter c1(1);
std::thread t1(amp;Counter::getAndIncrement, amp;c1);
c1.show(); // 1 or 2
t1.join();
c1.show(); // 2
}
PS: Я все еще не совсем уверен, в чем на самом деле заключается / заключалось ваше недоразумение. Учтите, что многопоточность — это не просто однопоточность потоки. Это часть дизайна. Часто должны / должны использоваться разные алгоритмы или структуры данных.
Комментарии:
1. Таким образом, конечное значение «value» не должно быть 4? Потому что вызываются операции 3x ..
2. В этом случае я бы рекомендовал оставить ввод-вывод вне охраняемой зоны:
void show() const { int valueLocal; { std::lock_guard<std::mutex> lk(m); valueLocal = value;} std::cout << "value = " << valueLocal << 'n'; }
3. final равен 4 даже при отсоединении вместо присоединения? @fabian почему?
4. @mhm вы хотите сделать как можно меньше в критических разделах, чтобы свести к минимуму время, необходимое другим потокам для ожидания снятия блокировки. Копирование значения поля выполняется быстро, запись его в стандартный вывод выполняется не так быстро, поэтому предпочтительно уничтожить значение
lock_guard
перед записью.5. Спасибо вам всем. @fabian Спасибо.