ЧАСТЬ   IV
ОСОБЫЕ ВОЗМОЖНОСТИ С#

ГЛАВА 15
Программирование многопоточности

С формальной точки зрения потоки не являются чем-то характерным для С#; поэтому многие книги по С# стремятся обойти этот вопрос. Хотя я старался не отклоняться от рассмотрения С#, многопоточность — это один из общих предметов, который должен быть хорошо знаком программисту при изучении этого нового языка. Конечно, в одной главе я не смогу раскрыть все вопросы, связанные с организацией потоков, но дам основы и даже расскажу о несколько более продвинутых вещах, касающихся аварийного завершения, планирования и управления временем жизни потоков. Мы также обсудим синхронизацию потоков с помощью классов System. Monitor и System.Mutex и оператор lock.

  Основы организации потоков

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

Так же обстоят дела с потоками и приложениями. Многопоточность позволяет приложениям разделять задачи и работать над каждой независимо, чтобы максимально эффективно задействовать процессор и пользовательское время. Однако если вы новичок в программировании многопоточности, хочу предупредить: не стоит выбирать многопточность для всех приложений, работа некоторых из них при этом может даже замедлиться. Важно, чтобы одновременно с изучением синтаксиса многопоточности вы также поняли, когда ее использовать. Чтобы помочь вам определить, когда следует идти по пути создания нескольких потоков в ваших приложениях, я поместил в конце главы раздел "Правила использования потоков". А сейчас давайте начнем с рассмотрения способа реализации потоков в Microsoft .NET Framework.

  Потоки и многозадачность

Поток является единицей обработки данных, а многозадачность — это одновременное исполнение нескольких потоков. Существует два вида многозадачности — совместная (cooperative) и вытесняющая (preemptive). Самые ранние версии Microsoft Windows поддерживали совместную многозадачность. Это означало, что каждый поток отвечал за возврат управления процессору, чтобы тот смог обработать другие потоки. Тот, кто раньше работал с другими ОС [в моем случае это IBM System/38 (CPF) и OS/2], может рассказать историю, как в один прекрасный день он "подвесил" компьютер, забыв поместить вызов PeekMessage, чтобы обеспечить обслуживание процессором других потоков в системе. Типичный ответ был: "Что поделаешь!"

Однако Microsoft Windows NT и (позже) Windows 95, Windows 98 и Windows 2000 стали поддерживать вытесняющую многозадачность, которую поддерживала OS/2. При этом процессор отвечает за выдачу каждому потоку определенного количества времени, в течение которого поток может выполняться, — кванта времени (timeslice). Далее процессор переключается между разными потоками, выдавая каждому потоку его квант времени, а программист может не заботится о том, как и когда возвращать управление, в результате чего могут работать и другие потоки. Поскольку .NET будет работать только в ОС с вытесняющей многозадачностью, то именно ей я уделю основное внимание.

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

  Переключение контекста

Неотъемлемый атрибут потоков — переключение контекста (context switching) — вызывает порой затруднения, поэтому позвольте мне привести его краткий обзор.

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

  Многопоточное приложение на С#

Прежде чем изучать способы использования потоков на С#, посмотрим, как легко создать вторичный поток на С#. Обсудив этот пример, я подробнее рассмотрю пространство имен System. Threading и особенно класс Thread. В этом примере я создаю второй поток в методе Main. Затем метод, ассоциированный со вторым потоком, выводит строку, сигнализирующую о вызове этого потока.

using System;

using System.Threading;

class SimpleThreadApp {

Public static void WorkerThreadMethodQ

{

Console.WriteLine("Worker thread started");

}

public static void Main() {

ThreadStart worker = new ThreadStart(WorkerThreadMethod);

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

Thread t = new Thread(worker); t.StartQ;

Console.WriteLine

("Main - Have requested the start of worker thread"); } }

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

