Программирование >>  Многопоточная библиотека с принципом минимализма 

1 ... 99 100 101 [ 102 ] 103 104 105 106


Поскольку первая операция считывает, вторая - модифицирует, а третья - записывает данные, они образуют тройку, известную по названием операция чтения-модификации-записи (read-modify-write operation - RMW).

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

К сожалению, другой процессор может применить операцию чтения-модификации-записи к той же переменной. Например, допустим, что существуют два оператора инкрементации переменной х, которая в начальный момент времени имеет значение О, и эти операторы вьтолняются двумя процессорами Р1 и Р2 в такой последовательности.

1. Процессор Р1 захватывает шину запоминающего устройства и извлекает переменную x.

2. Процессор Р1 освобождает шину запоминающего устройства.

3. Процессор Р2 захватывает шину запоминающего устройства и извлекает переменную x (значение которой по-прежнему равно 0). В то же время процессор Р1 увеличивает значение переменной х на единицу в своем арифметико-логическом устройстве. Результат равен 1.

4. Процессор Р2 освобождает шину запоминающего устройства.

5. Процессор Р1 захватывает шину запоминающего устройства и записывает значение 1 в переменную х. Одновременно процессор Р2 увеличивает значение переменной x на единицу в своем арифметико-логическом устройстве. Поскольку процессор Р2 извлек значение О, результат снова равен 1.

6. Процессор Р2 освобождает шину запоминающего устройства.

7. Процессор Р2 захватывает шину запоминающего устройства и записывает значение 1 в переменную х.

8. Процессор Р2 освобождает шину запоминающего устройства.

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

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

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



Обьшно операционные системы определяют атомарные операции для целочисленных типов, размер которых совпадает с шириной шины запоминаюшего устройства - в большинстве случаев это тип int. Потоковая подсистема библиотеки Loki (файл Threads.h) определяет тип IntType внутри каждой реализации стратегаи ThreadingModel.

Элементарные функции, выполняющие атомарные операции, определенные в стратегии ThreadingModel, имеют следующий вид.

template <typename т> class SomeThreadingModel {

public:

typedef int intType; или другой тип в зависимости от платформы static IntType AtomicAdd(volatile intTypeA Ival, intType val); static IntType AtomicSubtract(vo1ati1e intTypeA Ival, intType val);

... аналогичные определения для функций AtomicMultiply, AtomicDivide, Atomiclncrement и AtomicDecrement ...

static void AtomicAssign(volatile intTypeA Ival, IntType val);

static void AtomicAssign(lntType& Ival, volatile intTypeA val)

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

volatile int counter;

SomeThreadingMode1<widget>::AtomicAdd(counter, 5); if (counter == 10) ...

В этом случае код не проверяет переменную counter сразу после сложения, поскольку другой поток может модифицировать ее в интервале между вызовами функции Atomi cAdd и оператором if. Чаще всего проверять значение переменной counter необходимо сразу после вызова функции Atomi cAdd. Для этого достаточно написать следующий код. if (AtomicAdd(counter, 5) == 10) ...

Наличие двух разных функций AtomicAssign необходимо, поскольку даже операция копирования может быть не атомарной. Например, если ширина шины компьютера равна 32 бит, а тип long имеет размер 64 бит, для копирования переменной типа long понадобится дважды обращаться к памяти.

П.4. Мьютексы

Эдгар Дийкстра (Edgar Dijkstra) доказал, что в многопоточной среде планировщик потоков операционной системы должен содержать определенные объекты синхронизации. Без них написать правильное многопоточное приложение невозможно.

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



Мьютекс (mutex) - это аббревиатура слов Mutual Exclusive (взаимоисключающий), которые определяют способ функционирования этого объекта. Мьютекс предоставляет потокам взаимоисключающий доступ к ресурсу.

Основными функциями мьютекса являются функции Acquire и Release. Каждый поток, которому необходим исключительный доступ к ресурсу (например, к совместно используемой переменной), завладевает мьютексом. Только один поток может владеть мьютексом. После того как поток завладел мьютексом, остальные потоки, вызывающие функцию Acqui re, переводятся в состояние ожидания (функция Acqui re ничего не возвращает). Когда поток, владеющий мьютексом, вызывает функцию Release, планировщик потоков выбирает один из ожидающих потоков и передает право владения мьютексом ему.

Итак, мьютекс выступает в роли устройства последовательного доступа: часть кода между вызовом функции tiitx . Acqui re() и вызовом функции mtx .Release() является атомарной по отнощению к объекту mtx . Все попытки завладеть объектом mtx откладываются, пока атомарные операции не будут заверщены.

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

Например, представьте себе класс BankAccount, содержащий функции Deposit и withdraw. Эти операции представляют собой нечто большее, чем сложение и вычитание полей класса, имеющих тип double. Они также регистрируют дополнительную информацию, относящуюся к транзакции. Если к объекту класса BankAccount обращаются несколько потоков, то эти две операции должны быть атомарными. Для этого достаточно написать следующий код.

class BankAccount {

public:

void DepositCdouble amount, const char* user) {

mtx .Acquire();

... кладем деньги на счет...

mtx .Releasee);

void withdrawCdouble amount, const char* user) {

mtx .Acqui re();

... снимаем деньги со счета ... mtx .Releasee);

private:

Mutex mtx ;

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



1 ... 99 100 101 [ 102 ] 103 104 105 106

© 2006 - 2024 pmbk.ru. Генерация страницы: 0
При копировании материалов приветствуются ссылки.
Яндекс.Метрика