Когда необходимо реализовать блокировку при использовании pthreads в C ++? - PullRequest
0 голосов
/ 08 апреля 2009

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

Следующий псевдокод, смутно представляющий мое решение очень простым способом.

std::map<int, MyType1> myMap;

void firstFunctionRunFromThread1()
{
    MyType1 mt1;
    mt1.Test = "Test 1";
    myMap[0] = mt1;
}

void onlyFunctionRunFromThread2()
{
    MyType1 &mt1 = myMap[0];
    std::cout << mt1.Test << endl; // Prints "Test 1"
    mt1.Test = "Test 2";
}

void secondFunctionFromThread1()
{
    MyType1 mt1 = myMap[0];
    std::cout << mt1.Test << endl; // Prints "Test 2"
}

Я совсем не уверен, как реализовать блокировку, и даже не уверен, почему я должен это делать (обратите внимание, что реальное решение намного сложнее). Может кто-нибудь объяснить, как и почему я должен реализовать блокировку в этом сценарии?

Ответы [ 6 ]

2 голосов
/ 08 апреля 2009

На самом деле проблема не только в блокировке ...

Если вы действительно хотите, чтобы поток два ВСЕГДА печатал «Тест 1», тогда вам нужна переменная условия.

Причина в том, что есть состояние гонки. Независимо от того, создаете ли вы поток 1 до потока 2, возможно, что код потока 2 может быть выполнен до потока 1, и поэтому карта не будет правильно инициализирована. Чтобы никто не считывал данные с карты, пока она не была инициализирована, вам нужно использовать переменную условия, которую изменяет поток 1.

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

Вот концептуальный пример, который поможет вам подумать об этом:

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

Предположим, что метод удаления реализован следующим образом: создайте временный ptr, чтобы он указывал на второй элемент в списке, сделайте заголовок равным нулю, затем сделайте заголовок временным ptr ...

Что делать, если происходит следующая последовательность событий: -T1 удаляет головы рядом с вторым элементом - T2 пытается прочитать второй элемент, НО второго элемента нет, потому что следующий ptr головы был изменен -T1 завершает удаление головы и устанавливает 2-й элемент в качестве головы

Сбой чтения T2, потому что T1 не использовал блокировку, чтобы сделать удаление из связанного списка атомарным!

Это надуманный пример, и он не обязательно должен реализовывать операцию удаления; однако, это показывает, почему блокировка необходима: это необходимо, чтобы операции, выполняемые с данными, были атомарными. Вы не хотите, чтобы другие потоки использовали что-то в несогласованном состоянии.

Надеюсь, это поможет.

2 голосов
/ 08 апреля 2009

Одна функция (то есть нить) изменяет карту, две читают ее. Поэтому чтение может быть прервано записью или наоборот, в обоих случаях карта, вероятно, будет повреждена. Вам нужны замки.

1 голос
/ 08 апреля 2009

Как правило, потоки могут работать на разных процессорах / ядрах с разными кэшами памяти. Они могут работать на одном и том же ядре с одним прерыванием («вытеснение» другого). Это имеет два последствия:

1) У вас нет возможности узнать, будет ли один поток прерываться другим в процессе выполнения чего-либо. Таким образом, в вашем примере нет никакого способа быть уверенным, что thread1 не будет пытаться прочитать строковое значение до того, как thread2 его запишет, или даже что, когда thread1 его читает, оно находится в «согласованном состоянии». Если он не находится в согласованном состоянии, то его использование может сделать что угодно.

2) Когда вы пишете в память в одном потоке, невозможно сказать, увидит ли это изменение код, запущенный в другом потоке. Изменение может находиться в кэше потока записи и не быть сброшенным в основную память. Он может быть сброшен в основную память, но не попадет в кэш потока чтения. Часть изменений может произойти, а часть - нет.

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

Правильное использование блокировки гарантирует, что все изменения сбрасываются через кэши, поэтому код видит память в том состоянии, которое, как вы думаете, он должен видеть. Это также позволяет вам контролировать, могут ли отдельные биты кода выполняться одновременно и / или прерывать друг друга.

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

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

Самый простой способ сделать это - создать мьютекс, который "защищает" ваши данные. Вы сами решаете, какие данные вы защищаете, и любой код, который читает или записывает данные, должен содержать мьютекс, пока он это делает. Итак, сначала вы блокируете, затем читаете и / или записываете данные, затем разблокируете. Это гарантирует непротиворечивое состояние, но само по себе не гарантирует, что thread2 получит возможность что-либо сделать вообще между двумя различными функциями thread1.

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

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

1 голос
/ 08 апреля 2009

Вся идея состоит в том, чтобы предотвратить переход программы в неопределенное / небезопасное состояние из-за того, что несколько потоков обращаются к одному и тому же ресурсу и / или обновляют / модифицируют ресурс, так что последующее состояние становится неопределенным. Читайте о Мьютексы и Блокировка (с примерами).

0 голосов
/ 09 апреля 2009

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

0 голосов
/ 08 апреля 2009

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

Тест 1

Тест 1

Хуже того, один поток может быть прерван в середине назначения - если назначение не является атомарной операцией. В этом случае давайте представим atomic как наименьшую единицу работы, которая не может быть разделена дальше.

Чтобы создать логически атомарный набор инструкций - даже если они в действительности дают несколько инструкций машинного кода - это использовать lock или mutex . Mutex означает «взаимное исключение», потому что это именно то, что он делает. Он обеспечивает эксклюзивный доступ к определенным объектам или критическим разделам кода.

Одной из основных проблем при работе с мультипрограммированием является определение критических разделов. В этом случае у вас есть два критических раздела: где вы назначаете myMap и где вы меняете myMap [0]. Поскольку вы не хотите читать myMap перед записью в него, это также * критический раздел.

...