понедельник, 1 февраля 2010 г.

Многопоточность

Данная статья описывает реализацию и использование многопоточности в среде .NET Framework. Код для данной статьи и саму статью в формате Microsoft Word можно найти здесь.

Оглавление

Многопоточность. 1

Оглавление. 1

Задачи многопоточности. 2

Методы создания потоков. 3

Делегаты.. 3

Ожидание завершения работы потока. 4

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

Полезный совет. 6

Класс Thread. 6

Ожидание завершения потока. 7

Управление выполнением потока. 8

«Сон» потока. 9

Приоритет потоков. 9

Фоновые потоки и потоки «переднего плана». 9

Класс ThreadPool 10

Класс Task. 11

Создание потоков. 11

Ожидание завершения потока. 12

Получение результатов выполнения потока. 14

Прерывание потока. 14

PLINQ.. 14

Класс Parallel 15

Метод AsParallel(). 15

Синхронизация потоков. 16

Оператор lock. 16

Класс ReaderWriterLock. 18

Класс Mutex. 19

Класс WaitHandle. 20

Класс AutoResetEvent. 20

Класс ManualResetEvent. 23

Блокировка потоков. 23

Взаимодествие с пользовательским интерфейсом.. 24

WinForms. 24

Метод Invoke. 25

Использование SynchronizationContext. 25

Класс BackgroundWorker. 26

WPF. 29

Объект Dispatcher. 29

Класс BackgroundWorker. 29

Заключение. 30

Задачи многопоточности

Многопоточность представляет собой возможность выполнять несколько кусков кода «параллельно». Слово параллельно заключено в кавычки потому, что на самом деле процессор компьютера способен одновременно выполнять только одну инструкцию. Поэтому в каждый момент времени на нем выполняется только один поток. Соответственно двухъядерный процессор способен одновременно выполнять 2 потока и т.д.

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

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

Так какие же преимущества дает нам многопоточность в этом случае? Она не ускоряет работу, она сокращает время отклика системы. Если вы хотите выполнить какую-то длительную операцию, то выполнение ее в том же потоке, в котором происходит взаимодействие с пользователем (потоке пользовательского интерфейса) приведет к «зависанию» системы. Т.е. до тех пор, пока задача не будет выполнена, система не будет реагировать на действия пользователя (программа ThreadingSingleThreadBlocking). Если же вы выполняете данную длительную задачу в отдельном потоке, то за счет того, что операционная система время от времени передает управление потоку пользовательского интерфейса, он имеет возможность обрабатывать действия пользователя. Особенно это важно при работе с оконным интерфейсом Windows. Если загрузить поток интерфейса какой-либо задачей, то окна перестанут отрисовываться.

Какие же требования обычно предъявляются к механизмам, обеспечивающим поддержку многопоточности в языке программирования?

1. Конечно же необходимы простые средства создания отдельных потоков кода.

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

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

4. Иногда необходимо приостановить исполнение потока, например, чтобы освободить используемые им вычислительные ресурсы.

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

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

Методы создания потоков

Здесь описаны основные методы создания потоков в .NET Framework.

Делегаты

Наверное наиболее простым методом выполнения какого-либо метода в отдельном потоке является использование делегата (проект Delegates). Пусть у нас есть метод, выполняющий какую-либо длительную задачу (класс Exercise1):

private void CheckPrimeNumber(long checkNumber){…}

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

private delegate void CheckPrimeNumberDelegate(long checkNumber);

Затем создайте объект делегата, передав его конструктору ваш метод, который вы хотите выполнить в отдельном потоке:

CheckPrimeNumberDelegate dlgt = new CheckPrimeNumberDelegate(CheckPrimeNumber);

Запуск выполнения метода в новом потоке осуществляется вызовом метода BeginInvoke делегата.

dlgt.BeginInvoke(1000000021, null, null);

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

Ожидание завершения работы потока

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

private static long GetDivider(long checkNumber){…}

Очевидно, нам необходимо знать две вещи:

· Когда поток завершил работу?

· Как получить результат, возвращенный функцией, выполнявшейся в этом потоке?

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

private delegate long GetDividerDelegate(long checkNumber);

GetDividerDelegate dlgt = new GetDividerDelegate(GetDivider);

Однако теперь нас интересует объект, возвращаемый методом BeginInvoke:

IAsyncResult res = dlgt.BeginInvoke(1000000021, null, null);

Первый способ дождаться завершения созданного нами потока – проверять свойство IsCompleted объекта IAsyncResult:

while (!res.IsCompleted)

{

continue;

}

Внутри данного цикла вы можете производить какую-либо работу в основном потоке.

Второй подход заключается в использовании объекта, возвращаемого свойством AsyncWaitHandle объекта IAsyncResult (класс Exercise3):

res.AsyncWaitHandle.WaitOne();

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

В связи с этим наверно самым удобным способом получения уведомления о том, что ваш поток завершился, является делегат обратного вызова (класс Exercise4). Этот делегат передается одним из параметров метода BeginInvoke:

dlgt.BeginInvoke(1000000021, new AsyncCallback(GetDividerFinished), null);

Сигнатура этого метода:

private static void GetDividerFinished(IAsyncResult res){…}

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

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

Теперь, когда мы дождались завершения работы нашего потока, пришло время получить результат его работы. Для этого служит метод EndInvoke делегата, использовавшегося для вызова BeginInvoke. Метод EndInvoke возвращает тот же объект, что вернул и выполнявшийся в отдельном потоке метод. Учтите, что эти методы должны вызываться у одного и того же экземпляра делегата. Методу EndInvoke передается объект IAsyncResult, возвращенный методом BeginInvoke (класс Exercise5):

Console.WriteLine("Divider of {0} is {1}.", 1000000021 ,dlgt.EndInvoke(res));

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

Если вы использовали IsCompleted или AsyncWaitHandle для ожидания окончания работы потока, то все в порядке. У вас есть и делегат и IAsyncResult. Но при использовании делегата обратного вызова есть проблема. Ему передается IAsyncResult в качестве параметра, но делегата, инициировавшего поток у него нет. Для того, чтобы получить его, используется обходной маневр (класс Exercise6):