Первый новый элемент, на который следует обратить внимание, — это оператор using в пространстве имен System. Threading. Мы еще ознакомимся с этим пространством имен, а сейчас достаточно понимать, что оно содержит классы, необходимые для организации потоков в среде .NET. А теперь взгляните на первую строку метода Main:

ThreadStart WorkerThreadMethod = new ThreadStart(WorkerThreadMethod);

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

х имя_переменной = new х(имя_метода)\

Итак, ThreadStart — это делегат, но необычный. Именно он должен быть задействован при создании нового потока. Он используется, чтобы задать метод, который должен вызываться как метод потока. Здесь я создаю экземпляр объекта Thread, при этом конструктор принимает в качестве аргумента только делегат ThreadStart:

Thread t = new Thread(worker);

После этого я вызываю метод Start объекта Thread, в результате чего вызывается метод WorkerThreadMethod.

И все! Три строки кода — для подготовки и запуска потока и самого метода потока — и все готово и работает. А теперь поговорим о пространстве имен System. Threading и его классах, благодаря которым все это происходит.

  Работа с потоками

Создание потоков и управление ими осуществляется с помощью класса System. Threading. Thread. С него мы и начнем.

  AppDomain

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

  Класс Thread

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

Создание потоков и объектов Thread

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

Управление временем существования потоков

Чтобы управлять активностью или временем жизни потока, надо решить массу задач. С Ътим позволяют справиться методы Thread. Например, довольно часто поток нужно приостановить. Это можно сделать, вызвав метод Thread. Sleep, который принимает единственный аргумент, представляющий собой время (в миллисекундах), на которое требуется приостановить поток. Заметьте: метод Thread.Sleep является статическим и не может быть вызван с экземпляром объекта Thread. На то есть веская причина. Не допускается вызывать Thread.Sleep для любого другого потока, кроме исполняемого в текущий момент. Статический метод Thread.Sleep вызывает статический же метод CurrentThread, который затем приостанавливает этот поток на указанное время. Пример:

using System;

using System.Threading;

class ThreadSleepApp

