Как безопасно прочитать переменную из одного потока и изменить ее из другого? - PullRequest
4 голосов
/ 11 января 2011

У меня есть экземпляры класса, которые используются в нескольких потоках. Я обновляю несколько переменных-членов из одного потока и читаю одни и те же переменные-члены из одного потока. Как правильно поддерживать безопасность потока?

eg:
  phthread_mutex_lock(&mutex1)
    obj1.memberV1 = 1;
  //unlock here?

Должен ли я разблокировать мьютекс здесь? (Если другой поток обращается к переменным-членам obj1 1 и 2 сейчас, доступ к данным может быть неправильным, потому что memberV2 еще не обновлен. Однако, если я не снимаю блокировку, другой поток может заблокировать, потому что операция занимает много времени ниже.

 //perform some time consuming operation which must be done before the assignment to memberV2 and after the assignment to memberV1
    obj1.memberV2 = update field 2 from some calculation
 pthread_mutex_unlock(&mutex1) //should I only unlock here?

Спасибо

Ответы [ 5 ]

1 голос
/ 11 января 2011

Трудно сказать, что вы делаете, что вызывает проблемы. Шаблон мьютекса довольно прост. Вы блокируете мьютекс, получаете доступ к общим данным, разблокируете мьютекс. Это защищает данные, потому что мьютекс позволит только одному потоку получить блокировку одновременно. Любой поток, который не может получить блокировку, должен ждать, пока мьютекс не будет разблокирован. Разблокировка будит официантов. Затем они будут бороться, чтобы получить замок. Проигравшие возвращаются ко сну. Время, необходимое для пробуждения, может составлять несколько мс или более с момента снятия блокировки. Убедитесь, что вы всегда разблокируете мьютекс в конце концов.

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

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

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

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

Если вы действительно заботитесь о производительности, попробуйте сначала использовать атомарные операции. Посмотрите на мьютексы в качестве последнего средства. Большинство проблем параллелизма легко решаются с помощью атомарных операций (атомарные операции Google gcc, чтобы начать обучение), и очень немногие проблемы действительно нуждаются в мьютексах. Мьютексы намного медленнее.

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

Часто полезно иметь возможность запускать ваше приложение как с 1, так и с N потоками. Таким образом, вы можете легче отладить условия гонки.

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

Функции, которые в конечном итоге записывают в общие переменные, должны использовать временные переменные до последнего момента, а затем копировать результаты. Компилятор не только сгенерирует лучший код, но и доступ к разделяемым переменным, особенно их изменение, приведет к обновлению строки кэша между L2 и главным ОЗУ и ко многим другим проблемам с производительностью. Опять же, если вы не заботитесь о производительности, не обращайте на это внимания. Однако я рекомендую вам Google документ "все, что программист должен знать о памяти", если вы хотите узнать больше.

Если вы читаете одну переменную из общих данных, вам, вероятно, нет необходимости блокировать ее, если переменная является целочисленным типом, а не членом битового поля (члены битового поля читаются / записываются с несколькими инструкциями). Читайте об атомных операциях. Когда вам нужно иметь дело с несколькими значениями, тогда вам нужна блокировка, чтобы убедиться, что вы не прочитали версию A с одним значением, получить прерванный, а затем прочитать версию B следующего значения. То же самое относится и к письму.

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

Так что, возможно, вы хотите сделать следующее:

lock the mutex
  Make a copy of the input data to the long running calculation.
unlock the mutex

L1: Do the calculation

Lock the mutex
   if the input data has changed and this matters
     read the input data, unlock the mutex and go to L1
   updata data
unlock mutex

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

веселит.

1 голос
/ 11 января 2011

Возможно, было бы лучше сделать что-то вроде:

//perform time consuming calculation
pthread_mutex_lock(&mutex1)
  obj1.memberV1 = 1;
  obj1.memberV2 = result;
pthread_mutex_unlock(&mutex1)

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

1 голос
/ 11 января 2011

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

0 голосов
/ 11 января 2011

Что бы я сделал, это отделил расчет от обновления:

temp = some calculation
pthread_mutex_lock(&mutex1);
obj.memberV1 = 1;
obj.memberV2 = temp;
pthread_mutex_unlock(&mutex1);
0 голосов
/ 11 января 2011

Наверное, лучше всего это сделать:

temp = //perform some time consuming operation which must be done before the assignment to memberV2

pthread_mutex_lock(&mutex1)
obj1.memberV1 = 1;
obj1.memberV2 = temp; //result from previous calculation
pthread_mutex_unlock(&mutex1) 
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...