AsyncResult aRes = res as AsyncResult;

GetDividerDelegate dlgt = aRes.AsyncDelegate as GetDividerDelegate;

Теперь можно спокойно вызывать EndInvoke. Если же такой искусственный подход вам не нравится, есть еще один способ (класс Exercise7). Методу BeginInvoke в последнем параметре вы можете передать любой объект. В последствии этот объект будет доступен через свойство AsyncState объекта IAsyncResult. Вы можете передать в качестве этого параметра сам делегат:

dlgt.BeginInvoke(1000000021, new AsyncCallback(GetDividerFinished), dlgt);

а затем в делегате обратного вызова извлечь его:

GetDividerDelegate dlgt = res.AsyncState as GetDividerDelegate;

Теперь вы так же можете вызывать EndInvoke.

Таким образом создание потоков с помощью делегатов имеет следующие достоинства и недостатки:

1. Легко создать поток на основе любого метода.

2. Легко дождаться завершения работы потока как синхронно (с блокированием ожидающего потока), так и асинхронно (без блокирования).

3. Можно получить результат работы метода, вызываемого в отдельном потоке.

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

Полезный совет

Неудобно создавать для каждого вашего метода отдельный делегат. К счастью Microsoft позаботилась о нас в этом плане. В состав .NET Framework входят два generic-делегата Action и Func. Первый из них позволяет описать метод, не возвращающий параметров, второй – метод, возвращающий результат (класс Exercise8).

Класс Thread

Вторым способом создания отдельного потока является использование класса Thread. Необходимо создать объект класса Thread (проект Threads, класс Exercise1):

Thread trd = new Thread(new ThreadStart(CheckPrimeNumber));

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

trd.Start();

Отсутствие возможности передать методу, который будет выполняться в отдельном потоке, каких либо параметров, несомненно является ограничением. Поэтому существует несколько иной синтаксис создания потока, который позволяет передать вашему методу параметры (класс Exercise2). При этом в конструктор класса Thread передается не делегат ThreadStart, а делегат ParameterizedThreadStart. Он описывает метод с одним параметром типа object:

Thread trd = new Thread(new ParameterizedThreadStart(CheckParameterPrimeNumber));

Теперь методу Start передается параметр, который будет передан вашему методу при выполнении его в отдельном потоке:

trd.Start(1000000021);

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

private static void CheckParameterPrimeNumber(object state)

