Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Медведев_С++_CLI_C#_Java_J#.doc
Скачиваний:
3
Добавлен:
01.07.2025
Размер:
5 Мб
Скачать
☆

9. Потоки и синхронизация их выполнения

9.1. Процессы

Процесс process) - это программа (EXE файл), представленная в опе-

рационной системе как задача {task) и управляемая супервизором задач. Для

выполнения операционная система выделяет процессу необходимые ресурсы

и, в частности, область памяти, называемую адресным пространством про-

цесса. По мере надобности процесс может загружать в своё адресное про-

странство динамически компонуемые библиотеки (DLL — Dynamic Link Library)

или выделять в нём память для своих объектов или данных, которые

при необходимости могут быть удалены или заменены новыми.

Процесс может создать другие процессы, используя для управления

ими необходимые ЛР/-функции (API - Application Programming Interface).

Будучи созданными, процессы становятся независимыми и выполняются

параллельно с другими процессами.

Обычно каждый процесс в операционной системе представлен своим

прикладным окном (application window). Наличие окна с интерфейсными

элементами позволяет не только управлять программой, но и завершить про-

цесс, нажав на кнопку закрытия в его прикладном окне. Выполнение процес-

са состоит в выполнении составляющих его потоков.

Для обеспечения безопасности программного кода .NET технология в

рамках одного процесса создаёт один (по умолчанию) или произвольное ко-

личество (по желанию программиста) так называемых доменов, в которых

выполняется управляемый код.

9.2. Потоки в C# и C++/CLI

Под потоком (thread) понимают выполняющийся программный код.

Выполнение процесса начинается с выполнения основного потока

primary thread), представленного главной функцией main() или Main(). Ос-

новной поток обычно реализует диалог с пользователем. Основной поток

инициирует, выполняет и завершает работу процесса.

Если основной поток создаёт окно, то его называют основным оконным

потоком. Обычно с окном связан цикл обработки сообщений операционной

системы.

Основной поток может создать и запустить так называемые рабочие

потоки (work threads), которые будут выполняться параллельно с основным

и другими потоками.

В отличие от процесса потокам не предоставляются ресурсы. Поэтому

каждый из запущенных потоков будет выполняться в одном и том же адрес-

ном пространстве, выделенном процессу, к которому эти потоки принадле-

жат. Потоки создаются, запускаются, приостанавливаются и возобновляются

путём вызова API-функций. После завершения процесса все принадлежав-

шие ему потоки уничтожаются.

Если выполнение основного потока сводится к выполнению главной

функции Main() или main() приложения, то выполнение каждого рабочего по-

тока связано с выполнением так называемой его потоковой функции. Завер-

шение выполнения потоковой функции завершает выполнение потока. В при-

мерах программ данной книги потоковая функция содержит цикл while, оп-

ределяющий время жизни потока. В теле оператора while вызывается функ-

ция Sleep() или sleep(), приостанавливающая выполнение данного потока на

заданный промежуток времени, в течение которого выполняются другие по-

токи (ведь, процессор, выполняющий потоки, один!). Перед запуском неко-

торого потока необходимо установить значение булевского выражения его

оператора while, равным true. Для завершения выполнения потока достаточ-

но сделать это выражение, равным false.

В языках Java и C# потоки создаются и применяются по разному. В ка-

ждом из этих языков рабочий поток является объектом класса Thread. Но ес-

ли в языке C#, использующем библиотеку .NET Framework, объект потока

можно создать везде, где он, действительно, нужен, и можно создать просто

в одном потоке несколько "дочерних" потоков, то в языке Java, в виду ис-

ключительности потока, наложены ограничения, которые приходится учиты-

вать. В языке Java имя потоковой функции для всех рабочих потоков одно и

то же - run(). В классе может быть создан поток, если класс порождён (extends)

из потокового класса Thread, либо наследует (implements) интерфейс

Runnable. Потоки языка Java будут рассмотрены в разделе 9.3.

Для реализации потоков на языке C# библиотека .NET Framework со-

держит более 20 классов и структур, среди которых следует, прежде всего,

выделить потоковый класс Thread и делегат ThreadStart потока.

