Перейти к содержимому

Какого типа блокировки не существует в программировании

  • автор:

Соображения по поводу программирования без использования блокировок для Xbox 360 и Microsoft Windows

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

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

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

Программирование с помощью блокировок

При написании многопотокового кода часто требуется совместное использование данных между потоками. Если несколько потоков одновременно считывают и записывают общие структуры данных, может произойти повреждение памяти. Самый простой способ решения этой проблемы — использовать блокировки. Например, если за один раз необходимо выполнить только один поток, то для этого можно использовать CRITICAL_SECTION, как показано в следующем коде:

// Initialize CRITICAL_SECTION cs; InitializeCriticalSection(&cs); // Use void ManipulateSharedData() < EnterCriticalSection(&cs); // Manipulate stuff. LeaveCriticalSection(&cs); >// Destroy DeleteCriticalSection(&cs); 

Этот код довольно прост и прост, и легко сказать, что он правильный. Однако программирование с блокировками имеет несколько потенциальных недостатков. Например, если два потока пытаются получить одинаковые две блокировки, но получить их в другом порядке, может возникнуть взаимоблокировка. Если программа удерживает блокировку слишком долго (из-за плохой структуры или из-за того, что поток был переключен потоком с более высоким приоритетом), другие потоки могут быть заблокированы на длительное время. Этот риск особенно велик для Xbox 360, так как программные потоки назначаются разработчиком аппаратным потоком, и операционная система не перемещает их в другой аппаратный поток, даже если один из них простаивает. Xbox 360 также не имеет защиты от инверсии приоритета, когда поток с высоким приоритетом вращается в цикле, ожидая, пока низкоприоритетный поток отпустит блокировку. Наконец, если вызов отложенной процедуры или прерывание служебной процедуры пытается получить блокировку, может возникнуть взаимоблокировка.

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

Программирование без блокировки

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

При программировании без блокировки необходимо решить две проблемы: неатомные операции и изменение порядка.

Неатомные операции

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

На всех современных процессорах можно предположить, что операции чтения и записи естественно выровненных собственных типов являются атомарными. Если шина памяти по крайней мере так же широка, как и считываемый или записываемый тип, ЦП считывает и записывает эти типы в одной транзакции шины, что делает невозможным для других потоков видеть их в состоянии наполовину завершено. В x86 и x64 нет гарантии, что операции чтения и записи размером более восьми байтов являются атомарными. Это означает, что 16-байтовые операции чтения и записи регистров расширения SIMD потоковой передачи (SSE) и строковых операций могут быть не атомарными.

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

Составные операции, такие как последовательность чтения, изменения и записи, которая происходит при приращении общей переменной, не являются атомарными. На Xbox 360 эти операции реализуются в виде нескольких инструкций (lwz, addi и stw), и поток можно переключить на части с помощью последовательности. В x86 и x64 существует одна инструкция (inc), которую можно использовать для увеличения переменной в памяти. Если вы используете эту инструкцию, увеличение переменной будет атомарным в однопроцессорных системах, но по-прежнему не является атомарным в многопроцессорных системах. Чтобы сделать inc атомарным в многопроцессорных системах на базе x86 и x64, необходимо использовать префикс блокировки, который не позволяет другому процессору выполнять собственную последовательность чтения, изменения и записи между операцией чтения и записи инструкции inc.

В коде ниже приведено несколько примеров:

// This write is not atomic because it is not natively aligned. DWORD* pData = (DWORD*)(pChar + 1); *pData = 0; // This is not atomic because it is three separate operations. ++g_globalCounter; // This write is atomic. g_alignedGlobal = 0; // This read is atomic. DWORD local = g_alignedGlobal; 

Обеспечение атомарности

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

  • Естественно атомарные операции
  • Блокировки для переноса составных операций
  • Функции операционной системы, реализующие атомарные версии популярных составных операций

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

// This will be atomic. g_globalCounter = 0; // This is not atomic and gives undefined behavior // if executed on multiple threads ++g_globalCounter; 

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

// Incrementing our variable in a safe lockless way. InterlockedIncrement(&g_globalCounter); 

Переупорядочение

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

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

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

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

В этом коде один поток добавляет новую запись в массив спрайтов:

// Create a new sprite by writing its position into an empty // entry and then setting the ‘alive' flag. If ‘alive' is // written before x or y then errors may occur. g_sprites[nextSprite].x = x; g_sprites[nextSprite].y = y; g_sprites[nextSprite].alive = true; 

В следующем блоке кода другой поток считывает данные из массива спрайтов:

// Draw all sprites. If the reads of x and y are moved ahead of // the read of ‘alive' then errors may occur. for( int i = 0; i < numSprites; ++i ) < if( g_sprites[nextSprite].alive ) < DrawSprite( g_sprites[nextSprite].x, g_sprites[nextSprite].y ); >> 

Чтобы обеспечить безопасность этой системы спрайтов, необходимо предотвратить изменение порядка операций чтения и записи в компиляторе и ЦП.

Общие сведения о переупорядочении операций записи на ЦП

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

Xbox 360

Хотя ЦП Xbox 360 не изменяет порядок инструкций, он изменяет порядок операций записи, которые выполняются после самих инструкций. Такое переупорядочивание операций записи специально разрешено моделью памяти PowerPC.

Операции записи на Xbox 360 не переходят непосредственно в кэш L2. Вместо этого, чтобы улучшить пропускную способность записи кэша уровня 2, они проходят через очереди хранилища, а затем для буферов хранения и сбора. Буферы store-gather позволяют записывать 64-байтовые блоки в кэш L2 за одну операцию. Существует восемь буферов сбора хранилища, которые позволяют эффективно записывать данные в несколько различных областей памяти.

Буферы store-gather обычно записываются в кэш L2 в порядке fifo. Однако если целевая строка кэша записи не находится в кэше L2, эта запись может быть отложена, пока строка кэша извлекается из памяти.

Даже если буферы store-gather записываются в кэш L2 в строгом порядке FIFO, это не гарантирует, что отдельные операции записи записываются в кэш L2 по порядку. Например, представьте, что ЦП выполняет запись в расположение 0x1000, затем в расположение 0x2000, а затем в расположение 0x1004. Первая запись выделяет буфер store-gather и помещает его в начало очереди. Вторая операция записи выделяет другой буфер store-gather и помещает его в очередь. Третья запись добавляет свои данные в первый буфер store-gather, который остается в передней части очереди. Таким образом, третья запись попадает в кэш L2 перед второй записью.

