Обзор многопоточности

49

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

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

Разумеется, необходимо знать особенности одновременного выполнения множества потоков. Из-за того, что они выполняются в одно и то же время, при получении ими доступа к одним и тем же данным могут возникать проблемы. Чтобы этого не происходило, должны быть реализованы механизмы синхронизации.

Поток (thread) представляет собой независимую последовательность инструкций в программе. Потоки играют важную роль как для клиентских, так и для серверных приложений. К примеру, во время ввода какого-то кода C# в окне редактора Visual Studio проводится анализ на предмет различных синтаксических ошибок. Этот анализ осуществляется отдельным фоновым потоком. То же самое происходит и в средстве проверки орфографии в Microsoft Word. Один поток ожидает ввода данных пользователем, а другой в это время выполняет в фоновом режиме некоторый анализ. Третий поток может сохранять записываемые данные во временный файл, а четвертый — загружать дополнительные данные из Интернета.

В приложении, которое функционирует на сервере, один поток всегда ожидает поступления запроса от клиента и потому называется потоком-слушателем (listener thread). При получении запроса он сразу же пересылает его отдельному рабочему потоку (worker thread), который дальше сам продолжает взаимодействовать с клиентом. Поток-слушатель после этого незамедлительно возвращается к своим обязанностям по ожиданию поступления следующего запроса от очередного клиента.

Каждый процесс состоит из ресурсов, таких как оконные дескрипторы, файловые дескрипторы и другие объекты ядра, имеет выделенную область в виртуальной памяти и содержит как минимум один поток. Потоки планируются к выполнению операционной системой. У любого потока имеется приоритет, счетчик команд, указывающий на место в программе, где происходит обработка, и стек, в котором сохраняются локальные переменные потока. Стек у каждого потока выглядит по-своему, но память для программного кода и куча разделяются среди всех потоков, которые функционируют внутри одного процесса.

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

Основы многопоточной обработки

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

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

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

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

Главное преимущество многопоточной обработки заключается в том, что она позволяет писать программы, которые работают очень эффективно благодаря возможности выгодно использовать время простоя, неизбежно возникающее в ходе выполнения большинства программ. Как известно, большинство устройств ввода-вывода, будь то устройства, подключенные к сетевым портам, накопители на дисках или клавиатура, работают намного медленнее, чем центральный процессор (ЦП). Поэтому большую часть своего времени программе приходится ожидать отправки данных на устройство ввода-вывода или приема информации из него. А благодаря многопоточной обработке программа может решать какую-нибудь другую задачу во время вынужденного простоя.

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

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

В среде .NET Framework определены две разновидности потоков: приоритетный и фоновый. По умолчанию создаваемый поток автоматически становится приоритетным, но его можно сделать фоновым. Единственное отличие приоритетных потоков от фоновых заключается в том, что фоновый поток автоматически завершается, если в его процессе остановлены все приоритетные потоки.

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

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

В языке C# и среде .NET Framework поддерживаются обе разновидности многозадачности: на основе процессов и на основе потоков. Поэтому средствами C# можно создавать как процессы, так и потоки, а также управлять и теми и другими. Для того чтобы начать новый процесс, от программирующего требуется совсем немного усилий, поскольку каждый предыдущий процесс совершенно обособлен от последующего.

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

Классы, поддерживающие многопоточное программирование, определены в пространстве имен System.Threading. Поэтому любая многопоточная программа на C# включает в себя следующую строку кода:

using System.Threading;

Пространство имен System.Threading содержит различные типы, позволяющие создавать многопоточные приложения. Пожалуй, главным среди них является класс Thread, поскольку он представляет отдельный поток. Чтобы программно получить ссылку на поток, выполняемый конкретным его экземпляром, просто вызовите статическое свойство Thread.CurrentThread:

static void ExtractExecutingThread()
{
   // Получить поток, выполняющий данный метод.
   Thread currThread = Thread.CurrentThread;
}

На платформе .NET не существует прямого соответствия "один к одному" между доменами приложений (AppDomain) и потоками. Фактически определенный AppDomain может иметь несколько потоков, выполняющихся в каждый конкретный момент времени. Более того, конкретный поток не привязан к одному домену приложений на протяжении своего времени существования. Потоки могут пересекать границы доменов приложений, когда это вздумается планировщику Windows и CLR.

Хотя активные потоки могут пересекать границы AppDomain, каждый поток в каждый конкретный момент времени может выполняться только внутри одного домена приложений (другими словами, невозможно, чтобы один поток работал в более чем одном домене приложений сразу). Чтобы программно получить доступ к AppDomain, в котором работает текущий поток, вызовите статический метод Thread.GetDomain():

static void ExtractAppDomainHostingThread()
{
   // Получить AppDomain, в котором работает текущий поток.
   AppDomain ad = Thread.GetDomain();
}

Единственный поток также в любой момент может быть перемещен в определенный контекст, и он может перемещаться в пределах нового контекста по прихоти CLR. Для получения текущего контекста, в котором выполняется поток, используйте статическое свойство Thread.CurrentContext (которое возвращает объект System.Runtime.Remoting.Contexts.Context):

static void ExtractCurrentThreadContext()
{
   // Получить контекст, в котором работает текущий поток.
   Context ctx = Thread.CurrentContext;
}

Еще раз: за перемещение потоков между доменами приложений и контекстами отвечает CLR. Как разработчик .NET, вы всегда остаетесь в счастливом неведении относительно того, когда завершается каждый конкретный поток (или куда именно он будет помещен после перемещения). Тем не менее, полезно знать о различных способах получения лежащих в основе примитивов.

Пройди тесты
Лучший чат для C# программистов