Класс Thread используется для создания и управления потоками, а тип

ThreadStart - для создания объекта делегата потока, инкапсулирующего по-

токовую функцию.

Среди множества функций и свойств класса Thread мы выделим функ-

цию Start() запуска потока (запуска потоковой функции потока), функцию

Suspend() приостановки потока, функцию Resume() возобновления выполне-

ния потока и функцию Join() ожидания завершения указанного потока.

При создании экземпляра потока конструктору класса Thread передаёт-

ся в качестве аргумента объект делегата, инкапсулирующий потоковую

функцию. При этом возможны два варианта перегруженного конструктора

делегата: 1) указывается статическая потоковая функция некоторого класса

или 2) указывается потоковая функция конкретного объекта определённого

класса.

При объектно-ориентированном программировании целесообразно свя-

зать с потоком ряд данных некоторого класса, то есть, по сути дела, привя-

зать поток к конкретному классу, данные которого будут использоваться и

обрабатываться потоком. Будем такой класс называть классом потокового

объекта. Создав из класса потокового объекта множество потоковых объек-

тов и запустив их потоковые функции, мы получим параллельно функцио-

нирующие потоковые объекты. Они могут представлять, например, сово-

купность самолётов или людей. Каждая из этих совокупностей характеризу-

ется набором своих данных, определяющих состояние её объектов, и набо-

ром функций, определяющих поведение объектов.

Целесообразно включить в потоковый объект булевскую переменную

(например, bool life), которая будет определять жизненный цикл потока и

которой перед запуском этого потока присваивается истинное значение

(true). Жизненный цикл реализуется в потоковой функции с помощью опера-

тора while, условие которого содержит эту переменную. Поток выполняется

при значении life, равным true, но достаточно переменной life присвоить зна-

чение false, и потоковая функция завершит своё выполнение, закончив тем

самым и функционирование потока и потокового объекта.

Для координации параллельного совместного выполнения нескольких

потоков используются объекты синхронизации - мютексы (mutexes), мони-

торы (monitors) и события (events). Они позволяют выполняться в данный

момент ограниченному числу потоков, связанных с некоторым объектом-

разделяемым ресурсом, и приостанавливать потоки до свершения определен-

ных действий. Чтобы овладеть разделяемым ресурсом, потоки выстраивают-

ся в очередь.

Для отладки класса потоковых объектов в этот класс включим пере-

менную num - номер экземпляра конкретного объекта, а в цикл while пото-

ковой функции включим переменную n, определяющую номер цикла. Зна-

чения num и n, выданные на консоль, информируют о выполняющихся пото-

ковых объектах.

В нижеследующих примерах пояснено создание и использование пото-

ковых объектов. Особое внимание уделяется использованию событий пото-

ковыми объектами как для информирования об их выполнении, так и для

связи разных функционирующих потоковых объектов с помощью событий.

В примере 9.2.1 рассматривается параллельное выполнение двух пото-

ковых объектов.

Пример 9.2.1. Два параллельно выполняющихся потоковых объекта на

языке C#.

////////////////////

/ / C #

using System;

using System.Threading;

namespace CsTl_2

