Поведение streamа мьютексов

Я изучаю C. Я пишу приложение с несколькими streamами; Я знаю, что когда переменная распределяется между двумя или более streamами, лучше блокировать / разблокировать с помощью мьютекса, чтобы избежать взаимоблокировки и несогласованности переменных. Это очень ясно, когда я хочу изменить или просмотреть одну переменную.

int i = 0; /** Global */ static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; /** Thread 1. */ pthread_mutex_lock(&mutex); i++; pthread_mutex_unlock(&mutex); /** Thread 2. */ pthread_mutex_lock(&mutex); i++; pthread_mutex_unlock(&mutex); 

Я думаю, это правильно. Переменная i , в конце выполнения, содержит целое число 2 .
Во всяком случае, есть некоторые ситуации, в которых я не знаю точно, где положить два вызова функций.

Например, предположим, что у вас есть функция obtain() , которая возвращает глобальную переменную. Мне нужно вызвать эту функцию из двух streamов. У меня также есть два других streamа, которые вызывают функцию set() , определенную несколькими аргументами; эта функция будет устанавливать одну и ту же глобальную переменную. Эти две функции необходимы, когда вам нужно что-то сделать до получения / установки var.

 /** (0) */ /** Thread 1, or 2, or 3... */ if(obtain() == something) { if(obtain() == somethingElse) { // Do this, sometimes obtain() and sometimes set(random number) (1) } else { // Do that, just obtain(). (2) } } else { // Do this and do that (3) // If # of thread * 3 > 10, then set(3*10) For example. (4) } /** (5) */ 

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

Заранее спасибо.
-Alberto

    Без какой-либо защиты:

    Операционная система может прерывать каждый из ваших streamов в любое время и передавать процессор другому. «Anytime» включает «между двумя инструкциями по сборке, которые фактически исходят от одной и той же команды C».

    Теперь предположим, что ваша переменная занимает 64 бита в 32-битном процессоре. Это означает, что ваша переменная занимает два «слова» процессора. Чтобы записать его, процессор нуждается в двух инструкциях по сборке. То же самое для чтения. Если stream прерывается между двумя, у вас возникают проблемы.

    Чтобы дать более ясный пример, я буду использовать аналогию двух десятичных цифр для представления двух бинарных 32-битных слов. Так скажите, что вы увеличиваете двузначное десятичное число в 1-значном процессоре. Чтобы увеличить с 19 до 20, вы должны прочитать 19, выполнить математику, а затем написать 20. Чтобы написать 20, вы должны написать 2, затем написать 0 (или наоборот). Если вы напишете 2, прервите его перед записью 0, число в памяти будет 29, что далеко не так, как было бы правильно. Затем другой stream начинает считывать неправильный номер.

    Даже если у вас есть одна цифра, все еще существует проблема чтения-изменения-записи, которую объяснил Бланк Ксавье.

    С мьютексом :

    Когда stream A блокирует мьютекс, stream A проверяет переменную mutex. Если он свободен, stream A записывает его как выполненный. Он делает это с помощью атомарной инструкции, одной команды сборки, поэтому нет «промежутка» для прерывания. Затем он переходит к увеличению с 19 по 20. Его все равно можно прервать во время неправильного значения переменной 29, но это нормально, потому что теперь никто не может получить доступ к переменной. Когда stream B пытается заблокировать мьютекс, он проверяет переменную mutex, она берется. Таким образом, stream B знает, что он не может коснуться переменной. Затем он вызывает операционную систему, говоря: «Сейчас я отказываюсь от процессора». Thread B повторит это, если он снова получит процессор. И опять. Пока нить A наконец не вернет процессор, закончит то, что он делает, затем разблокирует мьютекс.

    Итак, когда блокировать?

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

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

    Не совсем “всегда”

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

    Некоторые слова объяснения.

    В примере кода увеличивается одна переменная.

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

    Итак, мы читаем в нашем целом. Скажем, целое число имеет длину 8 байт, а строка кэша также 8 байтов (например, современный 64-разрядный процессор Intel). Чтение необходимо в этом случае, так как нам нужно знать исходное значение. Итак, чтение происходит, и строка кэша входит в кеш L3, L2 и L1 (Intel использует инклюзионный кеш, все в L1 присутствует в L2, все в L2 присутствует в L3 и т. Д.).

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

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

    Итак, представьте, что у нас есть два streamа, на разных процессорах. Оба они читаются в целочисленном виде. На этом этапе их кэши отмечают эту строку кэша как общую. Затем один из них пишет. У писателя будет строка его кеша, отмеченная как измененная, второй процессор имеет свою лини кэш-строки недействительным – и поэтому, когда он приходит, чтобы попытаться написать, что тогда происходит, он пытается снова прочитать целое число из памяти, но так как существует в другом Процессоры кэшируют модифицированную копию, он захватывает копию модифицированного значения из первого процессора, первый процессор имеет свою копию с недействительными, а теперь второй процессор записывает свое новое значение.

    Итак, пока все хорошо, как может быть, нам нужно блокировать?

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

    Таким образом, один из приращений был потерян.

    если у вас есть функция obtain() , должна быть функция release() , правильно? затем выполните блокировку в get () и разблокируйте в release ().

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