Московский государственный технический университет им. Н.Э. Баумана
Опубликован: 28.06.2006 | Доступ: платный | Студентов: 1741 / 13 | Оценка: 4.54 / 3.83 | Длительность: 22:03:00
ISBN: 978-5-9556-0055-0
Лекция 15:

Параллельные операции в .NET

Аннотация: Основные классы .NET, применяемые для создания многопоточных приложений и их соответствие механизмам Windows.

Реализация параллельного выполнения кода в .NET основана на базовых механизмах, предоставляемых ядром операционной системы Windows. Аналогично средствам операционной системы средства .NET Framework могут быть разделены на следующие группы:

  • Обработка асинхронных запросов. Сюда относятся средства для выполнения асинхронных операций ввода-вывода, средства для работы с очередями сообщений, некоторые надстройки для работы в сетевой среде (ASP, XML и др.) и средства поддержания инфраструктуры COM объектов.
  • Организация многопоточных приложений. Сюда относятся средства создания потоков, управления ими, включая пулы потоков, средства взаимной синхронизации, а также организация локальной для потоков памяти. По большей части .NET Framework предоставляет надстройку над средствами операционной системы.
  • Работа с различными процессами и доменами приложений. В основном это надстройки над абстракциями высокого уровня, такими как RPC, COM объекты и пр.

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

Вообще говоря, использование фоновых потоков для выполнения специфических задач поддержки инфраструктуры .NET стало общим местом. При запуске .NET приложения автоматически создается пул потоков, используемый CLR по мере надобности. Этот пул применяется, в частности, в ситуациях, связанных с обработкой асинхронных запросов и асинхронного ввода-вывода, когда в рамках операционной системы реально использовался бы основной поток в состоянии ожидания оповещения.

.NET разрабатывался с учетом возможности переноса на другие платформы. Для облегчения этого процесса в архитектуру CLR включен специальный уровень адаптации к платформе (PAL, Platform Adaptation Layer), являющийся прослойкой между основными механизмами CLR и уровнем операционной системы. В случае платформы Windows уровень PAL достаточно прост - считается, что PAL должен предоставить для CLR функциональность, аналогичную Win32 API. Однако в случае иных платформ PAL может оказаться достаточно сложным и многоуровневым. Например, в случае платформ, не поддерживающих многопоточные приложения, PAL должен самостоятельно реализовать недостающую функциональность.

В данном курсе рассматриваются основные средства реализации многопоточных приложений и не затрагиваются вопросы создания ASP, COM-объектов и многого другого.

Потоки и пул потоков

Потоки в .NET реализованы на основе модели потоков операционной системы и не предусматривают средств управления волокнами. Если система не поддерживает многопоточные приложения, то PAL должен будет реализовать собственный планировщик потоков режима пользователя (и в этом случае окажется, что потоки .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 и др.) или приостановлен ( Suspended ) с помощью метода Thread.Suspend(). Исполнение приостановленного потока можно возобновить вызовом метода Thread.Resume. Также можно досрочно вывести поток из состояния ожидания вызовом метода Thread.Interrupt.

Завершение функции потока нормальным образом переводит поток в состояние "завершен" ( Stopped ), а досрочное прекращение работы вызовом метода Thread.Abort переведет его в состояние "прерван" ( Aborted ). Кроме того, .NET поддерживает несколько переходных состояний ( AbortRequested, StopRequested и SuspendRequested ). Состояния потока в общем случае могут комбинироваться, например, вполне корректно сочетание состояния ожидания ( WaitSleepJoin ) и какого-либо переходного, скажем, AbortRequested.

Для выполнения задержек в ходе выполнения потока предназначены два метода - Sleep, переводящий поток в состояние ожидания на заданное время, и SpinWait, который выполняет некоторую задержку путем многократных повторов внутреннего цикла. Этот метод дает высокую загрузку процессора, однако позволяет реализовать очень короткие паузы. К сожалению, продолжительность пауз зависит от производительности и загруженности процессора.

Для получения и задания приоритета потока используется свойство Thread.Priority. Приоритеты потока в .NET базируются на подмножестве относительных приоритетов Win32 API так, что при переносе на другие платформы существует возможность предоставить их корректные аналоги. В .NET используются приоритеты Highest, AboveNormal, Normal, BelowNormal и Lowest.

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

Разработчики могут использовать несколько статических методов класса ThreadPool. Так, например, существует возможность связать внутренний порт завершения ввода-вывода с файловым объектом, созданным неуправляемым кодом, для обработки событий, связанных с завершением ввода-вывода этим объектом (см. методы ThreadPool.BindHandle и описание порта завершения ввода-вывода ранее). Можно управлять числом потоков в пуле (методы GetAvailableThreads, GetMaxThreads, GetMinThreads и SetMinThreads ), можно ставить в очередь асинхронных вызовов собственные процедуры (метод QueueUserWorkItem ) и назначать процедуры, которые будут вызываться при освобождении какого-либо объекта (метод RegisterWaitForSingleObject ). Эти два метода имеют "безопасные" и "небезопасные" ( Unsafe...) версии; последние отличаются тем, что в стеке вызовов асинхронных методов не будут присутствовать данные о реальном контексте безопасности потока, поставившего в очередь этот вызов, - в подобном случае будет использоваться контекст безопасности самого пула потоков.

Анастасия Булинкова
Анастасия Булинкова
Рабочим названием платформы .NET было