Синхронизация кода с помощью класса Mutex

Лекция 9

Потоки (продолжение)

Безопасность и синхронизация потоков

Термин “потокобезопасность” не имеет формального определения. Википедия говорит следующее: «Кодпотоково-безопасный, если он функционирует корректно при использовании из нескольких потоков одновременно.В частности, он должен обеспечивать корректный доступ нескольких потоков к разделяемым данным.»

Пример 5. Демонстрация проблем с общими ресурсами, т.е. общими переменными, с которыми работают потоки.

intsum = 0;                                           //общая переменная, изменяемая потоками

Randomrnd = newRandom();         // объект, формирующий псевдослучайные

                                           // последовательности 

publicvoidMyThread_2()

{ for (inti = 0; i< 1000000; i++)           // данныйметодвыполняет 1`000`000 итераций,        

{ sum++;                                                       // добавляющих1 к переменной sum

 }

}

public void Test5()

{for (inti =0; i<10; i++)

{ Thread th1 = new Thread(MyThread_2);

Thread th2 = new Thread(MyThread_2);

int t0 = Environment.TickCount;

sum = 0; 

th1.Start();

th2.Start();

th1.Join();

th2.Join();

Console.Write ("{0} - ({1}) ",sum, Environment.TickCount-t0);

}

}