{

class CThread // Класс потокового объекта

int num; || Номер потокового объекта

Thread thread; // Ссылка на поток

bool run; // Признак выполнения потока

bool llfe; // Признак жизни потока

public CThread (lnt Num ) {num= Num; llfe= false; run= false;>

~CThread ( ) {llfe= false;>

public void Start ( ) // Стартовать потоковый объект

{

if (!life) // Если не жил, то

{

run= true; // пусть выполняется и

life= true; // живёт

Н Создать поток

thread= new Thread (new ThreadStart (ThreadFunc));

thread.Start ( ); || Запустить поток >

>

public vold Finish ( ) {life= false;}

public void Suspend ( ) // Приостановить потоковый объект

{

if (run) // Если выполнялся, то

{

run= false; // пусть не выполняется

thread.Suspend ( ); // Приостановить поток

>

>

public void Resume ( ) // Возобновить потоковый объект

{

if (irun) // Если не выполнялся, то

{

run= true; // пусть выполняется

thread.Resume ( ); // Возобновить поток >

>

private void ThreadFunc ( ) // Выполнить поток

{

int n= 0; // Номер цикла потока

while (llfe) // Пока живём, выполнять:

{

Console.WriteLine ("thread " + num + " n= " + n);

n++;

Thread.Sieep (50);

}

Console.WriteLine ("Thread " + num + " flnished");

class TestThread

{

static void Mairi ()

{

CThread threadl= new CThread (1); // Создать первый объект

CThread thread2= new CThread (2); // Создать второй объект

threadl.Start ( ); // Стартовать первый потоковый объект

thread2.Start (); // Стартовать второй потоковый объект

Thread.Sleep (100); // Поспать 100 мс

Console,WrlteLlne (" Приостановить поток первого объекта");

threadl.Suspend (); // Приостановить первый потоковый объект

Thread.Sleep (100); // Поспать 100 мс

Console.WriteLine (" Возобновить поток первого объекта");

threadl.Resume (); // Возобновить первый потоковый объект

Thread.Sleep (100); // Поспать 100 мс

Console.WriteLine (" Завершить потоки объектов");

threadl.Flnlsh (); // Завершить первый потоковый объект

thread2.Flnish ( ); // Завершить второй потоковый объект

>

>

////////////////////

// C++/CLI

#include "stdafx.h"

using namespace System;

using namespace System::Threading;

ref class CThread // Управляемый класс потокового объекта

{

int num;

Thread ^thread;

bool run;

bool life;

public:

CThread (int Num ) {num= Num; life= false; run= false;>

// Номер потокового объекта

// Дескриптор на поток

// Признак выполнения потока

// Признак жизни потока

-CThread ( ) <life= false;}

vold Start ( )

{

if(!life)

{

// Стартовать потоковый объект

// Если не живёт, то

run= true; // пусть живёт и

life= true; // выполняется

// Создать поток

thread= gcnew Thread (gcnew ThreadStart (this,

&CThread::ThreadFunc));

thread ->Start ( ); // Стартовать поток

void Finish ( ) <life= false; thread -> Join ( );}

vold Suspend ( ) // Приостановить потоковый объект

{

If (run) // Если выполняется, то

{

run= false; // не выполнять

thread -> Suspend ( ); // Приостановить поток

>

>

vold Resurne ( ) // Возобновить потоковый объект

{

if (!run) // Если не выполнялся, то

{

run= true; // выполнять

thread ->Resume ( ); // Возобновить поток

>

>

void ThreadFunc ( ) // Выполнить поток

{

int n= 0; // Номер цикла

while (llfe) // Если живём, то выполнять:

{

n++;

Console::Wrlte ("thread {0}", num.ToString ());

Console::WriteUne (" n= {0}", n.ToString ());

Thread::Sleep (50); >

Console::Write ('Thread {0>", num.ToString ());

Console::WrlteLine (" finished"); >

>;

void ma!n ()

{

CThread ^threadl= gcnew CThread (1); // Создать первый объект

CThread ^thread2= gcnew CThread (2); // Создать второй объект

threadl -> Start (); // Стартовать первый потоковый объект

thread2 -> Start ( ); // Стартовать второй потоковый объект

Thread::Sleep (100); // Поспать 100 мс

Console::WriteLine (" Приостановить поток первого объекта");

threadl -> Suspend (); // Приостановить первый потоковый объект

Thread::Sleep (100); // Поспать 100 мс

Console::WriteLine (" Возобновить поток первого объекта");

threadl -> Resume (); // Возобновить первый потоковый объект

Thread::Sleep (100); // Поспать 100 мс

Console::WrlteUne (" Завершить потоки объектов");

threadl -> Finish (); // Завершить первый потоковый объект

thread2 -> Finish (); // Завершить второй потоковый объект

/>*

Result:

thread 1 n= 1

thread 2 n= 1

thread 1 n= 2

thread 2 n= 2

Приостановить поток первого объекта

thread 2 n= 3

thread 2 n= 4

Возобновить поток первого объекта

thread 1 n= 3

thread 2 n= 5

thread 1 n= 4

tnread 2 n= 6

Завершить потоки объектов

Thread 1 finished

Thread 2 finished

*/

C#. Класс CThread потокового объекта содержит данные: num - номер экзем-

пляра потокового объекта, life - признак жизни потока, run - признак выпол-

нения потока и thread - ссылка на поток.

Поток thread является объектом класса Thread библиотеки .NET

Framework и создаётся с помощью оператора new в теле функции StartQ

класса CThread потокового объекта с обязательным предварительным при-

сваиванием переменной life значения true. Аргументом конструктора Thread

является объект потокового делегата ThreadStart, создаваемый также с помо-

щью оператора new. Конструктор делегата содержит в качестве аргумента

имя потоковой функции ThreadFunc(). Созданный поток thread запускается

посредством функции StartQ класса Thread.

Интерфейсные функции Start(), Suspend(), Resume() и Finish() потоко-

вого объекта обеспечивают его запуск, приостановку и возобновление функ-

ционирования, и завершение функционирования. Объявленная в классе пе-

ременная run используется функциями Suspend() и Resume() для переключе-

ния потока из приоетановленного состояния в состоянии выполнения, и на-

оборот.

Потоковая функция ThreadFunc(), выполняясь пока значение life равно

true, после каждого цикла while выдаёт на консоль строку вида:

thread номер-потокового-объекта n= номер-цикла

Столбик таких строк на консоли свидетельствует о выполнении циклов пото-

ковой функции потокового объекта.

Главная функция Main() приложения создаёт потоковые объекты

threadl и thread2 класса CThread. Конструктор класса CThread только создаёт

поток, связанный с этим объектом, и лишь после применения функции Start()

класса CThread к объектам threadl и thread2 эти объекты начинают функцио-

нировать. Потоки выполняются параллельно, о чём свидетельствует инфор-

мация, выданная на консоль: строки, выданные потоковыми функциями раз-

ных объектов, чередуются. Поспав 100 мс (причиной чего является вызов

функции Sleep()), основной поток, представленный главной функцией Main(),

позволяет этим объектам функционировать самостоятельно и выдать резуль-

тативную информацию на консоль. Затем к объекту threadl применяется

функция Suspend(), которая приостанавливает его функционирование. По-

скольку функция Sleep() опять задерживает выполнение основного потока, то

теперь выполняется только поток объекта thread2. О чём свидетельствуют

две выданные подряд строки объекта thread2 и отсутствие строк от объекта

threadl. Через 100 мс функция Resume() возобновляет приостановленное вы-

полнение объекта threadl. Теперь параллельно выполняются потоки обоих

объектов. А через 100 мс функция Finish(), применённая к объектам, завер-

шает их функционирование. Функция Finish() присваивает переменной life

значение false - цикл потоковых функций прекращается.

C++/CLI. Программа на языке C++/CLI очень схожа с программой на C#.

Прежде всего бросается в глаза присутствие ref в описании класса CThread.

Это связано с тем, что класс CThread должен бьггь управляемым, поскольку

он использует управляемый класс Thread и управляемый делегат ThreadStart

библиотеки .NET Framework.

Вместо ссылочной переменной в классе CThread объявлен дескриптор

thread на объект управляемого класса Thread. При ссылке на функции Start(),

Suspend(), Resume() класса Thread в функциях Start(), Suspend(), Resume()

нашего класса CThread использован этот дескриптор со стрелкой.

Также отличается получение объекта делегата в аргументе конструкто-

ра класса Thread при создании объекта потока.

Как видим, создание объектов с потоками не представляет сложности в

языках C# и C++/CLI. Использование потокового делегата существенно уп-

рощает создание в объекте множества потоков с разноимёнными потоковыми

функциями.

Заметим, что применение функций Suspend() и Resume() класса Thread

не рекомендуется при разработке многопоточных сложных программ, по-

скольку может привести к непредвидимым аварийным ситуациям при парал-

лельном выполнении потоков. В случае нашей простой программы использо-

вание этих функций допустимо. Как избежать применения функций

Suspend() и Resume() будет изложено в разделах книги, посвященных син-

хронизации параллельного выполнения потоков и в конце книги при рас-

смотрении разработки многопоточной программы.