Изменение порядка, вызванное буферами store-gather, принципиально непредсказуемо, особенно потому, что оба потока в ядре совместно используют буферы store-gather, что делает выделение и очистку буферов store-gather очень изменчивым.

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

x86 и x64

Несмотря на то, что процессоры x86 и x64 переупорядают инструкции, они, как правило, не меняют порядок операций записи по сравнению с другими операциями записи. Существуют некоторые исключения для объединенной памяти для записи. Кроме того, строковые операции (MOVS и STOS) и 16-байтовые операции записи SSE могут быть внутренне переупорядочены, но в противном случае операции записи не будут переупорядочены относительно друг друга.

Общие сведения о переупорядочении операций чтения на ЦП

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

Xbox 360

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

x86 и x64

Хотя процессоры x86 и x64 выполняют переупорядочение инструкций, они, как правило, не меняют порядок операций чтения по сравнению с другими операциями чтения. Строковые операции (MOVS и STOS) и 16-байтовые операции чтения SSE могут быть внутренне переупорядочены, но в противном случае операции чтения не будут переупорядочены относительно друг друга.

Другие изменения порядка

Хотя процессоры x86 и x64 не меняют порядок операций записи относительно других операций записи или переупорядочение операций чтения относительно других операций чтения, они могут изменять порядок операций чтения относительно операций записи. В частности, если программа выполняет запись в одно расположение, после чего выполняется чтение из другого расположения, данные чтения могут поступать из общей памяти до того, как записанные данные будут в нем. Такое изменение порядка может нарушить некоторые алгоритмы, например алгоритмы взаимного исключения Деккера. В алгоритме Деккера каждый поток устанавливает флаг, указывающий, что он хочет войти в критическую область, а затем проверяет флаг другого потока, чтобы узнать, находится ли другой поток в критической области или пытается войти в него. Ниже приведен исходный код.

volatile bool f0 = false; volatile bool f1 = false; void P0Acquire() < // Indicate intention to enter critical region f0 = true; // Check for other thread in or entering critical region while (f1) < // Handle contention. >// critical region . > void P1Acquire() < // Indicate intention to enter critical region f1 = true; // Check for other thread in or entering critical region while (f0) < // Handle contention. >// critical region . > 

Проблема заключается в том, что чтение f1 в P0Acquire может считываться из общего хранилища до того, как запись в f0 будет передана в общее хранилище. Между тем, чтение f0 в P1Acquire может считываться из общего хранилища, прежде чем запись в f1 будет передана в общее хранилище. Результатом является то, что оба потока устанавливают для своих флагов значение TRUE, и оба потока видят флаг другого потока как FALSE, поэтому они оба входят в критическую область. Таким образом, хотя проблемы с изменением порядка в системах на базе x86 и x64 встречаются реже, чем в Xbox 360, они, безусловно, могут возникнуть. Алгоритм Деккера не будет работать без аппаратных барьеров памяти на любой из этих платформ.

Процессоры x86 и x64 не будут изменять порядок записи перед предыдущим чтением. Процессоры x86 и x64 переупорядочены только перед предыдущими операций записи, если они предназначены для разных расположений.

ЦП PowerPC могут изменять порядок операций чтения перед записью и изменять порядок операций записи перед чтением, если они находятся на разных адресах.

Сводка по переупорядочению

ЦП Xbox 360 переупорядочивее выполняет операции с памятью, чем процессоры x86 и x64, как показано в следующей таблице. Дополнительные сведения см. в документации по процессору.

Изменение порядка действий x86 и x64 Xbox 360
Операции чтения, движущиеся впереди операций чтения Нет Да
Операции записи, движущиеся впереди операций записи Нет Да
Операции записи, движущиеся впереди операций чтения Нет Да
Считывает, перемещаясь впереди операций записи Да Да

Read-Acquire и Write-Release барьеры

Конструкции main, используемые для предотвращения переупорядочения операций чтения и записи, называются барьерами чтения и получения и записи. Чтение и получение — это чтение флага или другой переменной для получения права владения ресурсом в сочетании с барьером для изменения порядка. Аналогичным образом, запись-выпуск — это запись флага или другой переменной, чтобы отказаться от владения ресурсом, в сочетании с барьером для переупорядочения.

Формальные определения, любезно Херб Саттер, являются следующими:

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

Когда код получает право владения некоторой памятью путем получения блокировки или извлечения элемента из общего связанного списка (без блокировки), всегда используется чтение — проверка флага или указателя, чтобы узнать, было ли приобретено владение памятью. Это чтение может быть частью операции InterlockedXxx , в этом случае она включает как чтение, так и запись, но это чтение, которое указывает, было ли приобретено право владения. После получения права владения памятью значения обычно считываются из нее или записываются в нее, и очень важно, чтобы эти операции чтения и записи выполнялись после получения права владения. Барьер чтения и получения гарантирует это.

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

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

// Read that acquires the data. if( g_flag ) < // Guarantee that the read of the flag executes before // all reads and writes that follow in program order. BarrierOfSomeSort(); // Now we can read and write the shared data. int localVariable = sharedData.y; sharedData.x = 0; // Guarantee that the write to the flag executes after all // reads and writes that precede it in program order. BarrierOfSomeSort(); // Write that releases the data. g_flag = false; >

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

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

Использование правильного барьера для предотвращения переупорядочения компилятора и ЦП для вашей платформы имеет решающее значение.

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

Предотвращение изменения порядка компилятора

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

С помощью Visual C++ можно предотвратить изменение порядка компилятора с помощью встроенного _ReadWriteBarrier компилятора. При вставке _ReadWriteBarrier в код компилятор не перемещает операции чтения и записи.

#if _MSC_VER < 1400 // With VC++ 2003 you need to declare _ReadWriteBarrier extern "C" void _ReadWriteBarrier(); #else // With VC++ 2005 you can get the declaration from intrin.h #include #endif // Tell the compiler that this is an intrinsic, not a function. #pragma intrinsic(_ReadWriteBarrier) // Create a new sprite by filling in a previously empty entry. g_sprites[nextSprite].x = x; g_sprites[nextSprite].y = y; // Write-release, barrier followed by write. // Guarantee that the compiler leaves the write to the flag // after all reads and writes that precede it in program order. _ReadWriteBarrier(); g_sprites[nextSprite].alive = true; 

