Реализация параллельного выполнения кода в .NET основана на базовых механизмах, предоставляемых ядром операционной системы Windows. Аналогично средствам операционной системы средства .NET Framework могут быть разделены на следующие группы:
Как и в случае API системы, такое деление не является строгим - реальные средства "пересекают" границы этих категорий. При этом не все механизмы, предоставляемые операционной системой, нашли свое отражение в .NET Framework; равно как многие механизмы, оставаясь внешне схожими с механизмами операционной системы, существенным образом изменились. Так, например, .NET не поддерживает волокна;
Вообще говоря, использование фоновых потоков для выполнения специфических задач поддержки инфраструктуры .NET стало общим местом. При запуске .NET приложения автоматически создается пул потоков, используемый CLR по мере надобности. Этот пул применяется, в частности, в ситуациях, связанных с обработкой асинхронных запросов и
.NET разрабатывался с учетом возможности переноса на другие платформы. Для облегчения этого процесса в архитектуру CLR включен специальный уровень адаптации к платформе (
В данном курсе рассматриваются основные средства реализации многопоточных приложений и не затрагиваются вопросы создания ASP, COM-объектов и многого другого.
Потоки в .NET реализованы на основе модели потоков операционной системы и не предусматривают средств управления волокнами. Если система не поддерживает многопоточные приложения, то
Основные классы для реализации многопоточных приложений определены в пространстве имен System.Threading. Для описания собственных потоков предназначен класс Thread. При создании потока ему необходимо указать делегата, реализующего процедуру потока. К сожалению, в .NET, во-первых, не предусмотрено передачи аргументов в эту процедуру, во-вторых, процедура должна быть статическим методом, и в-третьих, класс Thread является опечатанным. В результате передача каких-либо данных в процедуру потока вызывает определенные трудности и требует явного или косвенного использования
Приведенный ниже пример демонстрирует работу с созданием нескольких потоков для параллельного перемножения двух квадратных матриц. Начальные значения всех элементов матриц равны 1, поэтому результирующая матрица должна быть заполнена числами, равными размерности перемножаемых матриц. В процессе умножения и суммирования элементов матриц синхронизация не выполняется, поэтому при достаточно большом размере матриц гарантированно будут возникать ошибки (необходимо синхронизировать выполнение некоторых действий в потоках, чтобы избежать возникновения ошибок, - об этом ниже, при рассмотрении
using System;
using System.Threading;
namespace TestNamespace {
class TestApp {
const int m_size = 600;
const int m_stripsize = 50;
const int m_stripmax = 12;
private static int m_stripused = 0;
private static double[,] m_A = new double[m_size,m_size],
m_B = new double[m_size,m_size],
m_C = new double[m_size,m_size];
public static void ThreadProc()
{
int i,j,k, from, to;
from = ( m_stripused++ ) * m_stripsize;
to = from + m_stripsize;
if ( to > m_size ) to = m_size;
for ( i = 0; i < m_size; i++ ) {
for ( j = 0; j < m_size; j++ ) {
for ( k = from; k < to; k++ )
m_C[i,j] += m_A[i,k] * m_B[k,j];
}
}
}
public static void Main()
{
Thread[] T = new Thread[ m_stripmax ];
int i,j,errs;
for ( i = 0; i < m_size; i++ ) {
for ( j = 0; j < m_size; j++ ) {
m_A[i,j] = m_B[i,j] = 1.0;
m_C[i,j] = 0.0;
}
}
for ( i = 0; i < m_stripmax; i++ ) {
T[i] = new Thread(new ThreadStart(ThreadProc));
T[i].Start();
}
// дожидаемся завершения всех потоков
for ( i = 0; i < m_stripmax; i++ ) T[i].Join();
// проверяем результат
errs = 0;
for ( i = 0; i < m_size; i++ )
for ( j = 0; j < m_size; j++ )
if ( m_C[i,j] != m_size ) errs++;
Console.WriteLine("Error count = {0}", errs );
}
}
}
Поток в .NET может находиться в одном из следующих состояний: незапущенном, исполнения, ожидания, приостановленном, завершенном и прерванном. Возможные переходы между этими состояниями изображены на рис. 7.1.
(рис 7.1) Состояния потока
Сразу после создания и до начала выполнения потока он находится в незапущенном состоянии ( Unstarted ). Текущее состояние можно определить с помощью свойства Thread.ThreadState. После запуска поток можно перевести в состояние исполнения ( Running ) вызовом метода Thread.Start. Работающий поток может быть переведен в состояние ожидания ( WaitSleepJoin ) явным или неявным вызовом соответствующих методов ( Thread.Sleep, Thread.Join и др.) или приостановлен ( ) с помощью метода Thread.. Исполнение приостановленного потока можно возобновить вызовом метода Thread.. Также можно досрочно вывести поток из состояния ожидания вызовом метода Thread.Interrupt.
Завершение функции потока нормальным образом переводит поток в состояние "завершен" ( Stopped ), а досрочное прекращение работы вызовом метода Thread.Abort переведет его в состояние "прерван" ( Aborted ). Кроме того, .NET поддерживает несколько переходных состояний ( AbortRequested, StopRequested и SuspendRequested ). Состояния потока в общем случае могут комбинироваться, например, вполне корректно сочетание состояния ожидания ( WaitSleepJoin ) и какого-либо переходного, скажем, AbortRequested.
Для выполнения задержек в ходе выполнения потока предназначены два метода - Sleep, переводящий поток в состояние ожидания на заданное время, и SpinWait, который выполняет некоторую задержку путем многократных повторов внутреннего цикла. Этот метод дает высокую
Для получения и задания Thread.Priority. Highest, AboveNormal, Normal, BelowNormal и Lowest.
Когда .NET приложение начинает исполняться в среде Windows, CLR создает внутренний пул потоков, используемый средой для реализации асинхронных операций ввода-вывода, вызова асинхронных процедур, обработки таймеров и других целей. Потоки могут добавляться в пул по мере надобности. Этот пул реализуется на основе пула потоков, управляемого операционной системой (построенного на основе порта завершения ввода-вывода). Для взаимодействия с пулом потоков предусмотрен класс ThreadPool, и единственный объект, принадлежащий этому классу, создается CLR при запуске приложения. Все домены приложений в рамках одного процесса используют общий пул потоков.
Разработчики могут использовать несколько статических методов класса ThreadPool. Так, например, существует возможность связать внутренний порт завершения ввода-вывода с файловым объектом, созданным неуправляемым кодом, для обработки событий, связанных с завершением ввода-вывода этим объектом (см. методы ThreadPool.BindHandle и описание порта завершения ввода-вывода ранее). Можно управлять числом потоков в пуле (методы GetAvailableThreads, GetMaxThreads, GetMinThreads и SetMinThreads ), можно ставить в очередь асинхронных вызовов собственные процедуры (метод QueueUserWorkItem ) и назначать процедуры, которые будут вызываться при освобождении какого-либо объекта (метод RegisterWaitForSingleObject ). Эти два метода имеют "безопасные" и "небезопасные" ( .) версии; последние отличаются тем, что в
Для реализации System.IO.Stream. В этом классе определены абстрактные синхронные методы чтения Read и записи Write, а также реализация асинхронных методов BeginRead, EndRead, BeginWrite и EndWrite. Асинхронные методы реализованы с помощью обращения к синхронным операциям фоновыми потоками пула.
На основе абстрактного класса Stream в .NET Framework реализуются потомки, осуществляющие взаимодействие с разного рода потоками данных. Так, например, System.IO.FileStream реализует операции с файлами, System.IO.MemoryStream предоставляет возможность использования байтового массива в качестве источника или получателя данных, System.IO.BufferedStream является "надстройкой" над другими объектами, производными от System.IO.Stream, и обеспечивает буферизацию запросов чтения и записи. Некоторые классы вне пространства имен Sytem.IO также являются потомками Stream. Так, например, класс System.NET.Sockets.NetworkStream обеспечивает сетевое взаимодействие.
using System;
using System.IO;
namespace TestNamespace {
class TestApp {
private const int m_size = 100000000;
private static byte[] m_data = new byte [m_size];
public TestApp()
{
int i;
for ( i = 0; i < m_size; i++ ) m_data[i] = (byte)i;
}
public static void DoneWritting( IAsyncResult state ) {
}
static void Main(string[] args) {
TestApp ta = new TestApp();
IAsyncResult state;
Stream st = new FileStream(
"test.dat", FileMode.OpenOrCreate,
FileAccess.ReadWrite, FileShare.Read, 1, true
);
state = st.BeginWrite(
m_data, 0, m_size,
new AsyncCallback(DoneWritting), null
);
// код в этом месте будет выполняться
// одновременно с выводом данных
st.EndWrite( state );
st.Close();
}
}
}
Данный пример демонстрирует использование FileStream для выполнения асинхронной операции записи большого объема данных.
При реализации собственных потомков класса Stream, возможно, будет иметь смысл переопределить не только Read и Write, но также некоторые базовые (например, BeginRead, ReadByte и др.), универсальная реализация которых может быть неэффективной в конкретном случае.
Для реализации вызова асинхронных процедур в .NET используются фоновые потоки пула, так же как для обработки асинхронных операций ввода-вывода. Класс ThreadPool предлагает два способа для вызова асинхронных процедур: явное размещение вызовов в очереди ( QueueUserWorkItem ) и связывание вызовов с переводом некоторых объектов в свободное состояние ( RegisterWaitForSingleObject ). Кроме того, .NET позволяет осуществлять асинхронные вызовы любых процедур с помощью метода BeginInvoke делегатов.
Статический метод ThreadPool.QueueUserWorkItem ставит вызов указанной процедуры в очередь для обработки. Если пул содержит простаивающие потоки, то обработка этой функции начнется немедленно:
using System;
using System.Threading;
namespace TestNamespace {
class GreetingData {
private string m_greeting;
public GreetingData( string text ) { m_greeting = text; }
public void Invoke() { Console.WriteLine( m_greeting ); }
}
class TestApp {
static void AsyncProc( Object arg )
{
GreetingData gd = (GreetingData)arg;
gd.Invoke();
}
public static void Main()
{
GreetingData gd = new GreetingData("Hello, world!");
ThreadPool.QueueUserWorkItem(new WaitCallback(AsyncProc), gd);
Thread.Sleep( 1000 );
}
}
}
При постановке в очередь асинхронного вызова можно указать объект, который является аргументом асинхронной процедуры (при создании собственных потоков передача аргументов процедуре потока затруднительна).
Второй способ вызова асинхронных процедур связан с использованием объектов, производных от класса System.Threading.WaitHandle (это события и
using System;
using System.Threading;
namespace TestNamespace {
class GreetingData {
private string m_greeting;
private RegisteredWaitHandle m_waithandle;
public GreetingData( string text ) { m_greeting = text; }
public void Invoke() { Console.WriteLine( m_greeting ); }
public RegisteredWaitHandle WaitHandle {
set {
if (value==null) m_waithandle.Unregister( null );
m_waithandle = value;
}
}
}
class TestApp {
static void AsyncProc( Object arg, bool isTimeout ) {
GreetingData gd = (GreetingData)arg;
if ( !isTimeout ) gd.WaitHandle = null;
gd.Invoke();
}
public static void Main() {
GreetingData gd = new GreetingData("Hello");
AutoResetEvent ev = new AutoResetEvent(false);
gd.WaitHandle=ThreadPool.RegisterWaitForSingleObject(
ev, new WaitOrTimerCallback(AsyncProc),
gd, 1000, false
);
Thread.Sleep( 2500 );
ev.Set();
Console.ReadLine();
}
}
}
Приведенный пример демонстрирует использование периодического вызова асинхронной процедуры - при регистрации делегата ( RegisterWaitForSingleObject ) указывается максимальное время ожидания 1 секунда (1000 миллисекунд), после чего основной поток переводится в состояние "спячки" на 2.5 секунды. За это время в очередь пула поступает два вызова асинхронных процедур (с признаком вызова по тайм-ауту). Через 2.5 секунды основной поток пробуждается, переводит событие в свободное состояние, и в очередь пула поступает третий вызов. При обработке этого вызова регистрация делегата отменяется.
Последний способ связан с использованием методов BeginInvoke и EndInvoke делегатов. Когда определяется какой-либо делегат функции, для него будут определены методы: BeginInvoke (содержащий все аргументы делегата плюс два дополнительных - AsyncCallback, который может быть вызван по завершении обработки асинхронного вызова, и AsyncState, с помощью которого можно определить состояние асинхронной процедуры) и EndInvoke, содержащий все выходные параметры (т.е. описанные как inout или out ), плюс IAsyncResult, позволяющий узнать результат выполнения процедуры.
Таким образом, использование BeginInvoke позволяет не только поставить в очередь вызов асинхронной процедуры, но также связать с завершением ее обработки еще один асинхронный вызов. Метод EndInvoke служит для ожидания завершения обработки асинхронной процедуры:
using System;
using System.Threading;
namespace TestNamespace {
public class GreetingData {
private string m_greeting;
public GreetingData( string text ) { m_greeting = text; }
public static void Invoke( GreetingData arg ) {
Console.WriteLine( arg.m_greeting );
}
}
public delegate void AsyncProcCallback ( GreetingData gd );
class TestApp {
public static void Main() {
GreetingData gd = new GreetingData( "Hello!!!" );
AsyncProcCallback apd = new AsyncProcCallback(
GreetingData.Invoke );
IAsyncResult ar = apd.BeginInvoke( gd, null, null );
ar.AsyncWaitHandle.WaitOne();
}
}
}
Данный пример иллюстрирует вызов асинхронной процедуры с использованием метода BeginInvoke и альтернативный механизм ожидания завершения - с использованием внутреннего объекта AsyncWaitHandle (класса WaitHandle ), благодаря которому, собственно говоря, становится возможен вызов асинхронной процедуры, обслуживающей завершение обработки данной процедуры.
В этом смысле асинхронный вызов процедур с помощью BeginInvoke очень близок к обработке асинхронных операций ввода-вывода.
Проблемы, встающие перед разработчиками многопоточных приложений .NET, очень похожи на проблемы разработчиков приложений Win32 API. Соответственно, .NET предоставляет в значительной мере близкий набор средств взаимодействия потоков и их взаимной синхронизации.
К этим средствам относятся
Платформа .NET предоставляет, аналогично базовой операционной системе Windows, набор некоторых основных операций над целыми числами ( int и long ), которые могут выполняться атомарно. Для этого предусмотрены четыре статических метода класса System.Threading., а именно Increment, , Exchange и CompareExchange. Применение этих методов аналогично соответствующим . процедурам Win32 API.
Возвращаясь к примеру использования потоков для умножения матриц, можно выделить один момент, требующий исправления: самое начало процедуры потока, там, где определяется номер полосы:
public static void ThreadProc()
{
int i,j,k, from, to;
from = ( m_stripused++ ) * m_stripsize;
to = from + m_stripsize;
...
Здесь потенциально возможна ситуация, когда несколько потоков одновременно начнут выполнять этот код и получат идентичные номера полос. В этом месте самым эффективным было бы использование атомарных операций для увеличения значения поля m_stripused. Для этого фрагмент надо переписать:
public static void ThreadProc()
{
int i,j,k, from, to;
from = (Interlocked.Increment(ref m_stripused) - 1 ) * m_stripsize;
to = from + m_stripsize;
...
Основные средства взаимной
Последний синхронизирующий объект ReaderWriterLock закрывает очень типичный класс
Мониторы
Мониторы в .NET являются аналогами SyncBlock, являющаяся, по сути, аналогом структуры CRITICAL_SECTION в Win32 API. Добавление такой записи к каждому объекту в управляемой куче чересчур накладно, особенно если учесть, что используются они относительно редко. Поэтому все записи SyncBlock выносятся в отдельный кэш, а в информацию об объекте включается ссылка на запись кэша (см. рис. 7.2). Такой прием позволяет, с одной стороны, содержать кэш синхронизирующих записей минимального размера, а с другой - любому объекту при необходимости можно сопоставить запись.
(рис 7.2) Использование кэша SyncBlock записей объектами управляемой кучи
Обычно объекты не имеют сопоставленной с ними SyncBlock записи, однако она автоматически выделяется при первом использовании монитора.
Класс Monitor, определенный в пространстве имен System.Threading, предлагает несколько статических методов для работы с записями синхронизации. Методы Enter и Exit являются наиболее применяемыми и соответствуют функциям EnterCriticalSection и LeaveCriticalSection операционной системы. Аналогично Monitor - Wait, и PulseAll - позволяют при необходимости временно разрешить доступ к объекту другому потоку, ожидающему его освобождения, не покидая критической секции.
Продолжим рассмотрение примера с многопоточным умножением матриц. Помимо уже рассмотренной проблемы с назначением полос, в процедуре потока есть еще одно некорректное место - прибавление накоплением к элементу результирующей матрицы произведения двух элементов исходной матрицы:
public static void ThreadProc()
{
int i,j,k, from, to;
from = (Interlocked.Increment(ref m_stripused)-1)
* m_stripsize;
to = from + m_stripsize;
if ( to > m_size ) to = m_size;
for ( i = 0; i < m_size; i++ ) {
for ( j = 0; j < m_size; j++ ) {
for ( k = from; k < to; k++ )
m_C[i,j] += m_A[i,k] * m_B[k,j];
}
}
}
Так как эта операция выполняется не атомарно, то вполне может быть так, что один поток считывает значение m_C[i,j], прибавляет к нему величину m_A[i,k] * m_B[k,j] и, прежде чем успевает записать в m_C[i,j] результат сложения, прерывается другим потоком. Второй поток успевает изменить величину m_C[i,j], потом первый снова пробуждается и записывает значение, вычисленное для предыдущего состояния элемента m_C[i,j], - то есть некорректную величину. Собственно говоря, именно эта ситуация и приводит к ошибкам, которые можно наблюдать в исходном примере.
Ситуацию можно исправить, используя синхронизацию при доступе к элементу m_C[i,j] с помощью мониторов:
...
for ( j = 0; j < m_size; j++ ) {
for ( k = from; k < to; k++ ) {
Monitor.Enter( m_C );
try {
m_C[i,j] += m_A[i,k] * m_B[k,j];
} finally {
Monitor.Exit( m_C );
}
}
}
...
В этом фрагменте надо выделить два существенных момента: во-первых, использование метода Exit в блоке finally, а во-вторых - использование всего массива m_C, а не отдельного элемента m_C[i,j].
Первое надо взять за правило, так как в случае возникновения исключения в критической секции блокировка может остаться занятой (т.е. в случае покидания секции без вызова метода Exit ).
Второе связано с тем, что элементы m_C[i,j] являются значениями, а не ссылочными типами. Для типов-значений соответствующее представление в управляемой куче не создается, и у них нет и не может быть ссылок на синхронизирующие записи SyncBlock.
Самое плохое в этой ситуации то, что попытка собрать приложение, использующее типы-значения в качестве аргументов методов Enter и Exit (как в примере ниже), пройдет успешно:
...
for ( j = 0; j < m_size; j++ ) {
for ( k = from; k < to; k++ ) {
Monitor.Enter( m_C[i,j] );
try {
m_C[i,j] += m_A[i,k] * m_B[k,j];
} finally {
Monitor.Exit( m_C[i,j] );
}
}
}
...
В прототипах методов Enter и Exit указано, что они должны получать ссылочный тип object ; соответственно тип-значение будет упакован, и методу Enter будет передан свой экземпляр упакованного типа-значения, на который будет поставлена блокировка, а методу Exit - свой экземпляр, на котором блокировки никогда не было. Понятно, что все остальные потоки будут создавать и множить свои собственные упакованные представления типов-значений, и никакой синхронизации не произойдет. Поэтому при использовании мониторов важно проследить, чтобы вызовы разных методов в разных потоках использовали один общий объект ссылочного типа.
Можно выделить интересный момент - типы объектов сами являются экземплярами класса Type, и для них выделяется место в управляемой куче. Это позволяет использовать тип объекта в качестве владельца записи SyncBlock:
...
for ( j = 0; j < m_size; j++ ) {
for ( k = from; k < to; k++ ) {
Monitor.Enter( typeof(double) );
try {
m_C[i,j] += m_A[i,k] * m_B[k,j];
} finally {
Monitor.Exit( typeof(double) );
}
}
}
...
Возможно неявное использование мониторов в C# с помощью ключевого слова lock:
lock ( obj ) { ... }
эквивалентна
Monitor.Enter( obj ); try { ... }
finally { Mointor.Exit( obj ); }
Использование ключевого слова lock предпочтительно, так как при этом выполняется дополнительная синтаксическая проверка - попытка использовать для блокировки тип-значение приведет к диагностируемой компилятором ошибке, вместо трудно отлавливаемой ошибки во время исполнения:
public static void ThreadProc()
{
int i,j,k, from, to;
double R;
from = (Interlocked.Increment(ref m_stripused) - 1) * m_stripsize;
to = from + m_stripsize;
if ( to > m_size ) to = m_size;
for ( i = 0; i < m_size; i++ ) {
for ( j = 0; j < m_size; j++ ) {
R = 0;
for ( k = from; k < to; k++ ) R += m_A[i,k]*m_B[k,j];
lock ( m_C ) { m_C[i,j] += R; }
}
}
}
Данный пример показывает процедуру потока, осуществляющего пополосное умножение матриц с необходимой синхронизацией. Следует заметить, что синхронизация доступа требует дополнительных ресурсов процессора (в данном случае, качественно превышающих затраты на умножение и сложение двух чисел с плавающей запятой), поэтому целесообразно как можно сильнее сократить число блокировок и время их наложения. В примере для этого использована промежуточная переменная R, накапливающая
Следует особо подчеркнуть, что мониторы и блокировки доступа только лишь позволяют разработчику реализовать соответствующую синхронизацию, но ни в коем случае не осуществляют принудительное ограничение конкурентного обращения к полям и методам объектов. Любой параллельно выполняющийся фрагмент кода сохраняет полную возможность обращаться со всеми объектами, независимо от того, связаны они с какими-либо блокировками или нет. Для синхронизации и блокирования доступа необходимо, чтобы все участники синхронизации явным образом использовали
Ожидающие объекты
.NET предоставляет базовый класс WaitHandle, служащий для описания объекта, который находится в одном из двух состояний: занятом или свободном. На основе этого класса строятся другие классы синхронизирующих объектов .NET, такие как события ( ManualResetEvent и AutoResetEvent ) и ).
Класс WaitHandle является, по сути, оберткой объектов ядра операционной системы, поддерживающих интерфейс синхронизации. Свойство Handle объекта WaitHandle позволяет установить (или узнать) соответствие этого объекта .NET с объектом ядра операционной системы.
Существует три метода класса WaitHandle для ожидания освобождения объекта: метод WaitOne, являющийся методом объекта, и статические методы WaitAny и WaitAll. Метод WaitOne является оберткой вызова WaitForSingleObject Win32 API, а методы WaitAny и WaitAll - вызова WaitForMultipleObjects. Соответственно семантике конкретных объектов ядра, представленных объектом WaitHandle, методы Wait... могут изменять или не изменять состояние ожидаемого объекта. Так, например, для событий с ручным сбросом ( ManualResetEvent ) состояние не меняется, а события с автоматическим сбросом и AutoResetEvent, ) переводятся в занятое состояние.
Объекты класса WaitHandle и производных от него, представляя объекты ядра операционной системы, могут быть использованы для
using System;
using System.Threading;
namespace TestNamespace {
public class SomeData {
public const int m_queries = 10;
private static int m_counter = 0;
private static Mutex m_mutex = new Mutex();
private static ManualResetEvent m_event =
new ManualResetEvent( false );
public static void Invoke( int no ) {
m_mutex.WaitOne();
m_counter++;
if ( m_counter >= m_queries ) m_event.Set();
m_mutex.ReleaseMutex();
m_event.WaitOne();
}
}
public delegate void AsyncProcCallback( int no );
class TestApp {
public static void Main() {
int i;
WaitHandle[] wh;
AsyncProcCallback apd;
wh = new WaitHandle[ SomeData.m_queries ];
apd = new AsyncProcCallback( SomeData.Invoke );
for ( i = 0; i < SomeData.m_queries; i++ ) wh[i] =
apd.BeginInvoke(i,null,null).AsyncWaitHandle;
WaitHandle.WaitAll( wh );
}
}
}
Приведенный пример показывает синхронизацию с использованием WaitHandle, представляющего состояние асинхронного вызова. В примере делается 10 асинхронных вызовов, после чего приложение ожидает завершения всех вызовов с помощью метода WaitAll. Каждый асинхронный метод в секции кода, защищаемой мьютексом (здесь было бы эффективнее использовать монитор или блокировку), подсчитывает число сделанных вызовов и переходит к ожиданию занятого события. Самый последний асинхронный вызов установит событие в свободное состояние, после чего все вызовы должны завершиться.
Помимо использования разных синхронизирующих объектов, в этом примере интересно поведение CLR: асинхронные вызовы должны обрабатываться в пуле потоков, однако число вызовов превышает число потоков в пуле. CLR по мере необходимости добавляет в пул потоки для обработки поступающих запросов.
Потоки не являются наследниками класса WaitHandle в силу того, что для разных базовых платформ потоки могут быть реализованы в качестве потоков операционной системы или легковесных потоков, управляемых CLR. В последнем случае потоки .NET не будут иметь никаких аналогов среди объектов ядра операционной системы. Для синхронизации с потоками надо использовать метод Join класса Thread.
Один "писатель", много "читателей"
Одной из
.NET предоставляет весьма эффективное ReaderWriterLock. В приводимом ниже примере демонстрируется применение методов . и Release... для корректного использования блокировки доступа при чтении и записи. Тестовый класс содержит две целочисленные переменные, которые считываются и увеличиваются на 1 с небольшими задержками по отношению друг к другу. Пока операции синхронизируются, попытка чтения или изменения всегда будет возвращать четный результат, а вот если бы синхронизация не выполнялась, то в некоторых случаях получались бы нечетные числа:
using System;
using System.Threading;
namespace TestNamespace {
public class SomeData {
public const int m_queries = 10;
private ReaderWriterLock m_rwlock = new ReaderWriterLock();
private int m_a = 0, m_b = 0;
public int summ() {
int r;
m_rwlock.AcquireReaderLock( -1 );
try {
r = m_a; Thread.Sleep( 1000 ); return r + m_b;
} finally {
m_rwlock.ReleaseReaderLock();
}
}
public int inc() {
m_rwlock.AcquireWriterLock( -1 );
try {
m_a++; Thread.Sleep( 500 ); m_b++;
return m_a + m_b;
} finally {
m_rwlock.ReleaseWriterLock();
}
}
public static void Invoke( SomeData sd, int no ) {
if ( no % 2 == 0 ) {
Console.WriteLine( sd.inc() );
} else {
Console.WriteLine( sd.summ() );
}
}
}
public delegate void AsyncProcCallback(SomeData sd, int no);
class TestApp {
public static void Main() {
int i;
SomeData sd = new SomeData();
WaitHandle[] wh;
AsyncProcCallback apd;
wh = new WaitHandle[ SomeData.m_queries ];
apd = new AsyncProcCallback( SomeData.Invoke );
for ( i = 0; i < SomeData.m_queries; i++ ) wh[i] =
apd.BeginInvoke(sd,i,null,null).AsyncWaitHandle;
WaitHandle.WaitAll( wh );
}
}
}
Конечно, аналогичного эффекта можно было бы добиться, просто используя блокировку ( lock или методы класса Monitor ) при доступе к объекту. Однако, такой подход потребует наложить блокировку исключительного доступа при чтении данных, что не эффективно. В обычных условиях вполне допустимо чтение данных несколькими одновременно выполняющимися потоками, что может дать заметное ускорение.
Применение локальной для потока памяти в .NET опирается на
Декларативный подход сводится к использованию атрибута ThreadStaticAttribute перед описанием любого
class SomeData {
[ThreadStatic]
public static double xxx;
...
Поле класса SomeData.xxx будет размещено в локальной для каждого потока памяти.
Императивный подход связан с применением методов AllocateDataSlot, AllocateNamedDataSlot, GetNamedDataSlot, FreeNamedDataSlot, GetData и SetData класса Thread. Использование этих методов очень похоже на использование . функций Win32 API, с той разницей, что вместо целочисленного индекса в LocalDataStoreSlot, который выполняет функции прежнего индекса:
class SomeData {
private static LocalDataStoreSlot m_tls = Thread.AllocateDataSlot();
public static void ThreadProc() {
Thread.SetData( m_tls, ... );
...
}
public void Main() {
SomeData sd = new SomeData();
...
// создание и запуск потоков
}
}
Методы Allocate... и GetNamedDataSlot позволяют выделить новую ячейку в GetData и SetData позволяют получить или сохранить ссылку на объект в
.NET предлагает два вида таймеров: один описан в пространстве имен System.Timers, а другой - в пространстве имен System.Threading.
Таймер пространства имен System.Threading является опечатанным и предназначен для вызова указанной асинхронной процедуры с заданным интервалом времени.
Таймер пространства имен System.Timers может быть использован для создания собственных классов-потомков - в нем вместо процедуры асинхронного вызова применяется обработка события, с которым может быть сопоставлено несколько обработчиков. Кроме того, этот таймер может вызывать обработку события конкретным потоком, а не произвольным потоком пула:
using System;
using System.Timers;
namespace TestNamespace {
class TestTimer : Timer {
private int m_minimal, m_maximal, m_counter;
public int count { get{ return m_counter - m_minimal; }}
public TestTimer( int mn, int mx ) {
Elapsed += new ElapsedEventHandler(OnElapsed);
m_minimal = m_counter = mn;
m_maximal = mx;
AutoReset = true;
Interval = 400;
}
static void OnElapsed( object src, ElapsedEventArgs e ) {
TestTimer tt = (TestTimer)src;
if ( tt.m_counter < tt.m_maximal ) tt.m_counter++;
if ( tt.m_counter >= tt.m_maximal ) tt.Stop();
}
static void Main(string[] args) {
TestTimer tm = new TestTimer( 0, 10 );
tm.Start();
Thread.Sleep( 5000 );
tm.Stop();
}
}
}
Приведенный выше пример иллюстрирует использование таймера пространства имен System.Timers.
Реализация параллельного выполнения кода в .NET основана на базовых механизмах, предоставляемых ядром операционной системы Windows. Аналогично средствам операционной системы средства .NET Framework могут быть разделены на следующие группы:
Как и в случае API системы, такое деление не является строгим - реальные средства "пересекают" границы этих категорий. При этом не все механизмы, предоставляемые операционной системой, нашли свое отражение в .NET Framework; равно как многие механизмы, оставаясь внешне схожими с механизмами операционной системы, существенным образом изменились. Так, например, .NET не поддерживает волокна;
Вообще говоря, использование фоновых потоков для выполнения специфических задач поддержки инфраструктуры .NET стало общим местом. При запуске .NET приложения автоматически создается пул потоков, используемый CLR по мере надобности. Этот пул применяется, в частности, в ситуациях, связанных с обработкой асинхронных запросов и
.NET разрабатывался с учетом возможности переноса на другие платформы. Для облегчения этого процесса в архитектуру CLR включен специальный уровень адаптации к платформе (
В данном курсе рассматриваются основные средства реализации многопоточных приложений и не затрагиваются вопросы создания ASP, COM-объектов и многого другого.
Потоки в .NET реализованы на основе модели потоков операционной системы и не предусматривают средств управления волокнами. Если система не поддерживает многопоточные приложения, то
Основные классы для реализации многопоточных приложений определены в пространстве имен System.Threading. Для описания собственных потоков предназначен класс Thread. При создании потока ему необходимо указать делегата, реализующего процедуру потока. К сожалению, в .NET, во-первых, не предусмотрено передачи аргументов в эту процедуру, во-вторых, процедура должна быть статическим методом, и в-третьих, класс Thread является опечатанным. В результате передача каких-либо данных в процедуру потока вызывает определенные трудности и требует явного или косвенного использования
Приведенный ниже пример демонстрирует работу с созданием нескольких потоков для параллельного перемножения двух квадратных матриц. Начальные значения всех элементов матриц равны 1, поэтому результирующая матрица должна быть заполнена числами, равными размерности перемножаемых матриц. В процессе умножения и суммирования элементов матриц синхронизация не выполняется, поэтому при достаточно большом размере матриц гарантированно будут возникать ошибки (необходимо синхронизировать выполнение некоторых действий в потоках, чтобы избежать возникновения ошибок, - об этом ниже, при рассмотрении
using System;
using System.Threading;
namespace TestNamespace {
class TestApp {
const int m_size = 600;
const int m_stripsize = 50;
const int m_stripmax = 12;
private static int m_stripused = 0;
private static double[,] m_A = new double[m_size,m_size],
m_B = new double[m_size,m_size],
m_C = new double[m_size,m_size];
public static void ThreadProc()
{
int i,j,k, from, to;
from = ( m_stripused++ ) * m_stripsize;
to = from + m_stripsize;
if ( to > m_size ) to = m_size;
for ( i = 0; i < m_size; i++ ) {
for ( j = 0; j < m_size; j++ ) {
for ( k = from; k < to; k++ )
m_C[i,j] += m_A[i,k] * m_B[k,j];
}
}
}
public static void Main()
{
Thread[] T = new Thread[ m_stripmax ];
int i,j,errs;
for ( i = 0; i < m_size; i++ ) {
for ( j = 0; j < m_size; j++ ) {
m_A[i,j] = m_B[i,j] = 1.0;
m_C[i,j] = 0.0;
}
}
for ( i = 0; i < m_stripmax; i++ ) {
T[i] = new Thread(new ThreadStart(ThreadProc));
T[i].Start();
}
// дожидаемся завершения всех потоков
for ( i = 0; i < m_stripmax; i++ ) T[i].Join();
// проверяем результат
errs = 0;
for ( i = 0; i < m_size; i++ )
for ( j = 0; j < m_size; j++ )
if ( m_C[i,j] != m_size ) errs++;
Console.WriteLine("Error count = {0}", errs );
}
}
}
Поток в .NET может находиться в одном из следующих состояний: незапущенном, исполнения, ожидания, приостановленном, завершенном и прерванном. Возможные переходы между этими состояниями изображены на рис. 7.1.
(рис 7.1) Состояния потока
Сразу после создания и до начала выполнения потока он находится в незапущенном состоянии ( Unstarted ). Текущее состояние можно определить с помощью свойства Thread.ThreadState. После запуска поток можно перевести в состояние исполнения ( Running ) вызовом метода Thread.Start. Работающий поток может быть переведен в состояние ожидания ( WaitSleepJoin ) явным или неявным вызовом соответствующих методов ( Thread.Sleep, Thread.Join и др.) или приостановлен ( ) с помощью метода Thread.. Исполнение приостановленного потока можно возобновить вызовом метода Thread.. Также можно досрочно вывести поток из состояния ожидания вызовом метода Thread.Interrupt.
Завершение функции потока нормальным образом переводит поток в состояние "завершен" ( Stopped ), а досрочное прекращение работы вызовом метода Thread.Abort переведет его в состояние "прерван" ( Aborted ). Кроме того, .NET поддерживает несколько переходных состояний ( AbortRequested, StopRequested и SuspendRequested ). Состояния потока в общем случае могут комбинироваться, например, вполне корректно сочетание состояния ожидания ( WaitSleepJoin ) и какого-либо переходного, скажем, AbortRequested.
Для выполнения задержек в ходе выполнения потока предназначены два метода - Sleep, переводящий поток в состояние ожидания на заданное время, и SpinWait, который выполняет некоторую задержку путем многократных повторов внутреннего цикла. Этот метод дает высокую
Для получения и задания Thread.Priority. Highest, AboveNormal, Normal, BelowNormal и Lowest.
Когда .NET приложение начинает исполняться в среде Windows, CLR создает внутренний пул потоков, используемый средой для реализации асинхронных операций ввода-вывода, вызова асинхронных процедур, обработки таймеров и других целей. Потоки могут добавляться в пул по мере надобности. Этот пул реализуется на основе пула потоков, управляемого операционной системой (построенного на основе порта завершения ввода-вывода). Для взаимодействия с пулом потоков предусмотрен класс ThreadPool, и единственный объект, принадлежащий этому классу, создается CLR при запуске приложения. Все домены приложений в рамках одного процесса используют общий пул потоков.
Разработчики могут использовать несколько статических методов класса ThreadPool. Так, например, существует возможность связать внутренний порт завершения ввода-вывода с файловым объектом, созданным неуправляемым кодом, для обработки событий, связанных с завершением ввода-вывода этим объектом (см. методы ThreadPool.BindHandle и описание порта завершения ввода-вывода ранее). Можно управлять числом потоков в пуле (методы GetAvailableThreads, GetMaxThreads, GetMinThreads и SetMinThreads ), можно ставить в очередь асинхронных вызовов собственные процедуры (метод QueueUserWorkItem ) и назначать процедуры, которые будут вызываться при освобождении какого-либо объекта (метод RegisterWaitForSingleObject ). Эти два метода имеют "безопасные" и "небезопасные" ( .) версии; последние отличаются тем, что в
Для реализации System.IO.Stream. В этом классе определены абстрактные синхронные методы чтения Read и записи Write, а также реализация асинхронных методов BeginRead, EndRead, BeginWrite и EndWrite. Асинхронные методы реализованы с помощью обращения к синхронным операциям фоновыми потоками пула.
На основе абстрактного класса Stream в .NET Framework реализуются потомки, осуществляющие взаимодействие с разного рода потоками данных. Так, например, System.IO.FileStream реализует операции с файлами, System.IO.MemoryStream предоставляет возможность использования байтового массива в качестве источника или получателя данных, System.IO.BufferedStream является "надстройкой" над другими объектами, производными от System.IO.Stream, и обеспечивает буферизацию запросов чтения и записи. Некоторые классы вне пространства имен Sytem.IO также являются потомками Stream. Так, например, класс System.NET.Sockets.NetworkStream обеспечивает сетевое взаимодействие.
using System;
using System.IO;
namespace TestNamespace {
class TestApp {
private const int m_size = 100000000;
private static byte[] m_data = new byte [m_size];
public TestApp()
{
int i;
for ( i = 0; i < m_size; i++ ) m_data[i] = (byte)i;
}
public static void DoneWritting( IAsyncResult state ) {
}
static void Main(string[] args) {
TestApp ta = new TestApp();
IAsyncResult state;
Stream st = new FileStream(
"test.dat", FileMode.OpenOrCreate,
FileAccess.ReadWrite, FileShare.Read, 1, true
);
state = st.BeginWrite(
m_data, 0, m_size,
new AsyncCallback(DoneWritting), null
);
// код в этом месте будет выполняться
// одновременно с выводом данных
st.EndWrite( state );
st.Close();
}
}
}
Данный пример демонстрирует использование FileStream для выполнения асинхронной операции записи большого объема данных.
При реализации собственных потомков класса Stream, возможно, будет иметь смысл переопределить не только Read и Write, но также некоторые базовые (например, BeginRead, ReadByte и др.), универсальная реализация которых может быть неэффективной в конкретном случае.
Для реализации вызова асинхронных процедур в .NET используются фоновые потоки пула, так же как для обработки асинхронных операций ввода-вывода. Класс ThreadPool предлагает два способа для вызова асинхронных процедур: явное размещение вызовов в очереди ( QueueUserWorkItem ) и связывание вызовов с переводом некоторых объектов в свободное состояние ( RegisterWaitForSingleObject ). Кроме того, .NET позволяет осуществлять асинхронные вызовы любых процедур с помощью метода BeginInvoke делегатов.
Статический метод ThreadPool.QueueUserWorkItem ставит вызов указанной процедуры в очередь для обработки. Если пул содержит простаивающие потоки, то обработка этой функции начнется немедленно:
using System;
using System.Threading;
namespace TestNamespace {
class GreetingData {
private string m_greeting;
public GreetingData( string text ) { m_greeting = text; }
public void Invoke() { Console.WriteLine( m_greeting ); }
}
class TestApp {
static void AsyncProc( Object arg )
{
GreetingData gd = (GreetingData)arg;
gd.Invoke();
}
public static void Main()
{
GreetingData gd = new GreetingData("Hello, world!");
ThreadPool.QueueUserWorkItem(new WaitCallback(AsyncProc), gd);
Thread.Sleep( 1000 );
}
}
}
При постановке в очередь асинхронного вызова можно указать объект, который является аргументом асинхронной процедуры (при создании собственных потоков передача аргументов процедуре потока затруднительна).
Второй способ вызова асинхронных процедур связан с использованием объектов, производных от класса System.Threading.WaitHandle (это события и
using System;
using System.Threading;
namespace TestNamespace {
class GreetingData {
private string m_greeting;
private RegisteredWaitHandle m_waithandle;
public GreetingData( string text ) { m_greeting = text; }
public void Invoke() { Console.WriteLine( m_greeting ); }
public RegisteredWaitHandle WaitHandle {
set {
if (value==null) m_waithandle.Unregister( null );
m_waithandle = value;
}
}
}
class TestApp {
static void AsyncProc( Object arg, bool isTimeout ) {
GreetingData gd = (GreetingData)arg;
if ( !isTimeout ) gd.WaitHandle = null;
gd.Invoke();
}
public static void Main() {
GreetingData gd = new GreetingData("Hello");
AutoResetEvent ev = new AutoResetEvent(false);
gd.WaitHandle=ThreadPool.RegisterWaitForSingleObject(
ev, new WaitOrTimerCallback(AsyncProc),
gd, 1000, false
);
Thread.Sleep( 2500 );
ev.Set();
Console.ReadLine();
}
}
}
Приведенный пример демонстрирует использование периодического вызова асинхронной процедуры - при регистрации делегата ( RegisterWaitForSingleObject ) указывается максимальное время ожидания 1 секунда (1000 миллисекунд), после чего основной поток переводится в состояние "спячки" на 2.5 секунды. За это время в очередь пула поступает два вызова асинхронных процедур (с признаком вызова по тайм-ауту). Через 2.5 секунды основной поток пробуждается, переводит событие в свободное состояние, и в очередь пула поступает третий вызов. При обработке этого вызова регистрация делегата отменяется.
Последний способ связан с использованием методов BeginInvoke и EndInvoke делегатов. Когда определяется какой-либо делегат функции, для него будут определены методы: BeginInvoke (содержащий все аргументы делегата плюс два дополнительных - AsyncCallback, который может быть вызван по завершении обработки асинхронного вызова, и AsyncState, с помощью которого можно определить состояние асинхронной процедуры) и EndInvoke, содержащий все выходные параметры (т.е. описанные как inout или out ), плюс IAsyncResult, позволяющий узнать результат выполнения процедуры.
Таким образом, использование BeginInvoke позволяет не только поставить в очередь вызов асинхронной процедуры, но также связать с завершением ее обработки еще один асинхронный вызов. Метод EndInvoke служит для ожидания завершения обработки асинхронной процедуры:
using System;
using System.Threading;
namespace TestNamespace {
public class GreetingData {
private string m_greeting;
public GreetingData( string text ) { m_greeting = text; }
public static void Invoke( GreetingData arg ) {
Console.WriteLine( arg.m_greeting );
}
}
public delegate void AsyncProcCallback ( GreetingData gd );
class TestApp {
public static void Main() {
GreetingData gd = new GreetingData( "Hello!!!" );
AsyncProcCallback apd = new AsyncProcCallback(
GreetingData.Invoke );
IAsyncResult ar = apd.BeginInvoke( gd, null, null );
ar.AsyncWaitHandle.WaitOne();
}
}
}
Данный пример иллюстрирует вызов асинхронной процедуры с использованием метода BeginInvoke и альтернативный механизм ожидания завершения - с использованием внутреннего объекта AsyncWaitHandle (класса WaitHandle ), благодаря которому, собственно говоря, становится возможен вызов асинхронной процедуры, обслуживающей завершение обработки данной процедуры.
В этом смысле асинхронный вызов процедур с помощью BeginInvoke очень близок к обработке асинхронных операций ввода-вывода.
Проблемы, встающие перед разработчиками многопоточных приложений .NET, очень похожи на проблемы разработчиков приложений Win32 API. Соответственно, .NET предоставляет в значительной мере близкий набор средств взаимодействия потоков и их взаимной синхронизации.
К этим средствам относятся
Платформа .NET предоставляет, аналогично базовой операционной системе Windows, набор некоторых основных операций над целыми числами ( int и long ), которые могут выполняться атомарно. Для этого предусмотрены четыре статических метода класса System.Threading., а именно Increment, , Exchange и CompareExchange. Применение этих методов аналогично соответствующим . процедурам Win32 API.
Возвращаясь к примеру использования потоков для умножения матриц, можно выделить один момент, требующий исправления: самое начало процедуры потока, там, где определяется номер полосы:
public static void ThreadProc()
{
int i,j,k, from, to;
from = ( m_stripused++ ) * m_stripsize;
to = from + m_stripsize;
...
Здесь потенциально возможна ситуация, когда несколько потоков одновременно начнут выполнять этот код и получат идентичные номера полос. В этом месте самым эффективным было бы использование атомарных операций для увеличения значения поля m_stripused. Для этого фрагмент надо переписать:
public static void ThreadProc()
{
int i,j,k, from, to;
from = (Interlocked.Increment(ref m_stripused) - 1 ) * m_stripsize;
to = from + m_stripsize;
...
Основные средства взаимной
Последний синхронизирующий объект ReaderWriterLock закрывает очень типичный класс
Мониторы
Мониторы в .NET являются аналогами SyncBlock, являющаяся, по сути, аналогом структуры CRITICAL_SECTION в Win32 API. Добавление такой записи к каждому объекту в управляемой куче чересчур накладно, особенно если учесть, что используются они относительно редко. Поэтому все записи SyncBlock выносятся в отдельный кэш, а в информацию об объекте включается ссылка на запись кэша (см. рис. 7.2). Такой прием позволяет, с одной стороны, содержать кэш синхронизирующих записей минимального размера, а с другой - любому объекту при необходимости можно сопоставить запись.
(рис 7.2) Использование кэша SyncBlock записей объектами управляемой кучи
Обычно объекты не имеют сопоставленной с ними SyncBlock записи, однако она автоматически выделяется при первом использовании монитора.
Класс Monitor, определенный в пространстве имен System.Threading, предлагает несколько статических методов для работы с записями синхронизации. Методы Enter и Exit являются наиболее применяемыми и соответствуют функциям EnterCriticalSection и LeaveCriticalSection операционной системы. Аналогично Monitor - Wait, и PulseAll - позволяют при необходимости временно разрешить доступ к объекту другому потоку, ожидающему его освобождения, не покидая критической секции.
Продолжим рассмотрение примера с многопоточным умножением матриц. Помимо уже рассмотренной проблемы с назначением полос, в процедуре потока есть еще одно некорректное место - прибавление накоплением к элементу результирующей матрицы произведения двух элементов исходной матрицы:
public static void ThreadProc()
{
int i,j,k, from, to;
from = (Interlocked.Increment(ref m_stripused)-1)
* m_stripsize;
to = from + m_stripsize;
if ( to > m_size ) to = m_size;
for ( i = 0; i < m_size; i++ ) {
for ( j = 0; j < m_size; j++ ) {
for ( k = from; k < to; k++ )
m_C[i,j] += m_A[i,k] * m_B[k,j];
}
}
}
Так как эта операция выполняется не атомарно, то вполне может быть так, что один поток считывает значение m_C[i,j], прибавляет к нему величину m_A[i,k] * m_B[k,j] и, прежде чем успевает записать в m_C[i,j] результат сложения, прерывается другим потоком. Второй поток успевает изменить величину m_C[i,j], потом первый снова пробуждается и записывает значение, вычисленное для предыдущего состояния элемента m_C[i,j], - то есть некорректную величину. Собственно говоря, именно эта ситуация и приводит к ошибкам, которые можно наблюдать в исходном примере.
Ситуацию можно исправить, используя синхронизацию при доступе к элементу m_C[i,j] с помощью мониторов:
...
for ( j = 0; j < m_size; j++ ) {
for ( k = from; k < to; k++ ) {
Monitor.Enter( m_C );
try {
m_C[i,j] += m_A[i,k] * m_B[k,j];
} finally {
Monitor.Exit( m_C );
}
}
}
...
В этом фрагменте надо выделить два существенных момента: во-первых, использование метода Exit в блоке finally, а во-вторых - использование всего массива m_C, а не отдельного элемента m_C[i,j].
Первое надо взять за правило, так как в случае возникновения исключения в критической секции блокировка может остаться занятой (т.е. в случае покидания секции без вызова метода Exit ).
Второе связано с тем, что элементы m_C[i,j] являются значениями, а не ссылочными типами. Для типов-значений соответствующее представление в управляемой куче не создается, и у них нет и не может быть ссылок на синхронизирующие записи SyncBlock.
Самое плохое в этой ситуации то, что попытка собрать приложение, использующее типы-значения в качестве аргументов методов Enter и Exit (как в примере ниже), пройдет успешно:
...
for ( j = 0; j < m_size; j++ ) {
for ( k = from; k < to; k++ ) {
Monitor.Enter( m_C[i,j] );
try {
m_C[i,j] += m_A[i,k] * m_B[k,j];
} finally {
Monitor.Exit( m_C[i,j] );
}
}
}
...
В прототипах методов Enter и Exit указано, что они должны получать ссылочный тип object ; соответственно тип-значение будет упакован, и методу Enter будет передан свой экземпляр упакованного типа-значения, на который будет поставлена блокировка, а методу Exit - свой экземпляр, на котором блокировки никогда не было. Понятно, что все остальные потоки будут создавать и множить свои собственные упакованные представления типов-значений, и никакой синхронизации не произойдет. Поэтому при использовании мониторов важно проследить, чтобы вызовы разных методов в разных потоках использовали один общий объект ссылочного типа.
Можно выделить интересный момент - типы объектов сами являются экземплярами класса Type, и для них выделяется место в управляемой куче. Это позволяет использовать тип объекта в качестве владельца записи SyncBlock:
...
for ( j = 0; j < m_size; j++ ) {
for ( k = from; k < to; k++ ) {
Monitor.Enter( typeof(double) );
try {
m_C[i,j] += m_A[i,k] * m_B[k,j];
} finally {
Monitor.Exit( typeof(double) );
}
}
}
...
Возможно неявное использование мониторов в C# с помощью ключевого слова lock:
lock ( obj ) { ... }
эквивалентна
Monitor.Enter( obj ); try { ... }
finally { Mointor.Exit( obj ); }
Использование ключевого слова lock предпочтительно, так как при этом выполняется дополнительная синтаксическая проверка - попытка использовать для блокировки тип-значение приведет к диагностируемой компилятором ошибке, вместо трудно отлавливаемой ошибки во время исполнения:
public static void ThreadProc()
{
int i,j,k, from, to;
double R;
from = (Interlocked.Increment(ref m_stripused) - 1) * m_stripsize;
to = from + m_stripsize;
if ( to > m_size ) to = m_size;
for ( i = 0; i < m_size; i++ ) {
for ( j = 0; j < m_size; j++ ) {
R = 0;
for ( k = from; k < to; k++ ) R += m_A[i,k]*m_B[k,j];
lock ( m_C ) { m_C[i,j] += R; }
}
}
}
Данный пример показывает процедуру потока, осуществляющего пополосное умножение матриц с необходимой синхронизацией. Следует заметить, что синхронизация доступа требует дополнительных ресурсов процессора (в данном случае, качественно превышающих затраты на умножение и сложение двух чисел с плавающей запятой), поэтому целесообразно как можно сильнее сократить число блокировок и время их наложения. В примере для этого использована промежуточная переменная R, накапливающая
Следует особо подчеркнуть, что мониторы и блокировки доступа только лишь позволяют разработчику реализовать соответствующую синхронизацию, но ни в коем случае не осуществляют принудительное ограничение конкурентного обращения к полям и методам объектов. Любой параллельно выполняющийся фрагмент кода сохраняет полную возможность обращаться со всеми объектами, независимо от того, связаны они с какими-либо блокировками или нет. Для синхронизации и блокирования доступа необходимо, чтобы все участники синхронизации явным образом использовали
Ожидающие объекты
.NET предоставляет базовый класс WaitHandle, служащий для описания объекта, который находится в одном из двух состояний: занятом или свободном. На основе этого класса строятся другие классы синхронизирующих объектов .NET, такие как события ( ManualResetEvent и AutoResetEvent ) и ).
Класс WaitHandle является, по сути, оберткой объектов ядра операционной системы, поддерживающих интерфейс синхронизации. Свойство Handle объекта WaitHandle позволяет установить (или узнать) соответствие этого объекта .NET с объектом ядра операционной системы.
Существует три метода класса WaitHandle для ожидания освобождения объекта: метод WaitOne, являющийся методом объекта, и статические методы WaitAny и WaitAll. Метод WaitOne является оберткой вызова WaitForSingleObject Win32 API, а методы WaitAny и WaitAll - вызова WaitForMultipleObjects. Соответственно семантике конкретных объектов ядра, представленных объектом WaitHandle, методы Wait... могут изменять или не изменять состояние ожидаемого объекта. Так, например, для событий с ручным сбросом ( ManualResetEvent ) состояние не меняется, а события с автоматическим сбросом и AutoResetEvent, ) переводятся в занятое состояние.
Объекты класса WaitHandle и производных от него, представляя объекты ядра операционной системы, могут быть использованы для
using System;
using System.Threading;
namespace TestNamespace {
public class SomeData {
public const int m_queries = 10;
private static int m_counter = 0;
private static Mutex m_mutex = new Mutex();
private static ManualResetEvent m_event =
new ManualResetEvent( false );
public static void Invoke( int no ) {
m_mutex.WaitOne();
m_counter++;
if ( m_counter >= m_queries ) m_event.Set();
m_mutex.ReleaseMutex();
m_event.WaitOne();
}
}
public delegate void AsyncProcCallback( int no );
class TestApp {
public static void Main() {
int i;
WaitHandle[] wh;
AsyncProcCallback apd;
wh = new WaitHandle[ SomeData.m_queries ];
apd = new AsyncProcCallback( SomeData.Invoke );
for ( i = 0; i < SomeData.m_queries; i++ ) wh[i] =
apd.BeginInvoke(i,null,null).AsyncWaitHandle;
WaitHandle.WaitAll( wh );
}
}
}
Приведенный пример показывает синхронизацию с использованием WaitHandle, представляющего состояние асинхронного вызова. В примере делается 10 асинхронных вызовов, после чего приложение ожидает завершения всех вызовов с помощью метода WaitAll. Каждый асинхронный метод в секции кода, защищаемой мьютексом (здесь было бы эффективнее использовать монитор или блокировку), подсчитывает число сделанных вызовов и переходит к ожиданию занятого события. Самый последний асинхронный вызов установит событие в свободное состояние, после чего все вызовы должны завершиться.
Помимо использования разных синхронизирующих объектов, в этом примере интересно поведение CLR: асинхронные вызовы должны обрабатываться в пуле потоков, однако число вызовов превышает число потоков в пуле. CLR по мере необходимости добавляет в пул потоки для обработки поступающих запросов.
Потоки не являются наследниками класса WaitHandle в силу того, что для разных базовых платформ потоки могут быть реализованы в качестве потоков операционной системы или легковесных потоков, управляемых CLR. В последнем случае потоки .NET не будут иметь никаких аналогов среди объектов ядра операционной системы. Для синхронизации с потоками надо использовать метод Join класса Thread.
Один "писатель", много "читателей"
Одной из
.NET предоставляет весьма эффективное ReaderWriterLock. В приводимом ниже примере демонстрируется применение методов . и Release... для корректного использования блокировки доступа при чтении и записи. Тестовый класс содержит две целочисленные переменные, которые считываются и увеличиваются на 1 с небольшими задержками по отношению друг к другу. Пока операции синхронизируются, попытка чтения или изменения всегда будет возвращать четный результат, а вот если бы синхронизация не выполнялась, то в некоторых случаях получались бы нечетные числа:
using System;
using System.Threading;
namespace TestNamespace {
public class SomeData {
public const int m_queries = 10;
private ReaderWriterLock m_rwlock = new ReaderWriterLock();
private int m_a = 0, m_b = 0;
public int summ() {
int r;
m_rwlock.AcquireReaderLock( -1 );
try {
r = m_a; Thread.Sleep( 1000 ); return r + m_b;
} finally {
m_rwlock.ReleaseReaderLock();
}
}
public int inc() {
m_rwlock.AcquireWriterLock( -1 );
try {
m_a++; Thread.Sleep( 500 ); m_b++;
return m_a + m_b;
} finally {
m_rwlock.ReleaseWriterLock();
}
}
public static void Invoke( SomeData sd, int no ) {
if ( no % 2 == 0 ) {
Console.WriteLine( sd.inc() );
} else {
Console.WriteLine( sd.summ() );
}
}
}
public delegate void AsyncProcCallback(SomeData sd, int no);
class TestApp {
public static void Main() {
int i;
SomeData sd = new SomeData();
WaitHandle[] wh;
AsyncProcCallback apd;
wh = new WaitHandle[ SomeData.m_queries ];
apd = new AsyncProcCallback( SomeData.Invoke );
for ( i = 0; i < SomeData.m_queries; i++ ) wh[i] =
apd.BeginInvoke(sd,i,null,null).AsyncWaitHandle;
WaitHandle.WaitAll( wh );
}
}
}
Конечно, аналогичного эффекта можно было бы добиться, просто используя блокировку ( lock или методы класса Monitor ) при доступе к объекту. Однако, такой подход потребует наложить блокировку исключительного доступа при чтении данных, что не эффективно. В обычных условиях вполне допустимо чтение данных несколькими одновременно выполняющимися потоками, что может дать заметное ускорение.
Применение локальной для потока памяти в .NET опирается на
Декларативный подход сводится к использованию атрибута ThreadStaticAttribute перед описанием любого
class SomeData {
[ThreadStatic]
public static double xxx;
...
Поле класса SomeData.xxx будет размещено в локальной для каждого потока памяти.
Императивный подход связан с применением методов AllocateDataSlot, AllocateNamedDataSlot, GetNamedDataSlot, FreeNamedDataSlot, GetData и SetData класса Thread. Использование этих методов очень похоже на использование . функций Win32 API, с той разницей, что вместо целочисленного индекса в LocalDataStoreSlot, который выполняет функции прежнего индекса:
class SomeData {
private static LocalDataStoreSlot m_tls = Thread.AllocateDataSlot();
public static void ThreadProc() {
Thread.SetData( m_tls, ... );
...
}
public void Main() {
SomeData sd = new SomeData();
...
// создание и запуск потоков
}
}
Методы Allocate... и GetNamedDataSlot позволяют выделить новую ячейку в GetData и SetData позволяют получить или сохранить ссылку на объект в
.NET предлагает два вида таймеров: один описан в пространстве имен System.Timers, а другой - в пространстве имен System.Threading.
Таймер пространства имен System.Threading является опечатанным и предназначен для вызова указанной асинхронной процедуры с заданным интервалом времени.
Таймер пространства имен System.Timers может быть использован для создания собственных классов-потомков - в нем вместо процедуры асинхронного вызова применяется обработка события, с которым может быть сопоставлено несколько обработчиков. Кроме того, этот таймер может вызывать обработку события конкретным потоком, а не произвольным потоком пула:
using System;
using System.Timers;
namespace TestNamespace {
class TestTimer : Timer {
private int m_minimal, m_maximal, m_counter;
public int count { get{ return m_counter - m_minimal; }}
public TestTimer( int mn, int mx ) {
Elapsed += new ElapsedEventHandler(OnElapsed);
m_minimal = m_counter = mn;
m_maximal = mx;
AutoReset = true;
Interval = 400;
}
static void OnElapsed( object src, ElapsedEventArgs e ) {
TestTimer tt = (TestTimer)src;
if ( tt.m_counter < tt.m_maximal ) tt.m_counter++;
if ( tt.m_counter >= tt.m_maximal ) tt.Stop();
}
static void Main(string[] args) {
TestTimer tm = new TestTimer( 0, 10 );
tm.Start();
Thread.Sleep( 5000 );
tm.Stop();
}
}
}
Приведенный выше пример иллюстрирует использование таймера пространства имен System.Timers.
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.