Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
4 сем Инфа готово.docx
Скачиваний:
24
Добавлен:
04.06.2015
Размер:
255.66 Кб
Скачать

Вопрос 28. Синхронизация потоков.

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

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

Как избежать подобных непредсказуемых состояний? На самом деле, как это обычно бывает в программировании, существует несколько способов решения этой хорошо известной проблемы. В этом разделе я расскажу о стандартном средстве — синхронизации. Синхронизация позволяет задавать критические секции (critical sections) кода, в которые в каждый отдельный момент может входить только один поток, гарантируя, что любые временные недействительные состояния вашего объекта будут невидимы его клиентам. Мы рассмотрим несколько средств определения критических секций, включая классы .NET Monitor и Mutex, а также оператор С# lock.

Защита кода с помощью класса Monitor

Класс System. Monitor позволяет упорядочить обращения к блокам кода с помощью блокировки и освобождения. Например, ваш метод, обновляющий БД, не может выполняться двумя и более потоками одновременно. Если выполняемая им работа требует особенно много времени и у вас есть несколько потоков, любой из которых может его вызывать, может возникнуть серьезная проблема. Здесь в дело вступает класс Monitor. Взгляните на пример синхронизации. Здесь у нас два потока, каждый из которых будет вызывать метод Database.SaveData: using System; using System.Threading;

class Database {

public void SaveData(string text)

{Console.WriteLine("Database.SaveData - Started");

Console.WriteLine("Database.SaveData - Working"); for (int 1=0; i < 100; i++)

{Console.Write(text);}

Console.WriteLine("\nDatabase.SaveData - Ended"); } }

class ThreadMonitorlApp