В следующем коде другой поток считывает данные из массива спрайтов:

// Draw all sprites. for( int i = 0; i < numSprites; ++i ) < // Read-acquire, read followed by barrier. if( g_sprites[nextSprite].alive ) < // Guarantee that the compiler leaves the read of the flag // before all reads and writes that follow in program order. _ReadWriteBarrier(); DrawSprite( g_sprites[nextSprite].x, g_sprites[nextSprite].y ); >> 

Важно понимать, что _ReadWriteBarrier не вставляет никаких дополнительных инструкций и не мешает ЦП переупорядочить операции чтения и записи, а только не позволяет компилятору переупорядочить их. Таким образом, _ReadWriteBarrier достаточно при реализации барьера выпуска записи на x86 и x64 (поскольку x86 и x64 не переупорядочены записи, а обычная запись достаточно для снятия блокировки), но в большинстве других случаев необходимо также предотвратить переупорядочение операций чтения и записи ЦП.

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

Можно также использовать встроенные _ReadBarrier и _WriteBarrier для более точного управления изменением порядка компилятора. Компилятор не перемещает операции чтения по _ReadBarrier и не перемещает операции записи по _WriteBarrier.

Предотвращение переупорядочения ЦП

Переупорядочение ЦП является более тонким, чем изменение порядка компилятором. Вы никогда не видите, что это происходит напрямую, вы просто видите необъяснимые ошибки. Чтобы предотвратить переупорядочение операций чтения и записи ЦП, необходимо использовать инструкции по барьерам памяти на некоторых процессорах. Универсальное имя инструкции по барьеру памяти на Xbox 360 и Windows — MemoryBarrier. Этот макрос реализуется соответствующим образом для каждой платформы.

На Xbox 360 MemoryBarrier определяется как lwsync (упрощенная синхронизация), которая также доступна через встроенный __lwsync , который определен в ppcintrinsics.h. __lwsync также служит барьером памяти компилятора, предотвращая переупорядочивание операций чтения и записи компилятором.

Инструкция lwsync — это барьер памяти на Xbox 360, который синхронизирует одно ядро процессора с кэшем L2. Это гарантирует, что все операции записи до lwsync будут входить в кэш L2 перед последующими. Это также гарантирует, что все операции чтения, следующие за lwsync , не будут получать более старые данные из L2, чем предыдущие операции чтения. Единственный тип изменения порядка, который он не предотвращает, — это чтение перед записью на другой адрес. Таким образом, lwsync применяет упорядочение памяти, соответствующее упорядочению памяти по умолчанию для процессоров x86 и x64. Чтобы получить полный порядок памяти, требуется более ресурсоемкая инструкция синхронизации (также известная как тяжеловесная синхронизация), но в большинстве случаев это не требуется. Параметры изменения порядка памяти на Xbox 360 приведены в следующей таблице.

Изменение порядка xbox 360 Без синхронизации lwsync sync
Операции чтения, движущиеся впереди операций чтения Да Нет Нет
Операции записи опережают операции записи Да Нет Нет
Операции записи, движущиеся впереди операций чтения Да Нет Нет
Считывает впереди операций записи Да Да Нет

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

В Windows MemoryBarrier определяется в Winnt.h и предоставляет разные инструкции по барьерам памяти в зависимости от того, выполняется ли компиляция для x86 или x64. Инструкция по барьеру памяти служит полным барьером, предотвращая переупорядочение операций чтения и записи через барьер. Таким образом, MemoryBarrier в Windows дает более надежную гарантию изменения порядка, чем на Xbox 360.

На Xbox 360 и многих других ЦП существует еще один способ предотвращения операций чтения и переупорядочения ЦП. Если вы считываете указатель, а затем используете его для загрузки других данных, ЦП гарантирует, что операции чтения указателя не старше, чем чтение указателя. Если флаг блокировки является указателем и если все операции чтения общих данных находятся вне указателя, параметр MemoryBarrier можно опустить, чтобы скромная экономия производительности.

Data* localPointer = g_sharedPointer; if( localPointer ) < // No import barrier is needed--all reads off of localPointer // are guaranteed to not be reordered past the read of // localPointer. int localVariable = localPointer->y; // A memory barrier is needed to stop the read of g_global // from being speculatively moved ahead of the read of // g_sharedPointer. int localVariable2 = g_global; > 

Инструкция MemoryBarrier запрещает только переупорядочение операций чтения и записи в кэшируемую память. Если вы выделяете память как PAGE_NOCACHE или PAGE_WRITECOMBINE( распространенный метод для авторов драйверов устройств и разработчиков игр на Xbox 360), MemoryBarrier не влияет на доступ к этой памяти. Большинству разработчиков не требуется синхронизация не кэшируемой памяти. Это выходит за рамки данной статьи.

Переупорядочение взаимосвязанных функций и ЦП

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

На Xbox 360 функции InterlockedXxx не содержат барьеров памяти ЦП. Они предотвращают изменение порядка операций чтения и записи компилятором, но не переупорядочение ЦП. Поэтому в большинстве случаев при использовании функций InterlockedXxx на Xbox 360 следует предшествовать или следовать за ними с помощью __lwsync, чтобы сделать их барьером для чтения, приобретения или записи и выпуска. Для удобства и удобства чтения существуют версии получения и выпуска многих функций InterlockedXxx . Они поставляются со встроенным барьером памяти. Например, InterlockedIncrementAcquire выполняет межблокированную приращение, за которой следует __lwsync барьер памяти, чтобы предоставить полную функциональность чтения и получения.

Рекомендуется использовать версии получения и выпуска функций InterlockedXx (большинство из которых также доступны в Windows без снижения производительности), чтобы сделать намерение более очевидным и упростить получение инструкций по барьерам памяти в правильном месте. Любое использование InterlockedXxx на Xbox 360 без барьера памяти следует тщательно изучить, так как это часто является ошибкой.

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

// Declarations for the Task class go here. // Add a new task to the list using lockless programming. void AddTask( DWORD ID, DWORD data ) < Task* newItem = new Task( ID, data ); InterlockedPushEntrySListRelease( g_taskList, newItem ); >// Remove a task from the list, using lockless programming. // This will return NULL if there are no items in the list. Task* GetTask()