Метод MyThread_2()увеличивает значение переменной sumна 1`000`000. Можно ожидать, что два параллельно работающие потока должны увеличить значение sumна 2`000`000`.

Результат выполнения примера

1597925 - (172) 1683165 - (141) 1477636 - (203) 1659371 - (140) 1457064 - (203)   

1629993 - (157) 1471787 - (203) 1648336 - (156) 1704287 - (141) 1907867 - (78)

Ни в одном из 10 случаев не получено корректного результата. В скобках указано время вычисления sum двумя потоками.

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

     прочитать значение из памяти в регистр,

     выполнить инкрементирование,

     записать результат в память.

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

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

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

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

sum++

Для работы с критическими секциями можно использовать классы Monitor, Mutex, а также оператор С# lock.

 

Использование класса Monitor

Класс System. Monitor позволяет упорядочить обращения к критическим блокам кода, работающих с общими ресурсами, с помощью блокировки участка и его освобождения.

Класс Monitor предоставляет следующие статические методы:

Enter — блокирует участок кода потока. Если другой поток уже заблокировал этот участок кода, то данный поток не сможет выполнить этот участок кода, пока другой поток не освободит его.

Exit — снимает блокировку, т.е. освобождает участок кода потока, делает его доступным для других потоков.

В примере 5 метод потока заменим следующим методом:

objectobj_lock = new object();

publicvoidMyThread_3()

{ for (inti = 0; i< 1000; i++)

           { Monitor.Enter(obj_lock);        // допустимоMonitor.Enter (this);

                          sum++;

           Monitor.Exit(obj_lock);                              // допустимоMonitor.Exit (this);

           }

}

Результат выполнения

2000000 - (78) 2000000 - (94) 2000000 - (78) 2000000 - (78) 2000000 - (78)   

2000000 - (94) 2000000 - (78) 2000000 - (78) 2000000 - (94) 2000000 - (78)

 

Методы Enter(), Exit() в качестве аргументов принимают некоторый объект (произвольного типа), доступный всем потокам. Метод потока в C# - всегда метод некоторого класса, поэтому можно использовать ключевой слово this, обозначающее текущий объект класса. Если в приложении необходимо использовать несколько групп потоков, то каждую группу можно синхронизировать с использованием своего объекта.

 

Оператор С# lock не поддерживает полный набор функций класса Monitor, но позволяет получать и освобождать блокировку монитора. Чтобы задействовать оператор lock, необходимо указать перед фигурными скобках, которые ограничивают блокируемыйучасток кода.

Следующий вариант функции потока выдаст информацию, аналогичную предыдущему примеру:

 

public void MyThread_4()

{ for (inti = 0; i< 1000; i++)

           { lock (obj_lock)                        //допустимо (this)

           { sum++;

           }         

           }

}

Результат выполнения

2000000 - (109) 2000000 - (94) 2000000 - (94) 2000000 - (93) 2000000 - (94) 

2000000 - (94) 2000000 - (94) 2000000 - (93) 2000000 - (94) 2000000 - (94)

С точки зрения времени выполнения эти подходы можно считать почти равноценными.

Кроме блокировки и разблокировки объекта класс Monitor имеет еще ряд методов, которые позволяют управлять синхронизацией потоков. Так, метод Monitor.Wait освобождает блокировку объекта и переводит поток в очередь ожидания объекта. Все потоки, которые вызвали метод Wait, остаются в очереди ожидания, пока не получат сигнала от методаMonitor.Pulse или Monitor.PulseAll, посланного владельцем блокировки. Если метод Monitor.Pulse отправлен, поток, находящийся во главе очереди ожидания, получает сигнал и блокирует освободившийся объект. Если же метод Monitor.PulseAll отправлен, то все потоки, находящиеся в очереди ожидания, получают сигнал и переходят в очередь готовности, где им снова разрешается получать блокировку объекта.

 

Синхронизация кода с помощью класса Mutex

 

Класс Mutex, определенный в пространстве имен System. Threading — это представление примитива мьютекс системы Win32 (объекта ядра). Термин мьютекс(mutex) происходит от ‘mutuallyexclusive’ (взаимно исключающий). Мьютекс используют как средство синхронизации для монопольного обращения к критическому участку кода так же, как монитор. Только один поток в любой момент времени может получить конкретный объект- мьютекс.

Класс Mutex предоставляет три конструктора для создания объектов:

Mutex();

Mutex(boolизначально_6локированный);

Mutex(boolизначально_блокированный, stringимя_мьютекса).

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

Второй принимает логический флаг, который определяет, владеет ли мьютексомпоток, который создал его (true) или нет (false).

Третий конструктор дополнительно указывает имя мьютекса.

 

При работе с мьютексами практически всегда требуются следующие методы класса Mutex— WaitOne() и ReleaseMutex (), которые выполняют основную работу по синхронизации:

Метод ReleaseMutex() предназначен для освобождения мьютекса.

Для того чтобы получить мьютекс, в коде программы следует вызвать метод WaitOne() для этого мьютекса. Метод WaitOne() наследуется классом Mutex от класса Thread.WaitHandle. Метод WaitOne() ожидает до тех пор, пока не будет получен мьютекс, для которого он был вызван, т.е. метод блокирует выполнение вызывающего потока до тех пор, пока не станет доступным указанный мьютекс.

Имеются несколько вариантов перегрузки метода WaitOne:

WaitOne ()

WaitOne (TimeSpanвремя)

WaitOne (intмиллисекунды)

Основное различие между этими способами перегрузки в том, что первый будет ждать неопределенно долго, а второй и третий будут ждать в течение указанного промежутка времени, выраженного значением типа TimeSpanили int.

Результат - значение true при получении сигнала текущим экземпляром; в противном случае — значение false.

Метод может генерировать следующие исключения:

ObjectDisposedException Текущий экземпляр мьютекса уже был удален.
ArgumentOutOfRangeException Параметр millisecondsTimeout является отрицательным числом, отличным от –1, что означает бесконечное время ожидания.
AbandonedMutexException Ожидание завершено, поскольку поток завершил работу, не освободив мьютекс. Это исключение не вызывается в Windows 98 или WindowsMillenniumEdition.
InvalidOperationException Текущий экземпляр является прозрачный прокси дляWaitHandle в другом домене приложения.

 

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

staticMutexmt = newMutex(false);                 //статическая переменная класса      

public void MyThread_5()

{for (inti = 0; i< 1000000; i++)

           {  while (!mt.WaitOne (1)) Thread.Sleep (2);

                sum++;

           mt.ReleaseMutex();

           }

}

Результат выполнения

2000000 - (8437) 2000000 - (8375) 2000000 - (8422) 2000000 - (8563) 2000000 - (8422)  

2000000 - (8328) 2000000 - (8547) 2000000 - (8390) 2000000 - (8438) 2000000 - (8156)

 

Временные показатели можно улучшить, если функцию потока определить как

public void MyThread_5_1()

{ for (inti = 0; i< 1000000; i++)

{ mt.WaitOne ());

sum++;

mt.ReleaseMutex();

}

}

2000000 - (6000) 2000000 - (5890) 2000000 - (5860) 2000000 - (5906) 2000000 - (5890)

2000000 - (5891) 2000000 - (5875) 2000000 - (5813) 2000000 - (5812) 2000000 - (5781)

КлассAutoResetEvent

 

КлассAutoResetEventтакжеслужитцелямсинхронизациипотоков. Этот класс является оберткой над объектом ядра ОС "событие". Данный объект может находиться в одном из двух состояний – “сигнализирует” или “молчит”. Данный объект-событие можно установить в любое состояние и переводить из одного состояния в другое.

Конструктор
public AutoResetEvent(bool initialState)

инициализирует новый экземпляр объекта, устанавливая его в состояние “сигнализирует”, если аргумент initialState=true, или в состояние “молчит”в противном случае.

 

Основные методы класса:

Reset() - задает несигнальное состояние события, вызывая блокировку потоков. Возвращает значение true, если операция прошла успешно; в противном случае — false.

Set() - устанавливает сигнальное состояние события, что позволяет продолжить выполнение одному или нескольким ожидающим потокам. Возвращает значение true, если операция прошла успешно; в противном случае — false.

WaitOne() -блокирует текущий поток до получения сигнала объектом (до перехода объекта в состояние ‘сигнализирует”.

WaitOne(Int32), WaitOne(TimeSpan) -блокирует текущий поток до получения сигнала текущим экземпляром, используя значение типа Int32 или TimeSpan для указания интервала времени.

 

Пример вычисления суммы с использованием нескольких потоков и объекта-события для синхронизации потоков.

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

 

class Sum {

static AutoResetEventmyWait = new AutoResetEvent(false); //объектвсостоянии ”молчит”

IntmyTime = 0;



Понравилась статья? Добавь ее в закладку (CTRL+D) и не забудь поделиться с друзьями:  



double arrow
Сейчас читают про: