2011-01-17 2 views
3

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

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(), которая возвращает глобальную переменную. Мне нужно вызвать эту функцию из двух потоков. У меня есть еще два потока, которые вызывают функцию 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

ответ

10

Без защиты:

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

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

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

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

С мьютексом:

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

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

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

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

Не совсем «всегда»

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

+2

Кто-то, кто пишет ответы на этот полный и поучительный, должен иметь более 466 человек. +1! –

+1

+1 для очень четкого, продуманного и точного объяснения! –

+1

Спасибо. Этот ответ мне очень помог. – Donovan

0

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

3

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

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

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

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

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

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

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

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

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

Таким образом, один из этапов был утерян.

0

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

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