Переменные и изменение порядка

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

Однако гарантий стандарта недостаточно для использования переменных для многопотоочной работы. Стандарт C++ не запрещает компилятору переупорядочение непостоянных операций чтения и записи относительно переменных операций чтения и записи, и ничего не говорит о предотвращении переупорядочения ЦП.

Visual C++ 2005 выходит за рамки стандартного C++ и определяет многопоточность для доступа к переменным. Начиная с Visual C++ 2005 для операций чтения из переменных переменных определяется семантика чтения и получения, а для операций записи в переменные — семантика выпуска записи. Это означает, что компилятор не будет изменять порядок операций чтения и записи после них, а в Windows он будет гарантировать, что ЦП не будет делать этого.

Важно понимать, что эти новые гарантии применяются только к Visual C++ 2005 и будущим версиям Visual C++. Компиляторы других поставщиков обычно реализуют другую семантику без дополнительных гарантий Visual C++ 2005. Кроме того, на Xbox 360 компилятор не вставляет инструкции, чтобы предотвратить переупорядочение операций чтения и записи ЦП.

Пример канала данных Lock-Free

Канал — это конструкция, которая позволяет одному или нескольким потокам записывать данные, которые затем считываются другими потоками. Версия канала без блокировки может быть элегантным и эффективным способом передачи работы от потока к потоку. Пакет SDK для DirectX предоставляет LockFreePipe— канал без блокировки с одним средством чтения и одним модулем записи, доступный в DXUTLockFreePipe.h. Тот же LockFreePipe доступен в пакете SDK для Xbox 360 в AtgLockFreePipe.h.

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

Производительность Xbox 360

Производительность инструкций и функций синхронизации на Xbox 360 зависит от того, какой другой код выполняется. Получение блокировок займет гораздо больше времени, если в настоящее время блокировкой владеет другой поток. Операции InterlockedIncrement и критически важных разделов будут выполняться гораздо дольше, если другие потоки записывают данные в ту же строку кэша. Содержимое очередей магазина также может повлиять на производительность. Таким образом, все эти числа являются всего лишь аппроксимациями, созданными на основе очень простых тестов:

  • Lwsync был измерен как принимая 33-48 циклов.
  • InterlockedIncrement измерялся как 225–260 циклов.
  • Получение или освобождение критического раздела было оценено как заняв около 345 циклов.
  • Получение или освобождение мьютекса было измерено как заняв около 2350 циклов.

Производительность Windows

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

Однако даже некоторые измерения, созданные на основе очень простых тестов, полезны:

  • MemoryBarrier был измерен как принимая 20-90 циклов.
  • InterlockedIncrement измеряется как 36–90 циклов.
  • Получение или освобождение критического раздела измерялось как выполнение 40–100 циклов.
  • Получение или освобождение мьютекса было измерено как принятие около 750-2500 циклов.

Эти тесты выполнялись в Windows XP на различных процессорах. Короткие периоды времени были на компьютере с одним процессором, а более длительные — на многопроцессорной машине.

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

Рекомендации по производительности

Получение или освобождение критического раздела состоит из барьера памяти, операции InterlockedXxx и дополнительной проверки для обработки рекурсии и возврата к мьютексу, если это необходимо. Следует опасаться реализации собственного критического раздела, потому что спиннинг в цикле, ожидающий блокировки, чтобы быть свободным, не возвращаясь к мьютексу, может тратить значительную производительность. Для критически важных разделов, которые активно оспариваются, но не удерживаются в течение длительного времени, следует рассмотреть возможность использования InitializeCriticalSectionAndSpinCount , чтобы операционная система некоторое время ждала, пока критически важный раздел станет доступным, а не сразу откладывать на мьютекс, если критически важный раздел принадлежит при попытке получить его. Чтобы определить критические разделы, которые могут извлечь выгоду из счетчика спинов, необходимо измерить длину типичного ожидания определенной блокировки.

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

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

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

Сводка по различиям платформ

  • Функции InterlockedXxx препятствуют переупорядочению операций чтения и записи ЦП в Windows, но не на Xbox 360.
  • Чтение и запись переменных переменных с помощью Visual Studio C++ 2005 предотвращает переупорядочение операций чтения и записи ЦП в Windows, а на Xbox 360 — только для чтения и записи компилятора.
  • Операции записи переупорядочены на Xbox 360, но не на x86 или x64.
  • Операции чтения переупорядочены на Xbox 360, но в x86 или x64 они переупорядочены только относительно операций записи и только в том случае, если операции чтения и записи предназначены для различных расположений.

Рекомендации

  • По возможности используйте блокировки, так как их проще использовать правильно.
  • Избегайте слишком частой блокировки, чтобы затраты на блокировку не стали значительными.
  • Избегайте слишком долгого хранения замков, чтобы избежать длинных киосков.
  • При необходимости используйте программирование без блокировки, но убедитесь, что преимущества оправдывают сложность.
  • Используйте программирование без блокировки или спин-блокировки в ситуациях, когда другие блокировки запрещены, например при совместном использовании данных между отложенными вызовами процедур и обычным кодом.
  • Используйте только стандартные алгоритмы программирования без блокировок, которые были признаны правильными.
  • При программировании без блокировки при необходимости используйте переменные переменных флага и инструкции по барьеру памяти.
  • При использовании InterlockedXxx на Xbox 360 используйте варианты Получения и выпуска .

Ссылки

  • библиотека MSDN. "volatile (C++)". Справочник по языку C++.
  • Вэнс Моррисон. "Понимание влияния методов Low-Lock в многопоточных приложениях". ЖУРНАЛ MSDN, октябрь 2005 г.
  • Лайонс, Майкл. "Модель хранилища PowerPC и программирование AIX". IBM developerWorks, 16 ноября 2005 г.
  • Маккенни, Пол Э. "Упорядочение памяти в современных микропроцессорах, часть II". Журнал Linux, сентябрь 2005 г. [Эта статья содержит некоторые сведения о x86.]
  • Корпорация Intel. "Упорядочение памяти в архитектуре Intel® 64". Август 2007 г. [Относится к процессорам IA-32 и Intel 64.]
  • Ниблер, Эрик. "Отчет о поездках: нерегламентированное собрание по потокам в C++". Источник C++, 17 октября 2006 г.
  • Харт, Томас Э. 2006. "Быстрая синхронизация без блокировки: влияние на производительность восстановления памяти". Материалы Международного симпозиума по параллельной и распределенной обработке 2006 года (IPDPS 2006), остров Родос, Греция, апрель 2006 года.

