C# Параллельная обработка

advertisement
C# Параллельная обработка
Данная лабораторная работа описывает механизм параллельной обработки данных в языке C#
Потоки позволяют программе выполнять параллельную обработку, за счет чего появляется
возможность одновременного выполнения нескольких операций. Например, потоки можно
использовать для наблюдения ввода данных пользователем, выполнения фоновых задач и
обработки одновременных потоков ввода.
По умолчанию программа один поток. Однако параллельно основному потоку могут создаваться
и использоваться вспомогательные потоки. Эти потоки часто называются рабочими потоками.
Рабочие потоки могут использоваться для выполнения трудоемких или срочных задач без
прерывания основного потока. Например, рабочие потоки часто используются в серверных
приложениях для выполнения входящих запросов, не дожидаясь завершения выполнения
предыдущего запроса. Рабочие потоки также используются для выполнения "фоновых" задач в
настольных приложениях, что позволяет основному потоку (который отвечает за элементы
пользовательского интерфейса) оставаться доступным для команд пользователя.
Разделение потоков позволяет решить проблемы с пропускной способностью и быстротой ответа
системы, однако при этом возникают проблемы совместного использования ресурсов, в частности
взаимоблокировки. Использование нескольких потоков лучше всего подходит для тех задач,
которые используют различные ресурсы, например дескрипторы файлов и сетевые подключения.
Назначение нескольких потоков одному ресурсу, вероятнее всего, вызовет проблемы
синхронизации и частую блокировку потоков во время ожидания, что сведет к минимуму
целесообразность использования нескольких потоков.
Обычно рабочие потоки используются для выполнения трудоемких или срочных задач, для
которых не требуется большое количество ресурсов, используемых другими потоками. Некоторые
используемые программой ресурсы должны быть доступны для нескольких потоков. В этих
случаях пространство имен System.Threading предоставляет классы для синхронизации потоков.
Эти классы включают Mutex, Monitor, Interlocked, AutoResetEvent и ManualResetEvent.
Многопоточные приложения
С помощью языка C# можно создавать приложения, которые выполняют несколько задач
одновременно. Задачи, которые потенциально могут задержать выполнение других задач,
выполняются в отдельных потоках; такой способ организации работы приложения называется
многопоточностью или свободным созданием потоков.
Приложения, использующие многопоточность, более оперативно реагируют на действия
пользователя, поскольку пользовательский интерфейс остается активным, в то время как задачи,
требующие интенсивной работы процессора, выполняются в других потоках. Многопоточность
также эффективна при создании масштабируемых приложений, поскольку пользователь может
добавлять потоки при увеличении рабочей нагрузки.
Использование компонента BackgroundWorker
Наиболее надежный способ создания многопоточного приложения является использование
компонента BackgroundWorker. Этот класс управляет отдельными потоками указанного метода
обработки.
Чтобы запустить операцию в фоновом режиме, необходимо создатьBackgroundWorker и
отслеживать события, сообщающие о ходе выполнения операции и сигнализирующие о ее
завершении. Можно создать объект BackgroundWorker программными средствами или
перетащить его в форму из вкладки КомпонентыПанели элементов. При создании
BackgroundWorker в Forms Designer, оно появляется в Области компонента, и его свойства
отображаются в окне Свойства.
Чтобы настроить выполнение операции в фоновом режиме, необходимо добавить обработчик
для события DoWork.
Чтобы начать выполнение операции в фоновом режиме, необходимо вызвать метод
RunWorkerAsync. Чтобы получать уведомления о ходе выполнения, необходимо обработать
событие ProgressChanged. Если необходимо получать уведомление после завершения операции,
обработайте событие RunWorkerCompleted.
Методы, обрабатывающие события ProgressChanged и RunWorkerCompleted имеют доступ к
пользовательскому интерфейсу приложения, так как эти события вызываются в потоке, который
вызвал метод RunWorkerAsync. Однако обработчик событий DoWork не может работать с
объектами пользовательского интерфейса, поскольку он запускается в фоновом потоке.
Создание и использование потоков
Если требуется больший контроль над поведением потоков приложения, можно управлять
потоками самостоятельно. Однако необходимо иметь в виду, что написание правильных
многопоточных приложений может быть сложной задачей. Приложение может перестать
отвечать на запросы или могут возникать временные ошибки, вызванные конфликтами.
Новый поток создается путем объявления переменной типа Thread и вызова конструктора,
которому предоставляется имя процедуры или метода, которые требуется выполнить в новом
потоке.
System.Threading.Thread newThread =
new System.Threading.Thread(AMethod);
Чтобы начать выполнение нового потока, следует использовать метод Start, как показано в
следующем примере кода.
newThread.Start();
Чтобы остановить выполнение потока, следует использовать метод Abort, как показано в
следующем примере кода.
newThread.Abort();
Помимо запуска и остановки потоки можно приостанавливать, вызывая метод Sleep или Suspend,
возобновлять приостановленный поток методом Resume и уничтожать поток методом Abort.
Потоки также имеют несколько полезных свойств, которые приведены в следующей таблице.
Свойство.
Значение
IsAlive
Содержит значение True, если поток активен.
IsBackground
Возвращает или задает логическое значение, которое указывает, является ли
поток (должен ли являться) фоновым потоком. Фоновые потоки отличаются от
основного потока лишь тем, что они не влияют на завершение процесса. Когда
обработка всех основных потоков закончена, общеязыковая среда выполнения
завершает процесс, применяя метод Abort к тем фоновым потокам, которые еще
продолжают существовать.
Name
Возвращает или задает имя потока. Наиболее часто используется для
обнаружения отдельных потоков при отладке.
Priority
Возвращает или задает значение, используемое операционной системой для
установки приоритетов потоков.
ApartmentState
Возвращает или задает потоковую модель для конкретного потока. Потоковые
модели важны, когда поток вызывает неуправляемый код.
ThreadState
Содержит значение, описывающее состояние или состояния потока.
Приоритеты потоков
Каждый поток имеет приоритетное свойство, которое определяет, какую часть процессорного
времени он должен занять при выполнении. Операционная система выделяет более длинные
отрезки времени на потоки с высоким приоритетом и более короткие на потоки с низким
приоритетом. Новые потоки создаются со значением Normal, но можно изменить свойство Priority
на любое значение в перечислении ThreadPriority.
Имя члена
Описание
AboveNormal
Выполнение потока Thread может быть запланировано после выполнения потоков с
приоритетом Highest и до потоков с приоритетом Normal.
BelowNormal
Выполнение потока Thread может быть запланировано после выполнения потоков с
приоритетом Normal и до потоков с приоритетом Lowest.
Highest
Выполнение потока Thread может быть запланировано до выполнения потоков с
любыми другими приоритетами.
Lowest
Выполнение потока Thread может быть запланировано после выполнения потоков с
любыми другими приоритетами.
Normal
Выполнение потока Thread может быть запланировано после выполнения потоков с
приоритетом AboveNormal и до потоков с приоритетом BelowNormal. По умолчанию
потоки имеют приоритет Normal.
Основные и фоновые потоки
Основной поток выполняется бесконечно, тогда как фоновый поток останавливается сразу после
остановки последнего основного потока.. Для определения или изменения фонового статуса
потока можно использовать свойство IsBackground.
Использование многопоточности для форм и элементов управления
Многопоточность наиболее эффективна при выполнении процедур и методов класса, однако ее
можно использовать и при работе с формами и элементами управления. При этом нужно принять
во внимание следующее:


