Этот простой мьютекс не имеет никакого эффекта

#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 Спасибо.