Блокировки как один из способов обеспечения изоляции транзакций

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

Блокировочники

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

Виды блокировок

База данных в зависимости от выставленного уровня изоляции и операции, которая должна быть выполнена, может выставлять блокировки разных видов. К самым популярным видам блокировок относятся Shared и Exclusive lock. Для простоты можно считать, что Shared lock является блокировкой на запись. Она накладывается в случае выполнения какого-нибудь select'а, причем одна запись может быть вычитана несколькими транзакциями параллельно. Exclusive lock выставляется в том случае, если осуществляется update или delete некоторой строчки. При таком виде блокировки чтение этой строчки осуществить невозможно.

Механизм двухфазных блокировок

Для обеспечения изоляции может применяться механизм так называемых двухфазных блокировок. Двухфазные блокировки (2PL) часто путают с двухфазными коммитами (2PC), хотя они относятся к совершенно разным контекстам. Алгоритм состоит из двух фаз (что следует из его названия): установка блокировки и ее снятие.

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

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

Эскалация блокировок

Одной из самых неприятных ситуаций, связанных с блокировками, является эскалация блокировки. Иногда даже параллельные и непересекающиеся операции update могут висеть в ожидании друг друга. База данных может посчитать, что вместо того, чтобы заблокировать много строк, ей будет дешевле заблокировать страницу/индекс/таблицу/базу данных. Это и называется эскалация блокировки. Грань, по которой определяется степень эскалации блокировки, проходит по-разному в зависимости от базы данных. Это может быть определенный % изменений, превышение фиксированного количества изменяемых строк. Если число запросов велико, эскалация может приводить к тому, что транзакции могут начать отваливаться по таймауту.

Deadlock

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

Механизм отката

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

Заключение

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

Если вы дочитали до конца, приглашаю на свой бесплатный вебинар по теме «Индексы в MySQL: best practices и подводные камни».

Магия программирования

Блокировка – это механизм, который предотвращает одновременный доступ к конкретному объекту данных. Когда одна транзакция владеет блокировкой на элемент, то непараллельная транзакция может читать и/или изменять этот элемента. Блокировка может быть только моментальным замком, состоявшимся в том время, как элемент был прочитан, или он может состояться до тех пор пока транзакция не завершится. Пессимистическая блокировка – это блокировка, которая устанавливается при чтении элемента и удерживается до тех пор, пока транзакция не завершится.

В режиме чтения подтвержденного (наш предпочтительный уровень изоляции транзакций), БД никогда не приобретает пессимистическую блокировку, если это явно не запрашивается приложением. Как правило, пессимистические блокировки не являются самым масштабируемым подходом к параллелизму. Однако, при определенных обстоятельствах, они могут быть полезны для предотвращения тупиковых ситуаций на уровне БД, которые приводят к сбою транзакций. Некоторые СУБД (например, Oracle и PostgreSQL) обеспечивают SQL SELECT … FOR UPDATE синтаксис, чтобы разрешить использование явных пессимистических блокировок. Вы можете проверить Hibernate диалекты, чтобы узнать поддерживает ли ваша БД эту функцию. Если БД не поддерживает такой функции, то Hibernate будет всегда выполнять обычный SELECT без FOR UPDATE пункта.

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

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

Transaction tx = session.beginTransaction();
Category cat = (Category) session.get(Category.class, catId);
cat.setName("New Name");
tx.commit();

тогда, вы можете установить пессимистичную блокировку следующим образом:
Transaction tx = session.beginTransaction();
Category cat = (Category) session.get(Category.class, catId, LockMode.UPGRADE);
cat.setName("New Name");
tx.commit();

В этом режиме, Hibernate будет загружать категории, используя SELECT … FOR UPDATE, таким образом, блокирую возвращаемые строки в БД, пока они не будут освобождены, когда транзакция завершится.

Hibernate определяет несколько стратегий блокировок:
LockMode.NONE – не обращается к БД, за исключением случае, если объект не в КЭШе.

LockMode.READ – обходит оба уровня КЭШа, а также выполняет проверки версии, чтобы убедиться, что версия объекта в памяти совпадает с той, что существует в БД.

LockMode.UPGRADE – обходит оба уровня КЭШа, выполняет проверку версии (если применимо), и получает пессимистическую блокировку обновления уровня БД, если она поддерживается.

LockMode.UPGRADE_NOWAIT – тоже самое, что UPGRADE, но использует SELECT … FOR UPDATE NOWAIT в Oracle. Это отключает ожидание освобождения параллельных блокировок, тем самым выбрасывая исключение о блокировке немедленно, если блокировка не может быть получена.

LockMode.WRITE – устанавливается автоматически, когда Hibernate записывает строку в текущей транзакции (это внутренний режим; вы не можете указать это явно).

По умолчанию, load() и get() используют LockMode.NONE. LockMode.READ является наиболее полезным с Session.lock() и несвязанным объектом. Например:

Item item = . ;
Bid bid = new Bid();
item.addBid(bid);
.
Transaction tx = session.beginTransaction();
session.lock(item, LockMode.READ);
tx.commit();

Этот код выполняет проверку версии на несвязанном Item для проверки того, что строка БД не обновлена другой транзакцией с момента её извлечения, перед сохранением нового Bid каскадно (предполагая, что каскадная ассоциация от Item к Bid разрешена).

Задавая явный LockMode, отличный от LockMode.NONE, вы заставляете Hibernate обойти оба уровня КЭШа и пройти весь путь к БД. Мы считаем, что в большинстве случаев кэширование является более полезным, чем пессимистическая блокировка, поэтому мы не используем явный LockMode, если мы действительно в нем не нуждаемся. Наш совет в том, что если у вас есть профессиональный администратор БД на вашем проекте, то пусть администратор решит какие операции требуют пессимистических блокировок, как только приложение запущено и работает. Это решение должно зависеть от тонких деталей взаимодействия между различными операциями, о которых можно не догадаться сразу.