{

long checkNumber = Convert.ToInt64(state);

Таким образом вы можете передавать в ваш метод несколько параметров, упаковав их в отдельный объект.

Недостатком такого подхода является отсутствие строгой типизации. Ничто не помешает мне вызвать метод Start с параметром типа string. Компилятор пропустит это, не выдав сообщения об ошибке. Тем не менее во время исполнения ошибка произойдет, поскольку string не может быть приведен к long. Стандартным методом решения этой проблемы является использование класса-обертки (класс Exercise3). Вы выносите метод, который вы хотите запустить в отдельном потоке, и параметры, необходимые для его работы, в отдельный класс:

class PrimeChecker

{

long checkNumber;

public PrimeChecker(long checkNumber)

{ this.checkNumber = checkNumber; }

public void Check()

{

for (long d = 2; d <>

{

if (checkNumber % d == 0)

{

Console.WriteLine("{0} is not prime. Divider is {1}.", checkNumber, d);

return;

}

}

Console.WriteLine("{0} is prime.", checkNumber);

}

}

Создание вашего потока теперь выглядит следующим образом:

PrimeChecker pc = new PrimeChecker(1000000021);

Thread trd = new Thread(new ThreadStart(pc.Check));

trd.Start();

Этот прием позволяет сохранить строгую типизацию.

Ожидание завершения потока

Класс Thread предоставляет только синхронные методы ожидания завершения потока. Первый из них – свойство IsAlive (класс Exercise4):

while (trd.IsAlive)

{ continue; }

Внутри данного цикла вы можете делать какую-либо работу. Другой подход – использование метода Join (класс Exercise5).

trd.Join();

Вызов этого метода блокирует вызвавший его поток до тех пор, пока поток trd не закончит свою работу.

Недостатком этого метода несомненно является блокирование основного потока. К сожалению класс Thread не предоставляет возможности узнать о завершении работы потока с помощью делегата обратного вызова.

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

Класс Thread предоставляет возможности приостановить выполнение потока, снова запустить его и совсем прервать поток. Рассмотрим их. Приостановка и продолжение выполнения потока осуществляются вызовом методов Suspend и Resume (класс Exercise6). Следует отметить, что Microsoft считает эти методы устаревшими и не рекомендует их использовать. Поэтому вам не следует прибегать к их услугам без крайней необходимости. Данные методы вызывают исключение в случае если поток не запущен или уже завершен. Так же метод Resume вызывает исключение в случае, если поток не приостановлен методом Suspend.

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

Первый метод, который прерывает поток – Abort (класс Exercise7).

trd.Abort();

Вызов этого метода приводит к генерации в соответствующем потоке исключения ThreadAbortException. Задача программиста – обработать его и правильно завершить поток:

private static void CheckPrimeNumberWithAbort()

{

try

{

}

catch (ThreadAbortException)

{

Console.WriteLine("Thread is aborted");

}

}

Особенностью, о которой следует знать, является то, что вызов метода Abort, когда нить находится в приостановленном состоянии приводит к появлению исключения. Поэтому вам придется вызвать Resume перед использованием Abort (класс Exercise8).

Вторым методом, который прерывает работу нити, является метод Interrupt. Его отличие от Abort заключается в том, что он прерывает работу не любого потока, а только того, который находится в состоянии ожидания (например, ожидает завершения работы другого потока, вызвав его метод Join) или «спит». Последний термин будет подробнее рассмотрен ниже. Если же поток не находится в таком состоянии, метод Interrupt ничего не сделает. Таким образом Interrupt прерывает скорее «сон» или ожидание потока, чем его самого. При этом в самом потоке возникает исключение ThreadInterruptedException, которое вы можете обработать (класс Exercise9).

«Сон» потока

Настало время рассмотреть, что же такое – «сон» потока. Статический метод Sleep класса Thread заставляет вызвавший его поток «заснуть». Т.е. то время, которое указано в параметре метода, поток не будет работать, ему не будут передаваться кванты времени. Как же это реализуется? Как уже говорилось выше, операционная система поддерживает список активных потоков. Но кроме него существует еще и список спящих потоков. Вызов метода Sleep помещает поток в этот список. Потоки в этом списке не участвуют в распределении квантов времени. Они ждут завершения времени своего сна. После этого поток снова перемещается в список активных потоков.

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

Единственным исключением из описанного механизма работы метода Sleep является его вызов с параметром 0. В этом случае поток не перемещается в список спящих потоков. Просто его выполнение немедленно прерывается и его квант времени передается другому потоку. Но поток попрежнему остается в списке активных потоков и участвует в распределении времени.

Приоритет потоков

Класс Thread имеет экземплярное свойство Priority, задающее приоритет потока. Его значением является член перечисления ThreadPriority (класс Exercise10). Названия членов этого перечисления говорят сами за себя. Значение Normal устанавливается по умолчанию. Вы можете сделать приоритет потока больше или меньше. Но что же такое этот приоритет? Может показаться, что потоки с большим приоритетом просто чаще получают кванты времени для своего выполнения. Но на самом деле это не так. На самом деле операционная система всегда выбирает из списка активных потоков поток с наибольшим приоритетом. Поэтому пока в списке активных потоков есть потоки, приоритет которых выше других, будут выполняться только они. Передать выполнение потокам с более низким приоритетом можно, вызвав метод Sleep в потоках с высоким приоритетом. Как уже говорилось, в этом случае данные потоки будут перемещены из списка активных потоков в список спящих потоков и не будут участвовать в распределении квантов времени.

Фоновые потоки и потоки «переднего плана»

У класса Thread есть еще одно свойство, о котором я хотел бы рассказать. Это свойство IsBackground (класс Exercise11). По умолчанию оно имеет значение false. Данное свойство определяет, является ли поток фоновым (background) или потоком «переднего плана». Как уже сказано, по умолчанию поток является потоком «переднего плана». Что же это такое? В системе Windows существует понятие процесса. Процесс обладает своей изолированой от других процессов памятью. Таким образом разделение на процессы повышает стабильность работы системы, поскольку один процесс не может изменять память другого процесса. Каждый процесс при старте обладает одним потоком. Этот поток называется основным. Однако этот основной поток может порождать другие (вторичные) потоки. Рассмотрим, что произойдет, если основной поток уже завершил свою работу, но есть выполняющийся вторичный поток. Если вторичный поток является потоком «переднего плана», то процесс не будет завершен до тех пор, пока не завершит работу этот вторичный поток. Если же вторичный поток является фоновым, то его работа будет остановлена, и процесс будет завершен. Таким образом фоновые потоки могут не завершить свою работу при закрытии приложения. Поэтому нельзя выносить в фоновые потоки обработку критических данных.

Пришло время подвести итоги по использованию класса Thread. Он имеет следующие достоинства и недостатки:

1. Легко создавать отдельные потоки.

2. Существуют легкие в использовании механизмы, позволяющие управлять выполнением потока: приостанавливать его, запускать снова, прерывать поток.

3. Легко проводить тонкую настройку потока: задавать приоритет, определять, является ли поток фоновым.

4. Немного затруднен механизм передачи параметров в метод потока.

5. Имеются только синхронные способы дождаться завершения потока.

Класс ThreadPool

Как уже говорилось ранее, если вы создадите слишком много потоков, то операционная система будет тратить слишком много времени на переключение между ними. С другой стороны так же уменьшится число квантов времени, выделяемых данному конкретному потоку за определенный интервал времени (например, за минуту). Все это может привести к резкому падению производительности не только приложения, которое использует множество потоков, но и всех других приложений на этом компьютере. Для частичного решения этой проблемы .NET предлагает класс ThreadPool (проект ThreadPools).

Класс ThreadPool имеет ряд возможностей. Он может производить ожидание каких-либо событий от вашего лица, но здесь мы рассмотрим только создание нового потока. Это делается очень просто:

ThreadPool.QueueUserWorkItem(new WaitCallback(CheckPrimeNumber), 1000000021);

Методу QueueUserWorkItem передается делегат WaitCallback описывающий не возвращающий значений метод с одним параметром типа object. Второй параметр метода QueueUserWorkItem – именно то, что будет передано методу делегата WaitCallback. Мы уже рассматривали использование этого параметра для передачи различных данных в метод, выполняемый в отдельном потоке. Все те же методики действенны и здесь.

За счет чего же класс ThreadPool обеспечивает нам выигрыш в производительности? Этот класс создает сразу же несколько потоков (по умолчанию 25 на каждый процессор компьютера). Но это пока неактивные потоки, они не выполняют никакой код, под них просто выделены ресурсы. Когда вы вызываете метод QueueUserWorkItem, ThreadPool выбирает очередной неактивный поток и выполняет в нем ваш метод. По завершению работы вашего метода поток возвращается в пул неактивных потоков. Таким образом происходит переиспользование потоков. Их не приходится создавать и уничножать каждый раз. Если же вы поставили в очередь слишком много методов на выполнение в отдельных потоках, и в пуле не осталось свободных неактивных потоков, то ThreadPool создаст новые потоки для вас. Но новый поток создается не чаще, чем раз в пол секунды. Это гарантирует, что не будет предпринята попытка создать сразу много потоков. Кроме того, за очередные пол секунды какой-либо из выполняемых методов может закончить свою работу и освободить поток. Тогда создание нового потока не потребуется. В общем, Microsoft рекомендует использовать ThreadPool вместо Thread когда это только возможно. Большинство технологий Microsoft, созданных в рамках .NET, используют именно его.

Подведем итоги. Класс ThreadPool имеет следующие достоинства и недостатки:

1. Наверно простейший способ создать отдельный поток.

2. Оптимизирован для работы со множеством потоков.

3. Практически никаких механизмов управления или ожидания завершения работы.

Класс Task

С появлением .NET Framework 4.0 Microsoft предоставляет нам новые возможности по созданию и управлению многопоточными приложениями. Ряд новых классов для работы с потоками был введен в пространстве имен System.Threading.Tasks (проект Tasks).

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

Начнем рассмотрение этого пространства имен с класса Task. Наиболее простым способом создания потока с использованием этого класса является вызов метода StartNew у статического свойства Factory (класс Exercise1):

Task.Factory.StartNew(CheckPrimeNumber);

Свойство Factory возвращает объект класса TaskFactory. Этот объект отвечает за создание объектов типа Task.

У метода StartNew есть много перезагрузок, позволяющих создавать потоки, которые возвращают результаты, потоки, которым передаются параметры и т.д (класс Exercise2):

Task.Factory.StartNew(CheckParameterPrimeNumber, 1000000021);

Ожидание завершения потока

Методы класса TaskFactory, и StartNew в том числе, возвращают объект класса Task. Этот объект предоставляет информацию о потоке. В числе прочих в него входит свойство IsComplete, с помощью которого можно проверить, завершился ли поток (класс Exercise3):

Task task = Task.Factory.StartNew(CheckParameterPrimeNumber, 1000000021);

while (!task.IsCompleted)

{ continue; }

Аналогичного результата можно достичь, вызвав метод Wait (класс Exercise4):

task.Wait();

Как обычно при этом выполнение вызывающего потока приостанавливается до завершения выполнения потока задачи.

Однако данный класс предоставляет и возможность асинхронно дождаться завершения выполнения потока. Эта функциональность аналогично той, что используют делегаты. Для этого используется метод FromAsync класса TaskFactory (класс Exercise5):

Action dlgt = CheckPrimeNumber;

Task.Factory.FromAsync(dlgt.BeginInvoke, GetDividerFinished, null);

Синтаксис этого метода несколько запутан, но все же позволяет осуществить задуманное. После завершения потока будет вызван указанный метод GetDividerFinished. Такой сложный синтаксис обусловлен тем, что данный подход обычно используется для вызова методов асинхронного чтения/записи у потоков. Например, следующий код иллюстрирует, как асинхронно прочитать файл (класс Exercise6):

FileInfo fi = new FileInfo("TextFile1.txt");

byte[] data = new byte[fi.Length];

FileStream fs = File.OpenRead("TextFile1.txt");

Task<int> task = Task<int>.Factory.FromAsync(fs.BeginRead, fs.EndRead, data, 0, data.Length, null);

Однако класс Task предоставляет более простой и мощный способ асинхронно дождаться завершения выполнения потока: присоединение задач. Этот механизм позволяет начать выполение потока после того, как один или несколько потоков завершат свое выполнение. Работает этот механизм с помощью метода ContinueWith (класс Exercise7):

Task task = new Task(CheckPrimeNumber);

task.ContinueWith(GetDividerFinished);

task.Start();

Этому методу передается делегат, ссылающийся на метод, принимающий экземпляр Task в качестве параметра:

private static void GetDividerFinished(Task finished)

Этот метод будет выполнен после того, как закончит свою работу задача. Кроме того, класс TaskFactory имеет методы ContinueWhenAll и ContinueWhenAny, позволяющие выполнить ваш метод после того, как завершатся все задачи из списка, или после того, как завершится любая задача из списка.

Такое выстраивание асинхронных методов в цепочку является в ряде случаев весьма полезным. Рассмотрим следующий пример (класс Exercise8). Пусть наше приложение принимает некоторые данные от Web-сервиса в интернет. Получение данных при этом должно выполняться асинхронно. Кроме того, для сокращения объема передаваемых данных они сжаты, поэтому после получения данные должны быть разжаты. Операция разжатия данных так же должна выполняться асинхронно. Для выполнения данных действий раньше приходилось создавать сложные цепочки делегатов, вызывать методы асинхронного исполнения один из другого. Теперь эта задача решается проще:

private static byte[] CompressedData;

private static string UncompressedResult;

public static void Run()

{

Task task = new Task(GetCompressedDataFromWebService);

task.ContinueWith(UncompressData);

task.Start();

}

private static void GetCompressedDataFromWebService()

{

// code to get data from Web service.

Console.WriteLine("Data were obtained from Web service.");

}

private static void UncompressData(Task finished)

{

// code to decompress obtained data.

Console.WriteLine("Data were uncompressed.");

}

Как видите, код вполне прямолинейный. На самом деле отдельные методы в цепочке могут ничего не знать друг о друге. Это несомненное достоинство и упрощение. Метод ContinueWith сам возвращает экземпляр класса Task, что позволяет строить цепочки произвольной длины.

Получение результатов выполнения потока

Класс Task позволяет получить результаты выполнения потока. Для этого используется generic-версия этого класса Task<T> (класс Exercise9). Пусть у нас есть метод, возвращающий некоторое значение:

private static long GetDivider(object state)

И мы хотим исполнить его асинхронно и получить результат. Для этого создается экземпляр класса Task<long>:

Task<long> task = Task<long>.Factory.StartNew(GetDivider, 1000000021);

Затем необходимо любым способом дождаться завершения выполнения задачи и получить результат через свойство Result:

Console.WriteLine("Divider of {0} is {1}.", 1000000021, task.Result);

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

Синтаксис прерывания потока пока не устоялся, поэтому мы пока не будем его рассматривать.

Подведем итоги. Класс Task имеет следующие достоинства и недостатки:

1. Простой способ создания отдельных потоков.

2. Существуют синхронные и асинхронные механизмы ожидания завершения потока.

3. Есть возможность отменить выполнение потока.

4. Существует легкая возможность составлять цепочки из потоков.

5. Легко получить результат выполнения метода потока.

6. Немного затруднен механизм передачи типизированных параметров в поток.

7. Невозможно приостанавливать поток и запускать его снова.

PLINQ

.NET Framework 4.0 принес с собой еще одну возможность, связанную с многопоточностью. Она связана с обработкой коллекций (проект PLINQ). Бывают случаи, когда нам нужно обработать в отдельных потоках элементы какой-либо коллекции. Раньше нам пришлось бы создавать цикл, в котором вызывать новый поток для каждого из элементов. Теперь .NET Framework предлагает упрощенный синтаксис.

Класс Parallel

Прежде всего, в .NET Framework 4.0 появился класс Parallel, который имеет методы For, ForEach и Invoke. Как несложно понять, первые два из них занимаются тем, что исполняют некоторый метод в цикле (класс Exercise1). Первому из них передаются границы цикла, а второму – коллекция:

Parallel.For((long)(1000000021 - 20), (long)(1000000021 + 20), CheckParameterPrimeNumber);

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

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

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

Метод Invoke в качестве параметра принимает коллекцию делегатов, каждый из которых он выполнит в отдельном потоке (класс Exercise2):

Parallel.Invoke(

() => CheckParameterPrimeNumber(1000000020),

() => CheckParameterPrimeNumber(1000000021),

() => CheckParameterPrimeNumber(1000000022),

() => CheckParameterPrimeNumber(1000000023)

);

Этот метод так же вернет управление только после завершения выполнения всех своих делегатов.

Метод AsParallel()

Кроме указанного класса Parallel Microsoft добавил похожую поддержку многопоточности в LINQ. Осуществляется она с помощью метода AsPartallel (класс Exercise3). Предположим, что нам нужно найти все простые числа из некоторого списка. Ранее мы могли бы сделать это так:

var primes = from v in values where IsPrime(v) select v;

Теперь, когда мы захотели бы перечислить все простые числа в цикле foreach, наш метод IsPrime последовательно вызывался бя для каждого из чисел списка values.

Теперь же мы можем заставить систему параллельно проверять несколько чисел сразу:

var primes = from v in values.AsParallel() where IsPrime(v) select v;

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

Синхронизация потоков

В реальном приложении потоки часто используют совместные ресурсы. Это означает, в частности, что несколько потоков сразу могут обращаться и изменять одну и ту же переменную. Зачастую это может приводить к нежелательным последствиям. Рассмотрим следующий пример. Есть 3 потока. Первый из них добавляет элементы в некоторый список. Остальные 2 потока удаляют элементы из этого списка. Чтобы удаление произошло правильно, необходимо, чтобы в списке был хотя бы один элемент. Поэтому удаляющие потоки используют следующий код:

if (list.Count > 0)

list.RemoveAt(0);

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

Квант времени

Удаляющий поток 1

Результат работы потока 1

Удаляющий поток 2

Результат работы потока 2

1

if (list.Count > 0)

true



2



if (list.Count > 0)

true

3



list.RemoveAt(0);

Ok

4

list.RemoveAt(0);

Exception



Поэтому существует необходимость синхронизировать (упорядочивать) работу потоков, использующих одни и те же данные. Здесь мы рассмотрим механизмы, существующие для этого в .NET Framework (проект Synchronization).

Оператор lock

Наиболее простым способом синхронизации работы потоков является использование оператора lock. Пусть вы хотите реализовать шаблон «одиночка». Он позволяет иметь в приложении только один экземпляр класса. Стандартная реализация этого шаблона выглядит следующим образом:

public class Singleton

{

private static Singleton _instance = null;

private Singleton() { }

public static Singleton Instance

{

get

{

if (_instance == null)

{

_instance = new Singleton();

}

return _instance;

}

}

}

Поскольку конструктор класса является закрытым, то получить экземпляр класса возможно только через статическое свойство Instance. Внутри этого свойства создается единственный экземпляр класса, который возвращается всем запрашивающим. Однако данная реализация имеет изъян. Если два потока одновременно вызовут свойство Instance, одновременно проверят, что _instance == null, им обоим будет указано, что экземпляр еще не создан. После этого каждому из них будет создан и возвращен собственный экземпляр класса Singleton, а переменная _instance будет иметь ссылку на последний созданный объект. Таким образом в приложении появятся 2 экземпляра класса Singleton. Но это не то поведение которое нам нужно. Поэтому код свойства Instance нужно модифицировать следующим образом (класс Exercise1):

private static object _locker = new object();

public static Singleton Instance

{

get

{

if (_instance == null)

{

lock (_locker)

{

if (_instance == null)

{

_instance = new Singleton();

}

}

}

return _instance;

}

}

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

Рассмотрим, как использование lock позволяет решить нашу проблему. Оба потока проверят _instance и узнают, что экземпляр Singleton еще не создан. Тогда они перейдут к блоку lock. Только один из потоков успеет первым заблокировать _locker, а второй поток останется ждать, пока этот объект не будет разблокирован. Первый поток успешно создаст экземпляр Singleton и покинет блок lock. Только в этот момент второй поток сможет войти в этот блок. Теперь становится ясна необходимость во второй проверке переменной _instance внутри блока lock. Если бы этой проверки не было, второй поток, войдя в блок, создал бы еще один экземпляр Singleton, что недопустимо.

В заключении рассмотрения работы оператора lock, хочу привести один полезный прием. Иногда у вас нет объекта вроде _locker, который можно было бы заблокировать. В этом случае можно использовать следующий синтаксис:

lock (typeof(Singleton))

Класс ReaderWriterLock

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

private static Dictionary<int, int> m_Cache = new Dictionary<int, int>();

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

Здесь на помощь приходит класс ReaderWriterLock (класс Exercise2). Все потоки, обращающиеся к одному ресурсу, должны использовать один экземпляр этого класса. Данный класс имеет методы получения доступа к ресурсу AcquireReaderLock и AcquireWriterLock, а так же соответствующие методы освобождения: ReleaseReaderLock и ReleaseWriterLock.

Работа с классом происходит следующим образом. Поток, который хочет получить доступ только на чтение, вызывает метод AcquireReaderLock. Этому методу в качестве параметра передается время, которое поток должен дожидаться получения соответствующего доступа. Если за это время доступа получить не удалось, то возникает ApplicationException. Вы так же можете передать этому методу параметр Timeout.Infinite. В этом случае ожидание будет длиться бесконечно. Экземпляр класса ReaderWriterLock разрешит доступ только в том случае, если у него есть только запросы на чтение, если же есть хотя бы один запрос на запись, то доступ предоставлен не будет до тех пор, пока все запросы на запись не будут удовлетворены. После завершения чтения поток должен обязательно вызвать метод ReleaseReaderLock. Этим он сигнализирует, что освободил ресурс. Для использования методов AcquireReaderLock и ReleaseReaderLock очень подходит блок tryfinally:

try

{

locker.AcquireReaderLock(Timeout.Infinite);

if (m_Cache.ContainsKey(key))

Console.WriteLine( "For key {0} reader value is {1}.", key, m_Cache[key]);

}

finally

{

locker.ReleaseReaderLock();

}

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

Поток, которому нужно производить запись, вызывает метод AcquireWriterLock. Смысл его параметра тот же. Этот метод будет ждать до тех пор, пока не будут удовлетворены все запросы на запись и чтение, которые были сделаны до его вызова. Потом он заблокирует объект до его освобождения методом ReleaseWriterLock. Пока объект заблокирован таким образом, никто не может получть доступ ни на запись, ни на чтение:

try

{

locker.AcquireWriterLock(Timeout.Infinite);

if (m_Cache.ContainsKey(key))

Console.WriteLine("For key {0} reader value is {1}.", key, m_Cache[key]);

else

{

m_Cache[key] = key * key;

Console.WriteLine("For key {0} written value is {1}.", key, m_Cache[key]);

}

}

finally

{

locker.ReleaseWriterLock();

}

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

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

Класс Mutex

Как видно из предыдущего примера, оператор lock и класс ReaderWriterLock позволяют эффективно производить синхронизацию потоков приложения. Однако иногда требуется синхронизация не только в рамках одного процесса, но и в пределах нескольких процессов. Классическим примером является создание приложения, у которого может быть запущен только единственный экземпляр. Несомненно, .NET имеет более гибкие механизмы, обеспечивающие подобное поведение приложения, но мы рассмотрим простейший способ, подразумевающий использование класса Mutex (класс Exercise3).

bool isCreated;

Mutex mtx = new Mutex(true, "SingleThreadingMutex", out isCreated);

if (!isCreated)

{

Console.WriteLine("Application is already running.");

Console.ReadLine();

return;

}

Console.WriteLine("Application works.");

Console.ReadLine();

mtx.Close();

Рассмотрим его применение. Конструктор класса Mutex имеет несколько перегруженных версий. Используемая нами принимает 3 параметра. Первый из них говорит, должны ли мы сразу получить мьютекс в эксклюзивное пользование (владеть им). В этом случае класс Mutex создает внутри себя глобальный для операционной системы объект с заданным именем, которое передается в качестве второго параметра конструктора. Именно потому, что этот объект является глобальным для операционной системы, мьютекс годен для синхронизации между процессами. Если такой объект с этим имененм уже существует, то создать его заново не удастся. Об этом сигнализирует третий параметр конструктора. Таким образом, только запущенный первым экземпляр приложения сумеет создать мьютекс. Всем остальным это не удастся и они получат соответствующее уведомление через переменную isCreated.

После окончания работы вашего приложения мьютекс нужно закрыть:

mtx.Close();

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

Кроме описанных выше возможностей класс Mutex является наследником класса WaitHandle и обладает всеми его возможностями.

Класс WaitHandle

Класс WaitHandle разработан для поддержки потоками ожидания завершения различных задач. Сам этот класс является абстрактным, но имеет несколько полезных методов. К ним относятся WaitAll, позволяющий дождаться освобождения всех объектов WaitHandle, переданных ему в массиве в качестве параметра, и WaitAny, позволяющий дождаться первого освобождения одного из объектов WaitHandle, переданных ему в массиве в качестве параметра.

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

Класс AutoResetEvent

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

public class Integrator

{

///

/// Класс отрезка интегрирование

///

private class IntegrationRange

{

public double Start { get; set; }

public double Finish { get; set; }

}

private double firstPartResult = 0;

private double secondPartResult = 0;

private double tolerance = 0.00000001;

private WaitHandle[] handles = new WaitHandle[]

{

new AutoResetEvent(false),

new AutoResetEvent(false)

};

public double GetIntegral(double start, double finish)

{

double center = (start + finish) / 2.0;

ThreadPool.QueueUserWorkItem(CalcFirstPart, new IntegrationRange() { Start = start, Finish = center });

ThreadPool.QueueUserWorkItem(CalcSecondPart, new IntegrationRange() { Start = center, Finish = finish });

WaitHandle.WaitAll(handles);

return firstPartResult + secondPartResult;

}

private void CalcFirstPart(object state)

{

IntegrationRange range = (IntegrationRange)state;

firstPartResult = CalcIntegral(range.Start, range.Finish);

(handles[0] as AutoResetEvent).Set();

}

private void CalcSecondPart(object state)

{

IntegrationRange range = (IntegrationRange)state;

secondPartResult = CalcIntegral(range.Start, range.Finish);

(handles[1] as AutoResetEvent).Set();

}

private double CalcIntegral(double start, double finish)

{

double i1 = 0;

double i2 = 0;

int n = 100;

do

{

i1 = i2;

i2 = 0;

for (int i = 0; i <>

{

double x = start + (finish - start) * i / n;

i2 += (x * x) * (finish - start) / n;

}

n *= 2;

}

while (Math.Abs(i1 - i2) > tolerance);

return i2;

}

}

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

ThreadPool.QueueUserWorkItem(CalcFirstPart, new IntegrationRange() { Start = start, Finish = center });

ThreadPool.QueueUserWorkItem(CalcSecondPart, new IntegrationRange() { Start = center, Finish = finish });

Далее метод GetIntegral ожидает завершение работы обоих этих потоков. Для этого он использует метод WaitAll класса WaitHandle. Как уже говорилось, этот метод ожидает, пока все переданные ему в массиве объекты WaitHandle не сообщат ему, что они свободны. Тонкостью здесь является только то, что в некоторых операционных системах этот метод не может работать более чем с 64 объектами WaitHandle.

Массив, который передается методу WaitAll, содержит объекты AutoResetEvent.

private WaitHandle[] handles = new WaitHandle[]

{

new AutoResetEvent(false),

new AutoResetEvent(false)

};

Конструктору этих объектов передается флаг, указывающий, в каком состоянии они находятся: свободно (true) или занято (false). В данном случае все объекты AutoResetEvent заняты, и потоку придется ждать их освобождения.

Методы CalcFirstPart и CalcSecondPart вычисляют интегралы и сигнализируют о завершении своей работы, вызывая метод Set объектов AutoResetEvent. Этот метод работает следующим образом. Предположим, что есть несколько потоков, которые ждут освобождения конкретного объекта AutoResetEvent. Вызов метода Set этого объекта закончит ожидание только одного ждущего потока, остальные потоки продолжат свое ожидание до следующего вызова метода Set. Если после очередного вызова этого метода не осталось более ждущих потоков, то объект AutoResetEvent переходит в состояние «свободно». И для того, чтобы снова перевести его в состояние занято, нужно вызвать его метод Reset.

Класс ManualResetEvent

Класс ManualResetEvent является практически полным аналогом класса AutoResetEvent, за исключением одной вещи. Как говорилось ранее, вызов метода Set класса AutoResetEvent освобождает только один ждущий поток. Вызов же этого метода класса ManualResetEvent освобождает все ждущие потоки.

Таким образом вам решать, когда использовать каждый из этих классов.

Блокировка потоков

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

Пусть для каких-то целей нам нужно очень быстро вычислять значение какой-либо функции. Стандартным приемом в этом случае является табуляция. Значения функции в нужных точках вычисляются и, затем, сохраняются в кэше. Метод GetValue, выполняясь в отдельном потоке, извлекает необходимые ему значения функции из кэша. Если же он не нашел значения в кэше, он помещает нужый ему аргумент функции в отдельный список. Другой метод (CalcFunction), выполняющийся так же в отдельном потоке, извлекает аргументы функции из этого списка, вычисляет значение функции и помещает ее в кэш.

Поскольку и список аргументов и кэш могут быть изменены в любой момент, то оба метода используют оператор lock для синхронизации доступа к этим объектам (класс Exercise5):

private static void GetValue(object state)

{

Random rnd = new Random();

while (true)

{

int number = rnd.Next(1000);

lock (squares)

{

if (squares.ContainsKey(number))

{

Console.WriteLine("Square of {0} is {1}.", number, squares[number]);

continue;

}

lock (numbersToProcess)

{

numbersToProcess.Add(number);

}

}

}

}

private static void CalcFunction(object state)

{

while (true)

{

lock (numbersToProcess)

{

if (numbersToProcess.Count > 0)

{

int number = numbersToProcess[0];

numbersToProcess.Remove(0);

lock (squares)

{

squares[number] = number * number;

}

Console.WriteLine("Square for {0} is calculated.", number);

}

}

}

}

Как видно из кода, метод GetValue сперва захватывает кэш, а потом список аргументов, метод же CalcFunction наоборот сперва захватывает список аргументов, а затем кэш. Именно поэтому оба потока взаимно блокируют друг друга. Первый успевает захватить кэш, а второй – список аргументов. Затем они будут ждать друг друга, пытаясь захватить уже заблокированные другим потоком объекты.

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

Взаимодествие с пользовательским интерфейсом

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

WinForms

Пока что наиболее распространенным в .NET Framework API для создания пользовательского интерфейса для настольных приложения является WinForms. На мой взгляд, со временем его вытеснит WPF, но пока до этого еще далеко. Начнем же наше рассмотрение взаимодействия потоков с пользовательским интерфейсом в WinForms-приложениях (проект WindowForms).

Метод Invoke

Наиболее распространенным способом выполнить свой код гарантировано в основном потоке является использование метода Invoke класса Control (форма Invoke). Этот метод гарантированно исполняет код переданного ему делегата в том потоке, в котором был создан соответствующий элемент Control:

Func<long> dlgt = new Func<long>(GetCheckNumber);

return (long) this.Invoke( dlgt );

Просто оберните вызов вашего метода в делегат и передайте его методу Invoke. Кроме того, класс Control имеет свойство InvokeRequired, позволяющий узнать, нужно ли вызывать для вашего коде Invoke, или можно выполнять его напрямую. В связи с этим полный код метода, который должен выполняться в потоке пользовательского интерфейса, выглядит следующим образом:

private long GetCheckNumber()

{

if (this.InvokeRequired)

{

Func<long> dlgt = new Func<long>(GetCheckNumber);

return (long) this.Invoke( dlgt );

}

else

{

return long.Parse(tbNumber.Text);

}

}

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

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

Как я уже говорил, метод Invoke является наиболее распространенным способом синхронизации пользовательского интерфейса с отдельным потоком. Однако у него есть недостаток, иногда ограничивающий его применимость. Дело в том, что для его работы элемент, у которого он вызывается, должен быть отображен на экране. Говоря более строго, у него должен быть действительный (рабочий, валидный) handle окна. Т.е. элемент не только должен быть создан конструктором, но и отображен. Это не всегда возможно. В данном случае на помощь приходит класс SynchronizationContext (форма SynchronizationContext). Получить объект этого класса очень просто. Для этого используется его статическое свойство Current. Оно возвращает синхронизационный контекст потока. Однако знайте, что у потока может не быть синхронизационного контекста. Поэтому это свойство может возвращать и null. Однако метод Application.Run , запускающий WinForms–приложение, всегда устанавливает этот контекст для потока пользовательского интерфейса. Поэтому вы можете безопасно получить его, обычно это делается по событию загрузки главной формы:

private void MainForm_Load(object sender, EventArgs e)

{

m_SyncContext = SynchronizationContext.Current;

}

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

Синхронизационный контекст имеет 2 основных метода: Send и Post. Обоим передается делегат SendOrPostCallback и параметр для него. Первый выполняет переданный ему делегат синхронно (блокируя вызвавший метод Send поток до тех пор, пока делегат не будет исполнен), а второй – асинхронно.

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

m_SyncContext.Send(

new SendOrPostCallback(

delegate

{

pb.Minimum = 0;

pb.Maximum = 100;

pb.Value = 0;

btnCheck.Enabled = false;

}

),

null

);

Использование анонимных методов позволяет избавиться от необходимости писать отдельные методы и передавать им параметры.

Класс BackgroundWorker

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

Основным для класса BackgroundWorker является событие DoWork. Обработчик этого события вы пишете сами, именно он будет исполняться в отдельном потоке. Запуск отдельного потока осуществляется вызовом метода RunWorkerAsync экземпляра класса BackgroundWorker. Этому методу можно передать параметр типа object, в который можно упаковать любые данные, необходимые вашему потоку:

long checkNumber = GetCheckNumber();

backgroundWorker.RunWorkerAsync(checkNumber);

В вашем обработчике события DoWork эти данные доступны через свойство Argument параметра типа DoWorkEventArgs:

long checkNumber = (long) e.Argument;

Окончание работы и возвращение результата

Если ваш метод, выполняющийся в отдельном потоке, должен возвращать какой-то результат, присвойте этот результат свойству Result объекта DoWorkEventArgs. Тогда он будет доступен после завершения работы вашего метода. Об окончании его работы уведомляет событие RunWorkerCompleted класса BackgroundWorker:

private void backgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)

{

pb.Minimum = 0;

pb.Maximum = 100;

pb.Value = 0;

btnCheck.Enabled = true;

if (e.Error != null)

{

MessageBox.Show(this, e.Error.Message, "Ошибка");

return;

}

if (e.Cancelled)

{

MessageBox.Show(this, "Пользователь отменил выполнение метода.", "Информация");

return;

}

bool isPrime = (bool) e.Result;

if (isPrime)

{

MessageBox.Show(this,

"Число является простым.",

"Информация");

}

else

{

MessageBox.Show(this,

"Число не является простым.",

"Информация");

}

}

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

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

2) Если пользователь прервал работу вашего метода (об этом позже), вы мощете узнать об этом из свойства Cancelled.

3) Наконец, вы можете получить результат работы вашего метода через свойство Result.