По возможности методы элемента управления должны выполняться в том же потоке, в
котором был создан этот элемент. Если необходимо вызвать метод элемента управления
из другого потока, необходимо использовать для вызова метода метод Invoke.
Не используйте оператор lock (C#) для блокировки потоков, работающих с элементами
управления или формами. Поскольку методы элементов управления и форм иногда
выполняют обратный вызов вызывающей процедуры, можно непреднамеренно вызвать
взаимоблокировку — ситуацию, когда два потока ожидают друг от друга выполнения
действий, необходимых для дальнейшей работы приложения.
Параметры и возвращаемые значения для многопоточных
процедур
При передаче параметров и возвращении значений в многопотоковое приложение, возникают
дополнительные сложности, поскольку конструктору потока передается только ссылка на
процедуру, которая не принимает аргументов и не возвращает значений. В следующих разделах
показаны некоторые простые способы передачи параметров и возращения значений из процедур,
выполняющихся в различных потоках.
Предоставление параметров для многопоточных процедур
Наилучшим способом передачи параметров при вызове многопотокового метода является
заключение нужного метода в класс и определение полей для этого класса, которые будут
использоваться в качестве параметров нового потока. Преимущество такого подхода заключается
в возможности создания нового экземпляра класса со своими параметрами при каждом запуске
нового потока. Рассмотрим, например, функцию, вычисляющую площадь треугольника, как
показано в следующем примере кода:
double CalcArea(double Base, double Height)
{
return 0.5 * Base * Height;
}
Можно написать класс, который включает в себя функцию CalcArea и создает поля для хранения
входных параметров, как показано ниже:
class AreaClass
{
public double Base;
public double Height;
public double Area;
public void CalcArea()
{
Area = 0.5 * Base * Height;
MessageBox.Show("The area is: " + Area.ToString());
}
}
Чтобы использовать AreaClass, можно создать объект AreaClass и установить свойства Base и
Height, как показано в следующем коде:
protected void TestArea()
{
AreaClass AreaObject = new AreaClass();
System.Threading.Thread Thread =
new System.Threading.Thread(AreaObject.CalcArea);
AreaObject.Base = 30;
AreaObject.Height = 40;
Thread.Start();
}
Обратите внимание, что процедура TestArea не будет проверять значение поля Area после вызова
метода CalcArea. Поскольку CalcArea выполняется в отдельном потоке, не гарантируется, что поле
Area будет задано, если проверить его сразу после вызова Thread.Start. В следующем разделе
показан более удобный способ получения возвращаемых значений от многопотоковых процедур.
Получение возвращаемых значений от многопоточных процедур
Получение возвращаемых значений от процедур, выполняемых в отдельных потоках,
осложняется тем, что процедуры не могут быть функциями и использовать аргументы ByRef.
Наиболее простым способом является использование компонента BackgroundWorker для
управления потоками и вызова события при завершении задачи и обработки результатов
обработчиком событий.
Следующий пример показывает получение возвращаемого значения от процедуры,
выполняющейся в отдельном потоке, путем создания события.
class AreaClass2
{
public double Base;
public double Height;
public double CalcArea()
{
// Calculate the area of a triangle.
return 0.5 * Base * Height;
}
}
private System.ComponentModel.BackgroundWorker BackgroundWorker1
= new System.ComponentModel.BackgroundWorker();
private void TestArea2()
{
InitializeBackgroundWorker();
AreaClass2 AreaObject2 = new AreaClass2();
AreaObject2.Base = 30;
AreaObject2.Height = 40;
// Start the asynchronous operation.
BackgroundWorker1.RunWorkerAsync(AreaObject2);
}
private void InitializeBackgroundWorker()
{
// Attach event handlers to the BackgroundWorker object.
BackgroundWorker1.DoWork +=
new System.ComponentModel.DoWorkEventHandler(BackgroundWorker1_DoWork);
BackgroundWorker1.RunWorkerCompleted +=
new
System.ComponentModel.RunWorkerCompletedEventHandler(BackgroundWorker1_RunWorkerCompleted);
}
private void BackgroundWorker1_DoWork(
object sender,
System.ComponentModel.DoWorkEventArgs e)
{
AreaClass2 AreaObject2 = (AreaClass2)e.Argument;
// Return the value through the Result property.
e.Result = AreaObject2.CalcArea();
}
private void BackgroundWorker1_RunWorkerCompleted(
object sender,
System.ComponentModel.RunWorkerCompletedEventArgs e)
{
// Access the result through the Result property.
double Area = (double)e.Result;
MessageBox.Show("The area is: " + Area.ToString());
}
Синхронизация потоков
В следующих разделах описаны функции и классы, которые можно использовать для
синхронизации доступа к ресурсам в многопоточных приложениях.
Одним из преимуществ использования нескольких потоков в приложении является асинхронное
выполнение каждого потока. В приложениях Windows это позволяет выполнять длительные
задачи в фоновом режиме, при этом окно приложения и элементы управления остаются
активными. Для серверных приложений многопоточность обеспечивает возможность обработки
каждого входящего запроса в отдельном потоке. В противном случае ни один новый запрос не
будет обработан, пока не завершена обработка предыдущего запроса.
Однако вследствие того, что потоки асинхронные, доступ к ресурсам, таким как дескрипторы
файлов, сетевые подключения и память, должен быть скоординирован. Иначе два или более
потоков могут получить доступ к одному и тому же ресурсу одновременно, причем один поток не
будет учитывать действия другого. В результате данные могут быть повреждены
непредсказуемым образом.
Для простых операций над числовыми типами данных синхронизация потоков выполняется с
помощью членов класса Interlocked. Для прочих типов данных и других ресурсов, не являющихся
потокобезопасными, многопоточность можно применять только с помощью структур,
описываемых в этом разделе.
Ключевые слова Lock
Выражене lock используется для того, чтобы выполнение блока кода не прерывалось кодом,
выполняемым в других потоках. Для этого нужно получить взаимоисключающую блокировку для
данного объекта на время длительности блока кода.
Оператор lock получает объект в качестве аргумента, и за ним следует блок кода, который должен
выполняться одновременно только в одном потоке. Например:
public class TestThreading
{
private System.Object lockThis = new System.Object();
public void Process()
{
lock (lockThis)
{
// Access thread-sensitive resources.
}
}
}
Аргумент, предоставляемый ключевому слову lock, должен быть объектом на основе ссылочного
типа; он используется для определения области блокировки. В приведенном выше примере
область блокировки ограничена этой функцией, поскольку не существует ссылок на объект
lockThis вне функции. Если бы такая ссылка существовала, область блокировки включала бы этот
объект. Строго говоря, предоставляемый объект используется только для того, чтобы уникальным
образом определить ресурс, к которому предоставляется доступ для различных потоков, поэтому
это может быть произвольный экземпляр класса. В действительности этот объект обычно
представляет ресурс, для которого требуется синхронизация потоков. Например, если объект
контейнера должен использоваться в нескольких потоках, то контейнер можно передать
блокировке, а блок синхронизированного кода после блокировки должен получить доступ к
контейнеру. Если другие потоки блокируются для того же контейнера перед доступом к нему,
обеспечивается безопасная синхронизация доступа к объекту.
Как правило, рекомендуется избегать блокировки типа public или экземпляров объектов,
которыми не управляет код вашего приложения. Например, использование lock(this) может
привести к неполадкам, если к экземпляру разрешен открытый доступ, поскольку внешний код
также может блокировать объект. Это может привести к созданию ситуаций взаимной
блокировки, когда два или несколько потоков будут ожидать высвобождения одного и того же
объекта. По этой же причине блокировка открытого типа данных (в отличие от объектов) может
привести к неполадкам. Блокировка строковых литералов наиболее опасна, поскольку строковые
литералы интернируются средой CLR. Это означает, что если во всей программе есть один
экземпляр любого строкового литерала, точно такой же объект будет представлять литерал во
всех запущенных доменах приложения и во всех потоках. В результате блокировка, включенная
для строки с одинаковым содержимым во всем приложении, блокирует все экземпляры этой
строки в приложении. По этой причине лучше использовать блокировку закрытых или
защищенных членов, для которых интернирование не применяется. В некоторых классах есть
члены, специально предназначенные для блокировки. Например, в типе Array есть SyncRoot. Во
многих типах коллекций есть член SyncRoot.
Мониторы
Как и ключевые слова lock и SyncLock, мониторы не допускают одновременное выполнение
несколькими потоками одних и тех же блоков кода. Метод Enter позволяет только одному методу
переходить к последующим операторам, все прочие методы заблокированы, пока выполняемый
метод не вызовет Exit. Это аналогично использованию ключевого слова lock. Например:
lock (x)
{
DoSomething();
}
Это соответствует следующей записи:
System.Object obj = (System.Object)x;
System.Threading.Monitor.Enter(obj);
try
{
DoSomething();
}
finally
{
System.Threading.Monitor.Exit(obj);
}
Использование ключевого слова lock предпочтительнее прямого использования класса Monitor,
так как слово lock более компактно, а также lock обеспечивает разблокировку основного монитора
даже в том случае, если защищенный код вызывает исключение. Для этого применяется ключевое
слово finally, которые выполняет свой блок кода вне зависимости от наличия исключений.
События синхронизации и дескрипторы ожидания
Использование блокировки или монитора полезно для предотвращения одновременного
выполнения блоков кода, но эти структуры не позволяют одному потоку передавать события в
другой. Для этого требуются события синхронизации — объекты, обладающие одним их двух
состояний (с сигналом или без сигнала), применяющиеся для активации и приостановки потоков.
Потоки можно приостанавливать, заставляя их ожидать события синхронизации без сигнала, и
активировать, меняя состояние события на состояние с сигналом. Если поток попытается ожидать
события, для которого уже есть сигнал, то выполнение потока продолжится без задержки.
Существует два типа событий синхронизации: AutoResetEvent и ManualResetEvent. Отличие только
одно: AutoResetEvent автоматически изменяется с состояния с сигналом на состояние без сигнала
всегда при активации потока. В отличие от него, ManualResetEvent позволяет активировать
состоянием с сигналом любое количество потоков, и вернется в состояние без сигнала только при
вызове своего метода Reset.
Потоки можно заставить дожидаться определенных событий, вызвав один из методов ожидания,
например WaitOne, WaitAny или WaitAll.
WaitHandle.WaitOne() приводит к ожиданию потока до тех пор, пока единственное событие не
становится сигнализирующим, WaitHandle.WaitAny() блокирует поток до тех пор, пока одно или
несколько указанных событий не становятся сигнализирующими, и WaitHandle.WaitAll() блокирует
поток до тех пор, пока все указанные события не становятся сигнализирующими. Событие выдает
сигнал при вызове метода Set этого события.
В следующем примере поток создается и запускается функцией Main. Новый поток ждет события с
помощью метода WaitOne. Поток приостанавливается до получения сигнала от события основным
потоком, выполняющим функцию Main. После получения сигнала возвращается дополнительный
поток. В этом случае, поскольку событие используется только для активации одного потока,
можно использовать классы AutoResetEvent или ManualResetEvent.
using System;
using System.Threading;
class ThreadingExample
{
static AutoResetEvent autoEvent;
static void DoWork()
{
Console.WriteLine("
worker thread started, now waiting on event...");
autoEvent.WaitOne();
Console.WriteLine("
worker thread reactivated, now exiting...");
}
static void Main()
{
autoEvent = new AutoResetEvent(false);
Console.WriteLine("main thread starting worker thread...");
Thread t = new Thread(DoWork);
t.Start();
Console.WriteLine("main thread sleeping for 1 second...");
Thread.Sleep(1000);
Console.WriteLine("main thread signaling worker thread...");
autoEvent.Set();
}
}
Мьютексные объекты
Мьютекс аналогичен монитору, он не допускает одновременного выполнения блока кода более
чем из одного потока. Название "мьютекс" – сокращенная форма слова "взаимоисключающий"
("mutually exclusive" на английском языке). Впрочем, в отличие от мониторов мьютексы можно
использовать для синхронизации потоков по процессам. Мьютекс представляется классом Mutex.
При использовании для синхронизации внутри процесса мьютекс называется именованным
мьютексом, поскольку он должен использоваться в другом приложении и к нему нельзя
предоставить общий доступ с помощью глобальной или статической переменной. Ему нужно
назначить имя, чтобы оба приложения могли получить доступ к одному и тому же объекту
мьютекса.
Несмотря на то, что для синхронизации потоков внутри процесса можно использовать мьютекс,
рекомендуется использовать Monitor, поскольку мониторы были созданы специально для .NET
Framework и более эффективно используют ресурсы. Напротив, класс Mutex является оболочкой
для структуры Win32. Мьютекс мощнее монитора, но для мьютекса требуются переходы
взаимодействия, на которые затрачивается больше вычислительных ресурсов, чем на обработку
класса Monitor. Пример использования мьютекса см. в разделе Объекты Mutex.
Класс Interlocked
Методы класса Interlocked можно использовать для предотвращения проблем, возникающих при
одновременной попытке нескольких потоков обновить или сравнить некоторое значение. Методы
этого класса позволяют безопасно увеличивать, уменьшать, заменять и сравнивать значения
переменных из любого потока.
Методы этого класса защищены от ошибок, которые могут возникнуть при переключении
контекстов планировщиком в то время, когда поток обновляет переменную, а она может быть
доступна другим потокам, или когда выполняются одновременно два потока на различных
процессорах. Члены этого класса не выдают исключения.
using System;
using System.Threading;
namespace InterlockedExchange_Example
{
class MyInterlockedExchangeExampleClass
{
//0 for false, 1 for true.
private static int usingResource = 0;
private const int numThreadIterations = 5;
private const int numThreads = 10;
static void Main()
{
Thread myThread;
Random rnd = new Random();
for(int i = 0; i < numThreads; i++)
{
myThread = new Thread(new ThreadStart(MyThreadProc));
myThread.Name = String.Format("Thread{0}", i + 1);
//Wait a random amount of time before starting next thread.
Thread.Sleep(rnd.Next(0, 1000));
myThread.Start();
}
}
private static void MyThreadProc()
{
for(int i = 0; i < numThreadIterations; i++)
{
UseResource();
//Wait 1 second before next attempt.
Thread.Sleep(1000);
}
}
//A simple method that denies reentrancy.
static bool UseResource()
{
//0 indicates that the method is not in use.
if(0 == Interlocked.Exchange(ref usingResource, 1))
{
Console.WriteLine("{0} acquired the lock", Thread.CurrentThread.Name);
//Code to access a resource that is not thread safe would go here.
//Simulate some work
Thread.Sleep(500);
Console.WriteLine("{0} exiting lock", Thread.CurrentThread.Name);
//Release the lock
Interlocked.Exchange(ref usingResource, 0);
return true;
}
else
{
Console.WriteLine("
return false;
}
{0} was denied the lock", Thread.CurrentThread.Name);
}
}
}
Блокировки чтения записи
В некоторых случаях может понадобиться блокировать ресурс только для записи данных и
разрешить нескольким клиентам одновременно считывать данные, когда они не обновляются.
Класс ReaderWriterLock обеспечивает монопольный доступ к ресурсу на то время, в течение
которого поток изменяет ресурс, но разрешает одновременно выполнять несколько операций
чтения. Блокировки чтения и записи являются удобной альтернативой монопольным
блокировкам, которые заставляют другие потоки находиться в состоянии ожидания, даже когда
им не нужно обновлять данные.
Класс ReaderWriterLock работает оптимально, когда большинство обращений к ресурсу
производятся для чтения, а операции записи выполняются нечасто и имеют короткую
продолжительность. Несколько читающих потоков чередуются с одним записывающим потоком,
так что ни те, ни другие не блокируются на длительные периоды времени.
Поток может удерживать блокировку чтения или записи, но не обе одновременно. Вместо
освобождения блокировки чтения для получения блокировки записи можно использовать методы
UpgradeToWriterLock и DowngradeFromWriterLock.
using System;
using System.Threading;
public class Test
{
// Declaring the ReaderWriterLock at the class level
// makes it visible to all threads.
static ReaderWriterLock rwl = new ReaderWriterLock();
// For this example, the shared resource protected by the
// ReaderWriterLock is just an integer.
static int resource = 0;
const int numThreads = 26;
static bool running = true;
static Random rnd = new Random();
// Statistics.
static int readerTimeouts = 0;
static int writerTimeouts = 0;
static int reads = 0;
static int writes = 0;
public static void Main(string[] args)
{
// Start a series of threads. Each thread randomly
// performs reads and writes on the shared resource.
Thread[] t = new Thread[numThreads];
for (int i = 0; i < numThreads; i++)
{
t[i] = new Thread(new ThreadStart(ThreadProc));
t[i].Name = new String(Convert.ToChar(i + 65), 1);
t[i].Start();
if (i > 10)
Thread.Sleep(300);
}
// Tell the threads to shut down, then wait until they all
// finish.
running = false;
for (int i = 0; i < numThreads; i++)
{
t[i].Join();
}
// Display statistics.
Console.WriteLine("\r\n{0} reads, {1} writes, {2} reader time-outs, {3} writer timeouts.",
reads, writes, readerTimeouts, writerTimeouts);
Console.WriteLine("Press ENTER to exit.");
Console.ReadLine();
}
static void ThreadProc()
{
// As long as a thread runs, it randomly selects
// various ways to read and write from the shared
// resource. Each of the methods demonstrates one
// or more features of ReaderWriterLock.
while (running)
{
double action = rnd.NextDouble();
if (action < .8)
ReadFromResource(10);
else if (action < .81)
ReleaseRestore(50);
else if (action < .90)
UpgradeDowngrade(100);
else
WriteToResource(100);
}
}
// Shows how to request and release a reader lock, and
// how to handle time-outs.
static void ReadFromResource(int timeOut)
{
try
{
rwl.AcquireReaderLock(timeOut);
try
{
// It is safe for this thread to read from
// the shared resource.
Display("reads resource value " + resource);
Interlocked.Increment(ref reads);
}
finally
{
// Ensure that the lock is released.
rwl.ReleaseReaderLock();
}
}
catch (ApplicationException)
{
// The reader lock request timed out.
Interlocked.Increment(ref readerTimeouts);
}
}
// Shows how to request and release the writer lock, and
// how to handle time-outs.
static void WriteToResource(int timeOut)
{
try
{
rwl.AcquireWriterLock(timeOut);
try
{
// It is safe for this thread to read or write
// from the shared resource.
resource = rnd.Next(500);
Display("writes resource value " + resource);
Interlocked.Increment(ref writes);
}
finally
{
// Ensure that the lock is released.
rwl.ReleaseWriterLock();
}
}
catch (ApplicationException)
{
// The writer lock request timed out.
Interlocked.Increment(ref writerTimeouts);
}
}
// Shows how to request a reader lock, upgrade the
// reader lock to the writer lock, and downgrade to a
// reader lock again.
static void UpgradeDowngrade(int timeOut)
{
try
{
rwl.AcquireReaderLock(timeOut);
try
{
// It is safe for this thread to read from
// the shared resource.
Display("reads resource value " + resource);
Interlocked.Increment(ref reads);
// If it is necessary to write to the resource,
// you must either release the reader lock and
// then request the writer lock, or upgrade the
// reader lock. Note that upgrading the reader lock
// puts the thread in the write queue, behind any
// other threads that might be waiting for the
// writer lock.
try
{
LockCookie lc = rwl.UpgradeToWriterLock(timeOut);
try
{
// It is safe for this thread to read or write
// from the shared resource.
resource = rnd.Next(500);
Display("writes resource value " + resource);
Interlocked.Increment(ref writes);
}
finally
{
// Ensure that the lock is released.
rwl.DowngradeFromWriterLock(ref lc);
}
}
catch (ApplicationException)
{
// The upgrade request timed out.
Interlocked.Increment(ref writerTimeouts);
}
// When the lock has been downgraded, it is
// still safe to read from the resource.
Display("reads resource value " + resource);
Interlocked.Increment(ref reads);
}
finally
{
// Ensure that the lock is released.
rwl.ReleaseReaderLock();
}
}
catch (ApplicationException)
{
// The reader lock request timed out.
Interlocked.Increment(ref readerTimeouts);
}
}
// Shows how to release all locks and later restore
// the lock state. Shows how to use sequence numbers
// to determine whether another thread has obtained
// a writer lock since this thread last accessed the
// resource.
static void ReleaseRestore(int timeOut)
{
int lastWriter;
try
{
rwl.AcquireReaderLock(timeOut);
try
{
// It is safe for this thread to read from
// the shared resource. Cache the value. (You
// might do this if reading the resource is
// an expensive operation.)
int resourceValue = resource;
Display("reads resource value " + resourceValue);
Interlocked.Increment(ref reads);
// Save the current writer sequence number.
lastWriter = rwl.WriterSeqNum;
// Release the lock, and save a cookie so the
// lock can be restored later.
LockCookie lc = rwl.ReleaseLock();
// Wait for a random interval (up to a
// quarter of a second), and then restore
// the previous state of the lock. Note that
// there is no time-out on the Restore method.
Thread.Sleep(rnd.Next(250));
rwl.RestoreLock(ref lc);
//
//
//
//
if
{
Check whether other threads obtained the
writer lock in the interval. If not, then
the cached value of the resource is still
valid.
(rwl.AnyWritersSince(lastWriter))
resourceValue = resource;
Interlocked.Increment(ref reads);
Display("resource has changed " + resourceValue);
}
else
{
Display("resource has not changed " + resourceValue);
}
}
finally
{
// Ensure that the lock is released.
rwl.ReleaseReaderLock();
}
}
catch (ApplicationException)
{
// The reader lock request timed out.
Interlocked.Increment(ref readerTimeouts);
}
}
// Helper method briefly displays the most recent
// thread action. Comment out calls to Display to
// get a better idea of throughput.
static void Display(string msg)
{
Console.Write("Thread {0} {1}.
}
\r", Thread.CurrentThread.Name, msg);
}
Взаимоблокировки
В многопоточных приложениях не обойтись без синхронизации потоков, однако всегда
существует опасность создания deadlock, когда несколько потоков ожидают друг друга, и
приложение зависает. Взаимоблокировка аналогична ситуации, в которой автомобили
останавливаются на перекрестке и каждый водитель ожидает, пока проедет другой. Важно
исключить возможность взаимоблокировок путем тщательного планирования. Взаимоблокировки
часто можно предвидеть еще до написания кода, построив диаграмму многопотокового
приложения.
Таймеры потоков
Класс System.Threading.Timer полезен для периодического выполнения задач в отдельном потоке.
Например, можно использовать таймер потока для проверки состояния и целостности базы
данных или для создания резервных копий важных файлов.
В следующем примере задача запускается каждые две секунды, а для инициализации метода
Dispose, который останавливает таймер, используется флаг. В этом примере сведения о состоянии
отображаются в окне вывода.
private class StateObjClass
{
// Used to hold parameters for calls to TimerTask.
public int SomeValue;
public System.Threading.Timer TimerReference;
public bool TimerCanceled;
}
public void RunTimer()
{
StateObjClass StateObj = new StateObjClass();
StateObj.TimerCanceled = false;
StateObj.SomeValue = 1;
System.Threading.TimerCallback TimerDelegate =
new System.Threading.TimerCallback(TimerTask);
// Create a timer that calls a procedure every 2 seconds.
// Note: There is no Start method; the timer starts running as soon as
// the instance is created.
System.Threading.Timer TimerItem =
new System.Threading.Timer(TimerDelegate, StateObj, 2000, 2000);
// Save a reference for Dispose.
StateObj.TimerReference = TimerItem;
// Run for ten loops.
while (StateObj.SomeValue < 10)
{
// Wait one second.
System.Threading.Thread.Sleep(1000);
}
// Request Dispose of the timer object.
StateObj.TimerCanceled = true;
}
private void TimerTask(object StateObj)
{
StateObjClass State = (StateObjClass)StateObj;
// Use the interlocked class to increment the counter variable.
System.Threading.Interlocked.Increment(ref State.SomeValue);
System.Diagnostics.Debug.WriteLine("Launched new thread " + DateTime.Now.ToString());
if (State.TimerCanceled)
// Dispose Requested.
{
State.TimerReference.Dispose();
System.Diagnostics.Debug.WriteLine("Done " + DateTime.Now.ToString());
}
}
Группировка потоков в пул
Пул потоков — это коллекция потоков, которые могут использоваться для выполнения нескольких
задач в фоновом режиме. Это позволяет разгрузить главный поток для асинхронного выполнения
других задач.
Пулы потоков часто используются в серверных приложениях. Каждый входящий запрос
назначается потоку из пула, таким образом, запрос может обрабатываться асинхронно без
задействования главного потока и задержки обработки последующих запросов.
Когда поток в пуле завершает выполнение задачи, он возвращается в очередь ожидания, в
которой может быть повторно использован. Повторное использование позволяет приложениям
избежать дополнительных затрат на создание новых потоков для каждой задачи.
Обычно пулы имеют максимальное количество потоков. Если все потоки заняты, дополнительные
задачи помещаются в очередь, где хранятся до тех пор, пока не появятся свободные потоки.
Можно реализовать собственный пул потоков, но гораздо проще использовать пул,
предоставляемый .NET Framework через класс ThreadPool.
Используя группировку потоков в пул, можно вызвать метод ThreadPool.QueueUserWorkItem с
делегатом для процедуры, которую требуется выполнить, а C# создаст поток и выполнит
процедуру.
Следующий пример показывает, как можно использовать группировку потоков в пул для запуска
нескольких задач.
public void DoWork()
{
// Queue a task.
System.Threading.ThreadPool.QueueUserWorkItem(
new System.Threading.WaitCallback(SomeLongTask));
// Queue another task.
System.Threading.ThreadPool.QueueUserWorkItem(
new System.Threading.WaitCallback(AnotherLongTask));
}
private void SomeLongTask(Object state)
{
// Insert code to perform a long task.
}
private void AnotherLongTask(Object state)
{
// Insert code to perform a long task.
}
Одно из преимуществ группировки потоков заключается в возможности передачи аргументов в
процедуру задачи в виде объекта состояния. Если вызываемой процедуре требуется нескольких
аргументов, можно привести структуру или экземпляр некоторого класса к типу данных Object.
Параметры и возвращаемые значения пула потоков
Получение возвращаемого значения из пула потоков является не такой простой задачей.
Стандартный способ получения возвращаемого значения при вызове функции использовать
нельзя, поскольку помещать в очередь на выполнение в пуле потоков можно только процедуры
Sub. Один из способов обеспечить передачу параметров и возвращения значений — это
заключение параметров, возвращаемых значений и методов в класс-оболочку.
Более простым способом передачи параметров и возвращаемых значений является
использование необязательной переменной состояния объекта типа ByVal метода
QueueUserWorkItem. Если эта переменная используется для передачи ссылки на экземпляр
класса, члены экземпляра могут быть изменены потоком, входящим в пул потоков, и
использоваться в качестве возвращаемых значений.
На первый взгляд возможность изменения объекта, на который ссылается переданная по
значению переменная, не является очевидной. На самом деле это возможно, потому что по
значению передается только ссылка на объект. При изменении членов объекта, указанного в
ссылке, изменения применяются к реальному экземпляру класса.
Структуры, входящие в состав объектов состояния, нельзя использовать для получения
возвращаемых значений. Поскольку структуры являются типами, передаваемыми по значению,
изменения, вносимые асинхронным процессом, не влияют на члены исходной структуры.
Структуры следует использовать для передачи параметров, когда не требуется получать
возвращаемые значения.
Задание
Для программы работы с таблицей необходимо сделать следующую доработку:



Разработать форму/компонент для отображения индикатора процесса работы и
возможности прерывания работы, производящейся в отдельном потоке;
Сделать кнопку и метод для генерации тестовых данных для таблицы, который должен
работать в отдельном потоке с показом индикации процесса работы и возможностью
прерывания. Количество данных должно быть соизмеримо с временем генерации в
течение, не менее, 30 секунд;
Разработать компонент для работы с большим количеством табличных данных.
Необходимо чтобы была возможность разбивать представление на страницы, по которым
можно было бы перемещаться. Используя данный компонент сделать возможность
загружать сгенерированные текстовые данные для просмотра, редактирования и
сохранения в файл.
Download