Давайте рассмотрим ещё один аспект одновременного доступа к данным. Мы считаем, что большинство Java разработчиков знакомы с понятием транзакции БД, и это то, что они обычно имеют ввиду под транзакцией. В этой книге, мы полагаем, что это мелкозернистые(fine-grained) транзакции, но мы также полагаем более крупнозернистое понятие. Наши крупнозернистые транзакции будут соответствовать тому, что пользователь считает одной единицей работы. Почему они должны отличаться от мелкозернистых транзакций БД?

БД изолирует воздействие параллельных транзакций БД. Она должна появиться в приложении, где каждая транзакция является единственной доступной транзакцией в настоящее время в БД (даже если это не так). Изоляция является дорогостоящей. БД должна выделять значительные ресурсы для каждой транзакции на время транзакции. В частности, мы уже обсуждали, что многие БД блокируют строки, которые были прочитаны или обновлены в транзакции, не допуская любые другие транзакции, до завершения первой транзакции. В системах с высокой степенью параллелизма, эти блокировки могут ухудшить масштабируемость, если они удерживаются дольше, чем это абсолютно необходимо. По этой причине, вы не должны удерживать транзакции БД (или даже JDBC соединений) открытых, в ожидании пользовательского ввода (все это разумеется также относится к Hibernate транзакции, поскольку это всего лишь адаптер к основном механизму транзакций БД).

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

5.2 Работа с транзакциями приложения

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

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

Мы сейчас переформулируем это определение точнее. Большинство веб-приложений включает в себя несколько примерно следующих типов функциональности.

1. Данные извлекаются и отображаются на экране в первой транзакции БД.
2. Пользователь имеет возможность просмотреть и изменить данные, вне всяких транзакций БД.
3. Изменения сохраняются в БД во второй транзакции.

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

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

Давайте обсудим применение транзакций приложения на примере.

В нашем приложении CaveatEmptor, как пользователь может размещать комментарий, так и любой администратор может открыть экран редактирования комментариев, удалять или редактировать текст комментария. Предположим, что два разных администратора открыли экран редактирования, чтобы посмотреть один и тот же комментарий одновременно. Как изменить текст комментария и отобразить эти изменения. На данные момент, мы имеем три способа справиться с попыткой одновременной записи в БД:

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

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

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

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

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

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

5.2.1 Использование управляемого версионирования

Управляемой версионирование основывается либо на номере версии, который увеличивается или на метке, которая обновляется в настоящем времени, при любом изменении объекта. Для управляемого версионирования в Hibernate, мы должны добавить новое свойство для нашего класса Comment и отобразить его в качестве номера версии, используя тег . Во-первых, давайте посмотрим на изменения в классе Comment:

public class Comment <
.
private int version;
.
void setVersion(int version) <
this.version = version;
>
int getVersion() <
return version;
>
>

Вы можете также использовать модификаторы public для сеттера и геттера. Свойство должно прийти сразу после отображения идентификатора в файле отображения для класса Comment:

Номер версии – это просто счетчик, он не имеет какой-либо полезного семантического значения. Некоторые люди предпочитают использовать временные метки(timestamp), вместо счетчика:

public class Comment <
.
private Date lastUpdatedDatetime;
.
void setLastUpdatedDatetime(Date lastUpdatedDatetime) <
this.lastUpdatedDatetime = lastUpdatedDatetime;
>
public Date getLastUpdatedDatetime() <
return lastUpdatedDatetime;
>

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

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

FAQ Если версия родительского объекта изменится, то изменится ли наследник? Например, если одна ставка в коллекции ставок Item изменится, то изменится ли номер версии для Item или нет? Ответ на этот и подобные вопросы прост: Hibernate будет увеличивать номер, когда объект «загрязнен»(dirty). Это включает в себя все «грязные» свойства, будь то одиночные значения или коллекции. Подумайте о взаимоотношениях Item и Bid: если изменяется Bid, то версия связанного Item не увеличивается. Если мы добавим или удалим Bid из коллекции ставок, то версия Item будет обновлена (конечно, мы хотели бы сделать Bid неизменяемым классом, поскольку не имеет смысла изменять ставки).

Всякий раз, когда Hibernate обновляет комментарий, он использует колонку с версией в SQL WHERE условии:

update COMMENTS set COMMENT_TEXT='New comment text', VERSION=3
where COMMENT_ID=123 and VERSION=2

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

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

Как вы можете видеть, Hibernate позволяет с легкостью использовать версии для осуществления оптимистической блокировки. Можете ли вы использовать оптимистические и пессимистические блокировки вместе, или вы можете принять решения только в пользу одной блокировки? А почему она называется оптимистической?

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

Конечно, обе стратегии имеют свои области применения. Многопользовательские приложения обычно по умолчанию используют оптимистический контроль параллелизма и используют пессимистические блокировки, когда это необходимо. Обратите внимание, что продолжительность пессимистической блокировки в Hibernate является продолжительность одной транзакции БД! Это означает, что вы не можете использовать эксклюзивную блокировку, чтобы заблокировать одновременный доступ дольше, чем на одну транзакцию БД. Мы считаем, что это хорошо, потому что единственным решением была бы чрезвычайно дорогая блокировка в памяти (или так называемая блокировка таблицы в БД) на срок, например, транзакции приложения. Это почти всегда узкое место в производительности; каждый доступ к данным требует дополнительной проверки блокировки с синхронизированным менеджером блокировок. Вы можете, если это сильно необходимо в вашем конкретном приложении, осуществлять простую длительную пессимистичную блокировку, используя Hibernate для управления блокированием таблицы. Шаблоны для этого могут быть найдены на сайте Hibernate, но мы определенно не рекомендуем такой подход. Необходимо внимательно изучить последствия этого исключительного случая.

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

5.2.2 Детализации сессии

Чтобы понять, как можно использовать Hibernate-сессию, давайте рассмотрим отношения между транзакциями. Ранее, мы обсуждали две взаимосвязанные концепции:

• Уровень идентичности (см. раздел 4.1.4)
• Детализацию БД и транзакций приложения

Hibernate сессия определяет сферу идентичности объекта. Транзакция Hibernate соответствует сфере транзакций БД.

(РИСУНОК 5.2)

Какова взаимосвязь между сессиями и транзакциями приложения? Давайте начнем обсуждение с наиболее распространенным использованием сессии.