Прогресс выполнения

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

backgroundWorker.ReportProgress((int)(100.0 * 2 * d / checkNumber));

Этот метод приводит к вызову события ProgressChanged, обработчик которого вы можете написать самостоятельно:

private void backgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)

{

pb.Value = e.ProgressPercentage;

}

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

Отмена выполнения метода

Иногда требуется прервать выполнение длительной задачи. Для этой цели класс BackgroundWorker предоставляет метод CancelAsync. Вызов данного метода приводит к выставлению в true свойства CancellationPending объекта BackgroundWorker. Программист в обработчике события DoWork может (но не обязан) проверять это свойство. Если оно истинно, программист может некоторым образом досрочно завершить работу потока. Так же он может установить в true свойство Cancel объекта DoWorkEventArgs, чтобы в обработчике события RunWorkerCompleted стало известно, что работа прервана пользователем:

if (backgroundWorker.CancellationPending)

{

e.Cancel = true;

return;

}

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

WPF

С версии 3 .NET Framework поддерживает новую технологию для создания пользовательских интерфейсов – WPF (Windows Presentation Foundation). В принципе с точки зрения взаимодействия с многопоточностью эта технология мало чем отличается от WinForms. Точно так же изменение состояние элементов управления WPF допускается только из потока пользовательского интерфейса. Рассмотрим же существующие различия (проект Wpf).