{

public static void WorkerThreadMethodQ

{

Console.WriteLine("Worker thread started");

int sleepTime = 5000;

Console.WriteLine("\tsleeping for {0} seconds",

sleepTime / 1000);

Thread.Sleep(sleepTime); // Приостановка на пять секунд. Console.WriteLine("\twaking up"); >

public static void Main()

{

ThreadStart worker = new ThreadStart(WorkerThreadMethod);

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

Thread t = new Thread(worker); t.Start();

Console.WriteLine

("Main - Have requested the start of worker thread"); } }

Есть еще два способа вызова метода Thread.Sleep. Первый — вызов Thread. Sleep со значением 0. При этом вы заставите текущий поток освободить неиспользованный остаток своего кванта. При передаче значения Timeout. Infinite поток будет приостановлен на неопределенно долгий срок, пока это состояние потока не будет прервано другим потоком, вызвавшим метод приостановленного потока Thread.Interrupt.

Второй способ приостановить исполнение потока — вызов метода Thread.Suspend. Между этими методиками несколько важных отличий. Во-первых, можно вызвать метод Thread. Suspend для потока, исполняемого в текущий момент, или для другого потока. Во-вторых, если таким образом приостановить выполнение потока, любой другой поток способен возобновить его выполнение с помощью метода Thread. Resume. Обратите внимание, что, когда один поток приостанавливает выполнение другого, первый поток не блокируется. Возврат управления после вызова происходит немедленно. Кроме того, единственный вызов Thread.Resume возобновит исполнение данного потока независимо от числа вызовов метода Thread.Suspend, выполненных ранее.

Уничтожение потоков

Уничтожить поток можно вызовом метода Thread.Abort. Исполняющая среда насильно завершает выполнение потока, генерируя исключение ThreadAbortException. Даже если метод попытается уловить ThreadAbort-Exception, исполняющая среда этого не допустит. Однако она исполнит код из блока finally потока, выполнение которого прервано, если этот блок присутствует. Это проиллюстрировано следующим кодом. Выполнение метода Main приостанавливается на 2 секунды, чтобы дать исполняющей среде время для запуска рабочего потока. После запуска рабочий поток считает до десяти, останавливаясь после каждого отсчета на секунду. Когда выполнение метода Main возобновляется после двухсе-кундной паузы, он прерывает выполнение рабочего потока. После этого исполняется блок finally.

using System;

using System.Threading;

class ThreadAbortApp {

public static void WorkerThreadMethodO {

try {

Console.WrlteLine("Worker thread started");

Console.WriteLine

("Worker thread - counting slowly to 10"); for (int i = 0; i < 10; i++) {

Thread. SleepdOOO};

Console.Writer{0}...", i); }

Console.WriteLinef'Worker thread finished"); }

catch(ThreadAbortException e)

{

}

finally

<

Console.WriteLine ("Worker thread -

I can't catch the exception, but I can cleanup"); } }

public static void Main() <

ThreadStart worker = new ThreadStart(WorkerThreadMethod);

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

Thread t = new Thread(worker); t.StartQ;

// Даем время для запуска рабочего потока. Console.WriteLine("Main - Sleeping for 2 seconds"); Thread.Sleep(2000);

Console.WriteLine("\nMain - Aborting worker thread"); t.AbortO; } }

Скомпилировав и запустив это приложение, вы получите такой результат:

Main - Creating worker thread

Main - Sleeping for 2 seconds

Worker thread started

Worker thread - counting slowly to 10

0...1...2...3...

Main - Aborting worker thread

Worker thread - I can't catch the exception, but I can cleanup

Вы должны отдавать себе отчет в том, что при вызове метода Thread.Abort выполнение потока не может остановиться сразу. Исполняющая среда ожидает, пока поток не достигнет той точки, которая в документации названа "безопасной точкой" ("safe point"). Поэтому, если ваша программа зависит от некоторых действий, которые происходят после прерывания потока, и вам надо быть уверенными в том, что поток остановлен, вы можете использовать метод Thread. Join. Это синхронный вызов, т. е. он не вернет управление, пока поток не будет остановлен. Напоследок обратите внимание, что после прерывания поток нельзя перезапустить. В этом случае, несмотря на то, что у вас есть действительный объект Thread, от него нет никакой пользы в плане выполнения кода.

  Планирование потоков

При переключении процессора по окончании выделенного потоку кванта времени, процесс выбора следующего потока, предназначенного для исполнения, далеко не произволен. У каждого потока есть приоритет, указывающий процессору, как должно планироваться выполнение этого потока по отношению к другим потокам системы. Для потоков, создаваемых в период выполнения, уровень приоритета по умолчанию равен Normal. Потоки, созданные не в период выполнения, сохраняют свой исходный приоритет. Для просмотра и установки этого значения служит свойство Thread.Priority. Установщик свойства Thread.Priority принимает аргумент типа Thread. ThreadPriority, который представляет собой enum, определяющий значения Highest, AboveNormal, Normal, BelowNormal и Lowest.

Чтобы проиллюстрировать, как приоритеты могут влиять даже на простейший код, взгляните на следующий пример, где один рабочий поток считает от 1 до 10, а другой — от 11 до 20. Обратите внимание на вложенный цикл в каждом методе WorkerThread. Циклы используются здесь для представления действий, которые выполняло бы настоящее приложение. Поскольку на самом деле эти методы ничего не делают, то без циклов каждый поток завершил бы свою работу в течение первого же своего кванта времени!

using System;

using System.Threading;

class ThreadSchedulelApp <

public static void WorkerThreadMethodK)

<

Console.WriteLine("Worker thread started");

Console.WriteLine

("Worker thread - counting slowly from 1 to 10"); for (int 1=1; i < 11; i++)

{

for (int j = 0; j < 100; j++) {

Console.Write(".");

// Код, который имитирует работу, подлежащую

// выполнению.

int a;

а = 15; }

Console.Write("{0}", i); }

Console.WriteLine("Worker thread finished"); }

public static void WorkerThreadMethod2() {

Console.WriteLine("Worker thread started");

Console.WriteLine

("Worker thread - counting slowly from 11 to 20"); for (int i = 11; i < 20; i++) <

for (int j = 0; j < 100; j++) {

Console.Write(".");

// Код, который имитирует работу, подлежащую // выполнению, int a; а = 15; }

Console.Write("{0}", i); >

Console.WriteLine("Worker thread finished"); }

public static void Main() {

ThreadStart workerl = 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(); } >

Запустив это приложение, вы получите следующее (для ясности я урезал выходную информацию — убрал большую часть точек):

Main - Creating worker threads

Worker thread started

Worker thread started

Worker thread - counting slowly from 1 to 10

Worker thread - counting slowly from 11 to 20

......1......11......2......12......3......13

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

using System;

using System.Threading;

class ThreadSchedule2App {

public static void WorkerThreadMethodK)

{

Console.WriteLine("Worker thread started");

Console.WriteLine

("Worker thread - counting slowly from 1 to 10"); for (int 1=1; i < 11; i++) <

for (int j = 0; j < 100; j++) {

Console.Write(".");

// Код, который имитирует работу, подлежащую

// выполнению.

int a;

a = 15; }

Console.Write("{0}", i); }

Console.WriteLine("Worker thread finished"); }

public static void WorkerThreadMethod2() {

Console.WriteLine("Worker thread started");

Console.WriteLine

("Worker thread - counting slowly from 11 to 20"); for (int i = 11; i < 20; i++) {

for (int j = 0; J < 100; J++) {

Console.Write(".");

// Код, который имитирует работу, подлежащую // выполнению, int a; а = 15; }

Console.Write("{0}", i); }

Console.WriteLine("Worker thread finished"); }

public static void Main() {

ThreadStart workerl = 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.Priority = ThreadPriority.Highest; t2.Priority = ThreadPriority.Lowest;

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

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

Main - Creating worker threads

Worker thread started

Worker thread started

Worker thread - counting slowly from 1 to 10

Worker thread - counting slowly from 11 to 20

......1......2......3......4......5......6......7......8......Э......

10......11......12......13......14......15......16......17......

18......19......20

Помните, что когда вы указываете процессору приоритет, который вы хотите установить для данного потока, то его значение в конечном счете используется ОС как часть алгоритма планирования распределения процессорного времени. В .NET этот алгоритм основан на уровнях приоритета, которые вы только что использовали (с помощью свойства Thread.Priority), а также на классе приоритета (priority class) процесса и значении динамического повышения (dynamic boost) приоритета. С помощью всех этих значений создается численное значение (для процессоров Intel — 0—31), представляющее приоритет потока. Потоки с наибольшим значением являются самыми высокоприоритетными.

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

ПРИМЕЧАНИЕ Несколько потоков с одинаковым приоритетом получают равное количество процессорного времени. Это называется циклическим планированием (round robin scheduling).

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

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

Как избежать подобных непредсказуемых состояний? На самом деле, как это обычно бывает в программировании, существует несколько способов решения этой хорошо известной проблемы. В этом разделе я расскажу о стандартном средстве — синхронизации. Синхронизация позволяет задавать критические секции (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. Безопасность потоков является важным понятием, связанным с созданием и использованием потоков. Безопасность потоков означает, что члены объекта всегда поддерживаются в действительном состоянии при одновременном использовании несколькими потоками. Важно, чтобы наряду с изучением синтаксиса многопоточности вы также поняли, когда ее применять, а именно: для повышения параллелизма, упрощения структуры и оптимального использования процессорного времени.