{

public static Database db = new DatabaseQ;

public static void WorkerThreadMethodlQ

{Console.WriteLine("Worker thread "1 - Started");

Console.WriteLine("Worker thread <M -Calling Database.SaveData"); db.SaveDataC'x");

Console.WriteLine("Worker thread *1 - Returned from Output"); }

public static void WorkerThreadMethod2()

{Console.WriteLine("Worker thread #2 - Started");

Console.WriteLine("Worker thread #2 - Calling Database.SaveData"); db.SaveData("o");

Console.WriteLine("Worker thread *2 - Returned from Output"); }

public static void MainQ

{ThreadStart workerl = new ThreadStart(WorkerThreadMethodl);

ThreadStart worker2 = new ThreadStart(WorkerThreadMethod2); Console.WriteLineC'Main - Creating worker threads");

Thread t1 = new Thread(workerl); Thread t2 = new Thread(worker2);

t1.Start(); t2.Start(); } }

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

Main - Creating worker threads

Worker thread #1 - Started Worker thread #2 - Started

Worker thread #1 - Calling Database.SaveOata Worker thread #2 - Calling Database.SaveData

Database.SaveData - Started Database.SaveData - Started

Database.SaveData - Working Database.SaveData - Working xoxoxoxoxoxoxoxoxoxoxoxoxoxoxoxoxoxoxoxoxoxoxox Database.SaveData - Ended Database.SaveData - Ended

Worker thread #1 - Returned from Output Worker thread #2 - Returned from Output

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

Для включения в этот пример класса Monitor мы воспользуемся двумя его статическими методами. Первый — Enter — во время исполнения пытается получить блокировку монитора для объекта. Если у другого потока уже есть эта блокировка, метод блокируется до тех пор, пока блокировка не будет освобождена. Заметьте: здесь не выполняется неявная операция упаковки, поэтому для этого метода вы можете предоставлять только ссылочные типы. Затем вызывается метод Monitor.Exit, чтобы освободить блокировку. Вот пример, переписанный для упорядоченного обращения к методу Database.SaveData:

using System;

using System.Threading;

class Database {

public void SaveData(string text)

{Monitor.Enter(this);

Console.WriteLine("Database.SaveData - Started");

Console.WriteLine("Database.SaveData - Working");

for (int i = 0; i < 100; i++)

{Console.Write(text); }

Console.WriteLine("\nDatabase.SaveData - Ended");

Monitor.Exit(this); }}

class ThreadMonitor2App

{public static Database db = new Database();

public static void WorkerThreadMethod1()

{Console.WriteLine("Worker thread #1 - Started");

Console.WriteLine("Worker thread #1 - Calling Database.SaveData"); db.SaveDataC'x");

Console.WriteLine("Worker thread t1 - Returned from Output");}

public static void WorkerThreadMethod2() {

Console.WriteLine("Worker thread #2 - Started");

Console.WriteLine("Worker thread #2 - Calling Database.SaveData"); db.SaveData("o");

Console.WriteLine("Worker thread #2 - Returned from Output");}

public static void Main() {

ThreadStart worker! = new ThreadStart(WorkerThreadMethodl);

ThreadStart worker2 = new ThreadStart(WorkerThreadMethod2);

Console.WriteLine("Main - Creating worker threads");

Thread t1 = new Thread(workerl); Thread t2 = new Thread(worker2);

t1.Start(); t2.Start(); } }

В приведенной ниже выходной информации обратите внимание на то, что даже если второй поток вызывал метод Database.SaveData, метод Monitor.Enter блокировал его до тех пор, пока первый поток не освобождал удерживаемую им блокировку:

Main - Creating worker threads

Worker thread #1 - Started Worker thread #2 - Started

Worker thread #1 - Calling Database.SaveData Worker thread #2 - Calling Database.SaveData

Database.SaveData - Started Database.SaveData - Working xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Database.SaveData - Ended

Database.SaveData - Started

Worker thread #1 - Returned from Output

Database.SaveData - Working ooooooooooooooooooooooooooooooooooooooo Database.SaveData - Ended

Worker thread #2 - Returned from Output

Применение блокировок монитора с оператором С# lock

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

using System;

using System.Threading;

class Database

{

public void SaveData(string text)

{

lock(this)

{

Console.WriteLine("Database.SaveData - Started");

Console.WriteLine("Database.SaveData - Working"); for (int 1=0; i < 100; i++)

{Console.Write(text); }

Console.WriteLine("\nDatabase.SaveData - Ended"); } }}

class ThreadLockApp {

public static Database db = new DatabaseO;

public static void WorkerThreadMethod1() {

Console.WriteLlne("Worker thread #1 - Started");

Console.WrlteLine

("Worker thread #1 - Calling Database.SaveData"); db.SaveData("x");

Console.WriteLlne("Worker thread #1 - Returned from Output");

}

public static void WorkerThreadMethod2() {

Console.WriteLine("Worker thread #2 - Started");

Console.WriteLine

("Worker thread "2 - Calling Database.SaveData"); db.SaveDataC'o");

Console.WriteLine("Worker thread #2 - Returned from Output"); }

public static void Main() {

ThreadStart worker"! = new ThreadStart(WorkerThreadMethodl);

ThreadStart worker2 = new ThreadStart(WorkerThreadMethod2);

Console.WriteLine("Main - Creating worker threads");

Thread t1 = new Thread(workerl); Thread t2 = new Thread(worker2);

t1.Start(); t2.Start(); } }

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

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

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

Mutex();

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

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

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

using System;

using System.Threading;

class Database {

Mutex mutex = new Mutex(false);

public void SaveData(string text)

{

mutex.WaitOne();

Console.WriteLine("Database.SaveData - Started");

Console.WriteLineC'Database.SaveData - Working"); for (int i = 0; i < 100; i++)

{

Console.Write(text);

}

Console.WriteLine("\nDatabase.SaveData - Ended");

mutex.CloseO; } }

class ThreadMutexApp {

public static Database db = new DatabaseO;

public static void WorkerThreadMethodK) {

Console.WriteLineC"Worker thread #1 - Started");

Console.WrlteLine

("Worker thread #1 - Calling Database.SaveData"); db. SaveDataC'x");

Console.WriteLineC'Worker thread #1 - Returned from Output"); }

public static void WorkerThreadMethod2() {

Console.WriteLineC'Worker thread #2 - Started");

Console.WriteLine

("Worker thread #2 - Calling Database.SaveData"); db.SaveData("o");

Console.WriteLineC'Worker thread 92 - Returned from Output");

}

public static void Main() {

ThreadStart worker! = new ThreadStart(WorkerThreadMethodl);

ThreadStart worker2 = new ThreadStart(WorkerThreadMethod2);

Console.WriteLine("Main - Creating worker threads");

Thread t1 = new Thread(workeM); Thread t2 = new Thread(worker2);

t1.Start(); t2.Start(); > }

Теперь в классе Database определено поле Mutex. Мы не хотим, чтобы поток владел мыотексом просто потому, что при этом будет невозможно обратиться к методу SaveData. Первая строка метода SaveData показывает, что вы должны пытаться получить мьютекс с помощью метода Mutex. WaitOne. В конце метода вызывается метод Close, освобождающий мьютекс.

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

WaitOne()

WaitOne(TimeSpan время, bool покину>ть_контекст) WaitOne(int миллисекунды, bool покинуть_контекст) Основное различие между этими способами перегрузки в том, что первая версия (она использована в примере) будет ждать неопределенно долго, а вторая и третья будут ждать в течение указанного промежутка времени, выраженного значением типа TimeSpan или int.

Безопасность потоков и классы .NET

В группах новостей и почтовых рассылках я часто встречал вопрос: во всех ли классах System.* .NET обеспечена безопасность потоков? Отвечаю: нет, но так и должно быть. Если бы доступ к функциональности каждого класса был упорядочен, производительности системы был бы нанесен серьезный ущерб. Например, вообразите использование одного из классов-наборов, если бы он получал блокировку монитора при каждом вызове метода Add. А теперь допустим, что вы создаете экземпляр объекта-набора и добавляете к нему около тысячи объектов. Производительность при этом будет удручающей — вплоть до того, что систему будет невозможно использовать.

Правила использования потоков

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

Когда использовать потоки

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

Повышенный параллелизм

Очень часто приложениям требуется выполнять несколько задач одновременно. Например, однажды я писал систему хранения документации для банка, которая получала данные с оптических дисков, хранящихся в устройствах с автоматической сменой оптических дисков. Представьте себе огромные массивы данных, о которых здесь идет речь, и устройство с одним приводом и 50 сменными дисками, обслуживающее до нескольких гигабайт данных. Иногда на загрузку диска и поиск запрошенного документа уходило не менее 5—10 секунд. Стоит ли говорить, что мое приложение не соответствовало бы идеалам производительности, если бы во время выполнения всех этих действий оно блокировало бы пользовательский ввод! Поэтому для обработки пользовательского ввода я создал другой поток, который выполнял физическую работу по получению данных и обеспечивал продолжение работы пользователя. Он уведомлял главный поток о загрузке документа. Это очень удачный пример разделения труда по загрузке документа и обработке пользовательского ввода между двумя потоками.

Упрощенная структура

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

Лучшее использование процессорного времени

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

Когда лучше обойтись без потоков

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

Затраты больше выгод

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

Производительность для обоих случаев невозможно сравнить

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

Нет веской причины

Не следует рассматривать применение многопоточности как подход по умолчанию. Из-за врожденной сложности написания многопоточных приложений всегда следует ориентироваться на однопоточный код, если только у вас нет веской причины поступить иначе.

Подведем итоги

Многопоточность позволяет приложениям разделять задачи и решать независимо каждую из них, максимально эффективно используя процессорное время. Однако многопоточность является верным выбором не всегда и порой может замедлить работу приложения. Создание и управление потоками на С# осуществляется посредством класса System. Threading. Thread. Безопасность потоков является важным понятием, связанным с созданием и использованием потоков. Безопасность потоков означает, что члены объекта всегда поддерживаются в действительном состоянии при одновременном использовании несколькими потоками. Важно, чтобы наряду с изучением синтаксиса многопоточности вы также поняли, когда ее применять, а именно: для повышения параллелизма, упрощения структуры и оптимального использования процессорного времени.

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]