Объект Dispatcher

Все объекты, для которых важно то, чтобы их изменение происходило из того же потока, в котором они были созданы, являются наследниками класса DispatcherObject (форма Dispatcher). Данный класс имеет два метода CheckAccess и VerifyAccess. Оба они проверяют, выполняется ли текущий код в том же потоке, в котором был создан объект. CheckAccess возвращает булевское значение, а VerifyAccess генерирует исключение, если код выполняется не в том потоке. Все элементы управления WPF унаследованы от DispatcherObject и часто используют VerifyAccess для проверки того, правильно ли производятся их изменения.

Кроме того, объект DispatcherObject имеет свойство Dispatcher, возвращающее объект Dispatcher, который можно использовать для выполнения вашего кода в потоке пользовательского интерфейса. Объект Dispatcher имеет метод Invoke, работающий совершенно так же, как и аналогичный метод элементов управления WinForms.

private long GetCheckNumber()

{

if (!this.CheckAccess())

{

Func<long> dlgt = new Func<long>(GetCheckNumber);

return (long)this.Dispatcher.Invoke(dlgt);

}

else

{

return long.Parse(tbCheckNumber.Text);

}

}

Вместо InvokeRequired вы используете CheckAccess, а метод Invoke вызываете у объекта, доступного через свойство Dispatcher. Вот и все различия.

Класс BackgroundWorker

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

Заключение

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

1 комментарий:

  1. Harrah's Cherokee Casino & Hotel - JSH Hub
    With its luxurious suites, 인천광역 출장안마 five-star 밀양 출장샵 hotel, 구리 출장마사지 casino, restaurants, gaming, 서산 출장마사지 luxury hotel, and lively entertainment, Harrah's Cherokee Casino 광명 출장마사지 & Hotel is

    ОтветитьУдалить