Обычно, мы открываем новую сессию для каждого запроса клиента (например, запроса веб-браузера) и начинаем новую транзакцию. После выполнения бизнес-логики, мы фиксируем изменений транзакции БД и закрываем сессию перед отправкой ответа клиенту (см. рис. 5.2).

Сессии (S1) и транзакции БД (T1) имеют одну и ту же степень детализации. Если вы работаете с концепцией транзакций приложения, то это простой подход – все что вам нужно в вашем приложении. Нам нравится называть такой подход сессия-на-запрос.

Если вам необходима длительная транзакция приложения, то вам могут помочь несвязанные объекты (и Hibernate поддерживает оптимистическую блокировку, обсуждаемую в предыдущем разделе), осуществите его, используя тот же подход (см. рис. 5.3).

Предположим, что ваша транзакция приложения охватывает два клиентских запроса/ответа, например два HTTP-запроса в веб-приложении. Вы можете загрузить интересующие объекты в первой сессии, а затем связать их к новой сессии после того, как они были изменены пользователем. Hibernate автоматически проверит версию. Время между (S1, T1) и (S2, T2) может быть «длинным», настолько длинным, насколько это нужно пользователю, чтобы сделать его изменения. Этот подход известен также, как сессия-на-запрос-с-несвязанными-объектами.

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

Сессия сериализуемая и может быть безопасно сохранена, например, в сервлете HttpSession. Конечно, базовое JDBC соединение должно быть закрыто и новое соединение должно быть получено на следующий запрос. Вы используете методы disconnect() и reconnect() интерфейса сессии для того, чтобы освободить соединение и затем получать новое соединение. Такое подход известен, как сессия-на-транзакцию-приложения или длинная сессия.

Как правило, ваш первый выбор должен сохранять Hibernate сессию открытой не более, чем на одну транзакцию БД (сессия-на-запрос). После завершения первоначальной транзакции БД, чем дольше сессия остается открытой, тем больше шансов, что она удерживает устаревшием данные в КЭШе хранимых объектов (кэш первого уровня является обязательным в сессии). Конечно, вы никогда не должны повторно использовать одну сессию дольше, чем это требуется для завершения одной транзакции приложения.

Вопрос о реализации транзакций приложения и объем сессии важен при построении приложения. Мы обсудим реализацию стратегии, с примерами в главе 8 «Реализация транзакций приложения».

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

5.2.3 Другие способы реализации оптимистической блокировки

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

Это альтернативная реализация оптимистической блокировки проверяет текущее состояние БД, на неизменность значений хранимые свойств, в то время, как объект был извлечен (или во время последней очистки сессии). Вы можете включить эту функцию, установив optimistic-lock атрибут в файле отображения класса:

Сейчас, Hibernate будет включать все свойства в WHERE условие:

update COMMENTS set COMMENT_TEXT='New text'
where COMMENT_ID=123
and COMMENT_TEXT='Old Text'
and RATING=5
and ITEM_ID=3
and FROM_USER_ID=45

Кроме того, Hibernate будет включать только изменяемые свойства (только COMMENT_TEXT в данном примере) если вы выставите optimistic-lock=”dirty” (заметим, что этот параметр также требует, чтобы вы установили файл отображения класса в dynamic-update=”true”).

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

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

Типы блокировок в Microsoft SQL Server

Приветствую всех посетителей сайта Info-Comp.ru! В данном материале мы рассмотрим типы блокировок, которые существуют в Microsoft SQL Server, поговорим о том, как и для чего они используются.

Типы блокировок

Блокировка – это механизм, с помощью которого SQL Server организовывает одновременный доступ нескольких пользователей к одному участку данных.

Заметка! Подробнее о том, что такое блокировки и для чего они нужны, мы рассматривали в статье – Блокировки в Microsoft SQL Server.

Типы блокировок

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

В следующей таблице представлены типы блокировок в SQL Server.

Блокировка Описание
Shared (S) Совмещаемая блокировка
Exclusive (X) Эксклюзивная блокировка
Update (U) Блокировка обновления
Intent shared (IS) Блокировка с намерением совмещаемого доступа
Intent exclusive (IX) Блокировка с намерением эксклюзивного доступа
Intent update (IU) Блокировка с намерением обновления
Shared with intent exclusive (SIX) Совмещаемая блокировка с намерением эксклюзивного доступа
Shared intent update (SIU) Совмещаемая блокировка с намерением обновления
Update intent exclusive (UIX) Блокировка обновления с намерением эксклюзивного доступа
Schema modification (Sch-M) Блокировка изменения схемы
Schema stability (Sch-S) Блокировка стабильности схемы
Bulk Update (BU) Массовое обновление
Key-range Блокировка диапазона строк

Давайте чуть более подробно рассмотрим каждый тип блокировки.

Совмещаемые блокировки

Обозначение блокировки – Shared (S)

Такие блокировки используются для операций чтения данных (инструкции SELECT).

Пока на ресурсе существуют совмещаемые блокировки, другие транзакции не могут изменять данные.

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

Эксклюзивные блокировки

Обозначение блокировки – Exclusive (X)

Используется для операций модификации данных: INSERT, UPDATE или DELETE. Такие блокировки гарантируют, что несколько изменений не будет выполнено одновременно для одного ресурса, иными словами, эксклюзивная блокировка запрещает транзакциям одновременный доступ к ресурсу.

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

Блокировки обновления

Обозначение блокировки – Update (U)

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

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

Сначала, чтобы обновить данные, SQL Server должен их считать и применить все условия отбора данных, таким образом, он должен наложить совмещаемую блокировку (S). Затем, у тех записей, которые не подходят под условия отбора, совмещаемая блокировка отпускается, а по записям, которые подходят, т.е. их нужно обновить, SQL Server преобразует совмещаемую блокировку в эксклюзивную (X), так как мы уже выяснили, изменять данные может только одна транзакция, и для этого ей на этом ресурсе нужна эксклюзивная блокировка.

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

Именно поэтому SQL Server, когда считывает данные для обновления и проверяет условия отбора, не ставит совмещаемую блокировку (S), как я описал ранее, а вместо нее ставит блокировку обновления (U), которая потом как раз и преобразуется в эксклюзивную (X).

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

Блокировки с намерением

В SQL Server блокировки с намерением применяются для защиты размещения совмещаемой (S) или эксклюзивной (X) блокировки ресурса на более низком уровне. Блокировки с намерением называются так потому, что их получают до блокировок более низкого уровня, т.е. они обозначают намерение поместить блокировку на более низком уровне.

