Потоки и синхронизация: часть 1
ОГЛАВЛЕНИЕ
Статья рассматривает потоки и синхронизацию потоков с позиции Microsoft .NET
Введение
Современная многопроцессорная операционная система выполняет несколько операций одновременно, даже если в системе есть только один физический процессор. Теоретически это кажется невозможным, но рассмотрим, как достигается параллельная обработка с помощью одного процессора.
Есть много этапов в ходе выполнения процесса, когда процессор бездействует и ждет наступления некоторого события. Например, если ваше приложение выполняет какую-то операцию ввода-вывода, то во время операции ввода-вывода процессор бездействует и ждет завершения операции ввода-вывода. Аналогично, если ваше приложение выполняет запрос к базе данных, процессор бездействует и ждет, когда сервер баз данных ответит на запрос. По сути, в ходе выполнения процесса есть много этапов, когда процессор бездействует. Операционные системы используют эту возможность и используют бездействующий процессор для выполнения другого процесса. То есть, операционная система делит время процессора между процессами, чтобы достичь параллельной обработки.
В ранние времена параллельной обработки операционная система полагалась на то, что каждый процесс регулярно уступал процессор другим процессам в системе. При таком подходе плохо спроектированное или зависшее приложение могло остановить всю систему. Данный подход называется совместная многозадачность, когда каждый процесс должен взаимодействовать с операционной системой, чтобы добиться параллельной обработки, регулярно уступая процессор.
Современная операционная система придерживается подхода вытесняющей многозадачности, когда операционные системы имеют встроенные условия для переключения процессора с одного процесса на другой и не зависят от освобождения процессора основными процессами. Действие отнятия контроля над процессором у одного процесса и передачи его другому процессу называется "вытеснение". Данный подход позволяет другим процессам продолжать выполняться, даже если один из процессов завис. Переключение процессоров прозрачно для процессов, поэтому разработчикам не надо выполнять специальных действий для поддержки многопроцессорных операционных систем. Условиями для переключения процессора с одного процесса на другой могут быть:
• Истекшее время: Операционная система может делить фиксированное время процессора между процессами
• Приоритет: Процесс с более высоким приоритетом ожидает процессора
• Ожидание наступления события: Текущий процесс ожидает ввода-вывода, и процессор бездействует
• Сочетание вышеназванного
Структура данных, используемая операционной системой для планирования и разделения времени процессора между процессами, называется "поток". Каждый процесс имеет минимум один поток.
Пока было рассмотрено выполнение нескольких процессов. Современные технологии, среды разработки и языки позволяют одновременно выполнять несколько задач внутри одного процесса, например, одновременно рисовать график на экране и вычислять простое число с помощью одного и того же процесса. То есть, один процесс может иметь более одного потока операционной системы, чтобы выполнять несколько задач одновременно.
Следующий пример показывает одновременное выполнение нескольких задач путем создания нового потока. Вновь созданный поток используется для выполнения функции ConcurrentTask().Окно вывода показывает результаты, порожденные главным потоком и вновь созданным потоком. Замечание: Всякий раз при выполнении следующей программы вы будете получать разный вывод, так как нельзя предсказать, когда операционная система переключит процессор между потоками.
class Program
{
static void Main(string[] args)
{
ThreadsExperiment.Program experiment;
experiment = new Program();
System.Threading.Thread newThread =
new System.Threading.Thread(
new System.Threading.ThreadStart(experiment.ConcurrentTask));
newThread.Start();
for (int iLoop = 0; iLoop <= 100; iLoop++)
{
Console.WriteLine("Главная задача " + iLoop.ToString());
}
newThread.Join();
Console.ReadLine();
}
public void ConcurrentTask()
{
for (int iLoop = 0; iLoop <= 100; iLoop++)
{
Console.WriteLine("Параллельная задача " + iLoop.ToString());
}
}
}
Исходя из примера выше, многопоточность/многозадачность кажется простой и интересной темой. Исходя из опыта, рекомендуется использовать как можно меньше потоков, потому что управлять выполнением нескольких потоков в приложении непросто. Это особенно верно, если потоки обращаются к общим ресурсам. В данной статье разобраны проблемы синхронизации в многопоточности и разные способы и приемы, доступные в Microsoft .NET.
Многопоточность в .NET
Каркас Microsoft .NET предоставляет разные способы достижения многопоточности. Простейший подход – использовать класс Thread, определенный в пространстве имен System.Threading. Хотя нет прямого соответствия между потоком операционной системы и классом управляемого потока, для простоты класс Thread можно считать управляемой оберткой вокруг потока операционной системы. Каждое управляемое приложение может иметь стандартный поток, предоставляемый CLR(общеязыковая среда исполнения).
Вы можете создать свой собственный поток путем передачи делегата типа System.Threading.ThreadStart. Этот делегат ссылается на метод, который будет выполняться асинхронно вновь созданным потоком. .NET 2.0 вводит новый конструктор класса Thread, принимающий делегат типа System.Threading.ParameterizedThreadStart в качестве параметра. Этот делегат, в свою очередь, принимает System.Object в качестве параметра. Итак, с .NET 2.0 и далее, вы можете отправить любой объект или даже коллекцию объектов асинхронной операции, как определено в следующем примере.
class Program
{
static void Main(string[] args)
{
ThreadsExperiment.Program experiment;
System.Threading.Thread thread;
int num1;
int num2;
System.Threading.Thread.CurrentThread.Name = "Default Thead";
experiment = new Program();
Console.Write ("Введите 1е число: ");
num1 = int.Parse(Console.ReadLine());
Console.Write("Введите 2nd число: ");
num2 = int.Parse(Console.ReadLine());
thread = new System.Threading.Thread(new
System.Threading.ParameterizedThreadStart(
experiment.CalculateFactorial));
thread.Name = "New Thread";
thread.Start(num1);
experiment.CalculateFactorial(num2);
thread.Join();
Console.ReadLine();
}
public void CalculateFactorial(object obj)
{
int factorial;
int num = (int)obj;
factorial = 1;
for (int iloop = num; iloop >= 1; iloop--)
{
factorial = factorial * iloop;
}
Console.WriteLine(System.Threading.Thread.CurrentThread.Name +
" вычисляется факториал " +
num.ToString() + " как " + factorial.ToString());
}
}
В примере выше асинхронно вычисляются факториалы двух чисел. Оператор thread.Join() используется для проверки того, что вновь созданный поток закончил обработку перед завершением приложения. В каркасе .NET управляемый поток может быть приоритетным или фоновым потоком. CLR предполагает, что управляемое приложение работает, если минимум один приоритетный поток работает. Если в приложении нет ни одного работающего приоритетного потока, CLR завершит работу приложения. По умолчанию, потоки, создаваемые классом Thread, являются приоритетными. Но можно сделать их фоновыми с помощью свойства IsBackground.
Пул потоков .NET
Чтобы дополнительно упростить модель потоков, Microsoft предоставляет встроенный пул потоков в .NET. Создание и уничтожение потоков тратит ресурсы и время. В ходе создания потока объект потока ядра создается и инициализируется. Затем выделяется стек потока, и извещения отправляются всем DLL(динамическая библиотека). Аналогично, когда поток уничтожается, объект ядра освобождается, память стека освобождается, и извещения отправляются DLL.
Для повышения производительности CLR предоставляет пул потоков. Когда приложению надо выполнить задачу асинхронно, он просит пул потоков выполнить эту задачу. Пул потоков выполняет задачу, используя один из своих потоков. Когда задача выполнена, поток возвращается обратно пулу. Для каждого асинхронного запроса не создается новый поток. Если приложение запрашивает больше потоков, чем имеется в пуле, пул потоков помещает запрос в очередь с ожиданием или создает дополнительные потоки. Если в пуле потоков есть много неработающих потоков, пул потоков уничтожает их для освобождения ресурсов системы. Следующий код использует тот же пример вычисления факториала двух чисел асинхронно с помощью пула потоков.
class Program
{
static void Main(string[] args)
{
ThreadsExperiment.Program experiment;
int num1;
int num2;
experiment = new Program();
Console.Write ("Please enter 1st number: ");
num1 = int.Parse(Console.ReadLine());
Console.Write("Please enter 2nd number: ");
num2 = int.Parse(Console.ReadLine());
System.Threading.ThreadPool.QueueUserWorkItem(new
System.Threading.WaitCallback(
experiment.CalculateFactorial), num1);
experiment.CalculateFactorial(num2);
System.Threading.Thread.Sleep(1000);
Console.ReadLine();
}
public void CalculateFactorial(object obj)
{
int factorial;
int num = (int)obj;
factorial = 1;
for (int iloop = num; iloop >= 1; iloop--)
{
factorial = factorial * iloop;
}
Console.WriteLine(System.Threading.Thread.CurrentThread.Name +
" calculates the factrorial of " +
num.ToString() + " as " + factorial.ToString());
}
}
Так как все потоки пула потоков являются фоновыми, главный приоритетный поток ждет, когда фоновые потоки завершат выполнение своих задач. Замечание: Вы можете получить другой результат из нижеприведенного.
Фоновый работник
Microsoft .NET также предоставляет компонент пользовательского интерфейса System.ComponentModel.BackgroundWorker для реализации многопоточности. Этот компонент внутри использует пул потоков для асинхронного выполнения задач. Этот класс возбуждает события, чтобы известить хост о состоянии асинхронной операции. Данный класс делает ваш пользовательский интерфейс отзывчивым, даже если вы выполняете длинную операцию.
Таймер
Для асинхронного выполнения задачи на основе интервалов Microsoft .NET предоставляет класс Timer. Этот класс внутри использует пул потоков для асинхронного выполнения задач.