Блокировка с намерением выполняет две функции:

  • Предотвращает изменение ресурса более высокого уровня другими транзакциями таким образом, что это сделает недействительной блокировку более низкого уровня;
  • Повышает эффективность SQL Server при распознавании конфликтов блокировок на более высоком уровне гранулярности.

Например, блокировка с намерением совмещаемого доступа запрашивается на уровне таблицы до того, как будут запрошены совмещаемые блокировки для страниц или строк в этой таблице. Установка блокировки намерений на уровне таблицы предотвращает последующее получение другой транзакцией эксклюзивной (X) блокировки таблицы, содержащей эту страницу. Блокировки намерений повышают производительность, поскольку SQL Server проверяет блокировки намерений только на уровне таблицы, чтобы определить, может ли транзакция безопасно получить блокировку этой таблицы. Это устраняет необходимость проверять блокировки в каждой строке и на каждой странице в таблице, чтобы определить, может ли транзакция заблокировать всю таблицу.

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

Тип блокировки Описание
Блокировка с намерением совмещаемого доступа (IS) Защищает запрошенные или полученные совмещаемые блокировки на некоторых (но не на всех) ресурсах на более низком уровне иерархии.
Эксклюзивная блокировка намерения (IX) Защищает запрошенные или полученные эксклюзивные блокировки на некоторых (но не на всех) ресурсах на более низком уровне иерархии. Режим IX является расширенным режимом IS, кроме того, он защищает запрос на совмещаемые блокировки на ресурсах более низкого уровня.
Совмещаемая блокировка с намерением эксклюзивного доступа (SIX) Защищает запрошенные или полученные совмещаемые блокировки на всех ресурсах более низкого уровня иерархии, а также блокировки с намерением на некоторых (но не всех) ресурсах более низкого уровня. На ресурсах верхнего уровня допускаются одновременные блокировки IS. Одновременно для одного ресурса может быть установлена только одна блокировка SIX, что предотвращает обновление ресурса другими транзакциями, хотя эти транзакции могут считывать данные с ресурсов более низкого уровня в иерархии, получая блокировки IS уровня таблицы.
Блокировка с намерением обновления (IU) Защищает запрошенные или полученные блокировки обновления на всех ресурсах более низкого уровня в иерархии. Блокировки IU применяются только на страничных ресурсах. Если выполняется операция обновления, то блокировки IU преобразуются в IX.
Совмещаемая блокировка с намерением обновления (SIU) Сочетание блокировок S и IU в результате раздельного запрашивания этих блокировок и одновременного удержания их обеих. Например, транзакция выполняет запрос с указанием PAGLOCK, затем выполняет операцию обновления. Запрос с указанием PAGLOCK получает блокировку S, а операция обновления получает блокировку IU.
Блокировка обновления с намерением эксклюзивного доступа (UIX) Сочетание блокировок U и IX в результате раздельного запрашивания этих блокировок, и одновременного удержания их обеих.

Блокировки схемы

  • Schema modification (Sch-M) – блокировка изменения схемы
  • Schema stability (Sch-S) – блокировка стабильности схемы

Блокировка изменения схемы (Sch-M) в SQL Server применяется с операциями языка DDL для таблиц, например, при добавлении столбца или очистке таблицы. Пока удерживается блокировка изменения схемы (Sch-M), одновременный доступ к таблице запрещен. Это означает, что любые операции вне блокировки изменения схемы (Sch-M) будут запрещены до снятия блокировки.

Также блокировка изменения схемы (Sch-M) применяется с некоторыми операциями языка обработки данных, например, усечением таблиц (TRUNCATE TABLE), чтобы предотвратить одновременный доступ к таблице.

Блокировка стабильности схемы (Sch-S) применяется в SQL Server при компиляции и выполнении запросов. Такая блокировка не влияет на блокировки транзакций, включая эксклюзивные (X) блокировки. Поэтому другие транзакции (даже транзакции с эксклюзивной блокировкой (X) для таблицы) могут продолжать работу во время компиляции запроса. Однако одновременные операции DDL и DML, которые запрашивают блокировки изменения схемы (Sch-M), не могут выполняться над таблицей.

Блокировки массового обновления

Обозначение блокировки – Bulk Update (BU)

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

SQL Server использует блокировки массового обновления (BU) в следующих случаях:

  • Используется инструкция BULK INSERT, функция OPENROWSET(BULK) или одна из таких команд массовой вставки API, как .NET SqlBulkCopy, OLEDB Fast Load APIs или ODBC Bulk Copy APIs, для массового копирования данных в таблицу;
  • Используется хинт TABLOCK или установлен параметр таблицы table lock on bulk load с помощью хранимой процедуры sp_tableoption.

Примечание! В отличие от инструкции BULK INSERT, которая удерживает менее строгую блокировку массового обновления (BU), инструкция INSERT INTO…SELECT с указанием TABLOCK удерживает блокировку таблицы с намерением эксклюзивного доступа (IX). Это означает, что отсутствует возможность вставки строк с помощью параллельных операций вставки.

Блокировки диапазона строк

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

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

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

Блокировки диапазона ключей содержат и компонент диапазона, и компонент строки:

  • Компонент диапазона соответствует режиму блокировки, защищающему диапазон между любыми двумя последовательными элементами индекса;
  • Компонент строки соответствует режиму блокировки, защищающему сами элементы индекса.

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

Диапазон Строка Режим Описание
RangeS S RangeS-S Блокировка общего диапазона и общего ресурса
RangeS U RangeS-U Совмещаемая блокировка диапазона, блокировка обновления ресурса
RangeI NULL RangeI-N Блокировка диапазона для вставки, блокировка ресурса не определена. Используется для проверки диапазонов перед вставкой новых ключей в индекс
RangeX X RangeX-X Эксклюзивная блокировка диапазона, эксклюзивная блокировка ресурса. Используется при обновлении ключа в диапазоне.

Заметка! Всем тем, кто только начинает свое знакомство с языком SQL, рекомендую прочитать книгу «SQL код» – это самоучитель по языку SQL для начинающих программистов. В ней очень подробно рассмотрены основные конструкции языка.

На сегодня это все, надеюсь, материал был Вам полезен!

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *