Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
БП_лаб раб 6.doc
Скачиваний:
7
Добавлен:
25.11.2019
Размер:
374.27 Кб
Скачать

Лабораторная работа № 6

Создание и защита данных в многопоточных приложениях

Цель работы: изучение пространства имен System.Threading

Теоретические сведения

Начиная с версии 4.0 в среде .NET Framework доступны 2 способа реализации многопоточности: с использованием потоков (Threads) и использованием задач (Tasks).

1. Потоки

1.1 Жизненный цикл потока

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

Процесс может содержать столько потоков сколько необходимо. Программа на C# запускается как единственный поток, обычно запускающий метод Main(), автоматически создаваемый CLR и операционной системой (“главный” поток), и становится многопоточной при помощи создания дополнительных потоков. При создании нового потока требуется указать последовательность действий, обычно метод, в котором должно начаться выполнение.

Работа с потоками осуществляется с помощью класса Thread, который расположен в пространстве имен System.Threading. Экземпляр Thread представляет собой один поток – одну последовательность исполнения.

В любой момент времени поток может находиться в одном из нескольких состояний (рисунок 1).

Рисунок 1. Жизненный цикл потока

Новый поток начинает жизненный цикл в состоянии Unstarted (не начатый). Поток будет находиться в этом состоянии до тех пор, пока программа не вызовет метод Start() для соответствующего экземпляра класса Thread, перемещающий поток в состояние Started (начатый) и сразу возвращает управление вызывающему потоку. После этого поток, вызвавший метод Start(), вновь начатый поток (Started) и все другие потоки в программе выполняются одновременно.

Поток Started входит в состояние Running (т.е. начинает выполняться), когда ОС выделит потоку процессорное время. Когда поток Started впервые получает процессорное время и становится потоком Running, он исполняет свой делегат ThreadStart, задающий операции, которые данный поток будет выполнять в течении всего жизненного цикла. Делегат указывается в качестве параметра конструктору Thread при создании потока. Делегат ThreadStart должен быть методом, возвращающим void и не принимающим никаких аргументов.

Поток Running входит в состояние Stopped (остановленный) при прекращении исполнения делегата ThreadStart. Также программа может насильно перевести поток в состояние Stopped, вызвав метод Abort() для соответствующего экземпляра класса Thread. Когда поток находится в состоянии Stopped, и ссылки на объект потока нет, тогда «сборщик мусора» может удалить объект потока из памяти.

Поток входит в состояние Blocked (заблокирован), когда поток выдает запрос на ввод/вывод. ОС блокирует исполнение потока до тех пор, пока не завершится операция ввода/вывода, которую ожидает поток. По завершению операции поток возвращается в состояние Started и может возобновить выполнение. Поток в состоянии Blocked не может использовать процессорное время, даже при его наличии.

Существует 3 пути, по которым поток Running переходит в состояние WaitSleepJoin (ожидание-объединение). Если поток сталкивается с кодом, который он пока не может выполнить (обычно из-за нарушения условия), поток может вызвать метод Wait() класса Monitor для вхождения в состояние WaitSleepJoin. Поток возвращается в состояние Started, когда другой поток вызывает метод Pulse() или PulseAll() класса Monitor. Метод Pulse() перемещает очередной ожидающий поток назад в состояние Started. Метод PulseAll() перемещает все ожидающие потоки назад в состояние Started.

Поток Running может вызвать метод Sleep() класса Thread для вхождения в состояние WaitSleepJoin на период, измеряемый миллисекундами, указанный как параметр метода Sleep(). Спящий поток возвращается в состояние Started по истечению заданного для него времени ожидания. Потоки в режиме ожидания не могут использовать процессорное время, даже при его наличии.

Любой поток, входящий в состояние WaitSleepJoin при вызове метода Wait() класса Monitor, или при вызове метода Sleep() класса Thread, также возвращается в состояние Started, если ожидающий метод Interrupt() экземпляра класса Thread вызывается другим потоком в программе.

Если поток не может продолжать исполнение до прекращения исполнения другого потока (данный феномен называется зависимым потоком), тогда зависимый поток вызывает метод Join() экземпляра класса Thread другого потока для объединения двух потоков. В этом случае, зависимый поток выходит из состояния WaitSleepJoin, когда другой поток завершает исполнение, т.е. переходит в состояние Stopped.

При вызове метода Suspend() экземпляра класса Thread, поток Running входит в состояние Suspended (приостановленный). Он возвращается в состояние Started, когда другой поток в программе вызывает метод Resume() экземпляра класса Thread.

1.2 Создание и запуск потока

Класс Thread обладает следующими основными свойствами:

  • IsAlive – получает значение, определяющее статус исполнения для данного потока.

  • IsBackground – получает или устанавливает флаг, определяющий является ли поток фоновым или нет.

  • ManagedThreadId – получает уникальный идентификатор для потока, управляемого в данный момент.

  • Name – получает или устанавливает имя потока.

  • Priority – получает или устанавливает значение, определяющее планируемый приоритет потока.

А также содержит следующие основные методы:

  • Start() – меняет состояние потока на Running;

  • Start(Object) – меняет состояние потока на Running и определяет параметры для метода, запускаемого потоком;

  • Abort() – генерирует исключение ThreadAbortException в потоке, чтобы начать процесс завершения процесса. Обычно завершает процесс;

  • ResetAbort – отменяет процесс завершения процесса;

  • Sleep(Int32) – приостанавливает текущий поток на заданное количество миллисекунд. Если указанное количество миллисекунд простоя равно нулю, то вызывающий поток приостанавливается лишь для того, чтобы предоставить возможность для выполнения потока, ожидающего своей очереди.

  • Sleep(TimeSpan) – приостанавливает текущий поток на определенное время;

  • Interrupt() – прерывает состояние WaitSleepJoin потока;

  • Join() – блокирует вызывающий поток до завершения вызванного потока;

  • Join(Int32) – блокирует вызывающий поток до завершения вызванного потока или до истечения заданного количества миллисекунд;

  • Join(TimeSpan) – блокирует вызывающий поток до завершения вызванного потока или до истечения заданного интервала времени;

  • Suspend – приостанавливает выполнение потока;

  • Resume – возобновляет выполнение приостановленного потока;

Для создания потока достаточно создать экземпляр класса Thread. Простейшая форма конструктора выглядит следующим образом:

public Thread(ThreadStart запуск),

где запуск — это имя метода, выполняемого созданным потоком;

ThreadStart — делегат, определенный в среде .NET Framework, как показано ниже.

public delegate void ThreadStart()

Т.о. метод, указываемый в качестве точки входа в поток, должен иметь возвращаемый тип void и не принимать никаких аргументов.

Вновь созданный новый поток не начнет выполняться до тех пор, пока не будет вызван его метод Start().

Однажды начавшись, поток будет выполняться до тех пор, пока не произойдет возврат из метода, на который указывает запуск. Таким образом, после возврата из этого метода поток автоматически прекращается. Если же попытаться вызвать метод Start() для потока, который уже начался, это приведет к генерированию исключения ThreadStateException. В приведенном ниже примере программы создается и начинает выполняться новый поток.

using System;

using System.Threading;

//Создать поток исполнения

class MyThread

{

public int Count;

string thrdName;

public MyThread(string name)

{

Count = 0;

thrdName = name;

}

// Точка входа в поток

public void Run()

{

Console.WriteLine(thrdName + " начат.");

do

{

Thread.Sleep(500);

Console.WriteLine("В потоке " + thrdName + ", Count = " + Count);

Count++;

}

while(Count < 10);

Console.WriteLine(thrdName + " завершен.");

}

}

class MultiThread

{

static void Main()

{

Console.WriteLine("Основной поток начат.");

//Создание объекта типа MyThread

MyThread mt = new MyThread("Потомок #1");

//Далее создать поток из этого объекта

Thread newThrd = new Thread(mt.Run);

//И, наконец, начать выполнение потока

newThrd.Start();

do

{

Console.Write(".");

Thread.Sleep(100);

}

while (mt.Count != 10);

Console.WriteLine("Основной поток завершен.");

}

}

Рассмотрим приведенную выше программу более подробно. В самом ее начале определяется класс MyThread, предназначенный для создания второго потока исполнения. В методе Run() этого класса организуется цикл для подсчета от 0 до 9.

В методе Main() новый объект типа Thread создается с помощью приведенной последовательности операторов. Благодаря этому метод mt.Run() выполняется в своем собственном потоке. После вызова метода Start() выполнение основного потока возвращается к методу Main(), где начинается цикл do-while. Оба потока продолжают выполняться, совместно используя процессорное время, вплоть до окончания цикла.

Зачастую в многопоточной программе требуется, чтобы основной поток был последним потоком, завершающим ее выполнение. Формально программа продолжает выполняться до тех пор, пока не завершатся все ее приоритетные потоки. Поэтому требовать, чтобы основной поток завершал выполнение программы, совсем не обязательно. Тем не менее, этого правила принято придерживаться в многопоточном программировании, поскольку оно явно определяет конечную точку программы. В рассмотренной выше программе предпринята попытка сделать основной поток завершающим ее выполнение. Для этой цели значение переменной Count проверяется в цикле do-while внутри метода Main (), и как только это значение оказывается равным 10, цикл завершается и происходит поочередный возврат из методов Sleep ().

Далее будут представлены более совершенные способы организации ожидания одного потока завершения другого. Внесем изменения в предыдущий пример. Во-первых, можно сделать так, чтобы выполнение потока начиналось сразу же после его создания. Для этого достаточно получить экземпляр объекта типа Thread в конструкторе класса MyThread. И, во-вторых, в классе MyThread совсем не обязательно хранить имя потока, поскольку для этой цели в классе Thread определено свойство Name. В-третьих, вместо того, чтобы отслеживать значение переменной Count для определения завершения потока, можно использовать доступное только для чтения свойство IsAlive. Оно возвращает логическое значение true, если поток, для которого оно вызывается, по-прежнему выполняется.

Кроме этого в программе можно породить столько потоков, сколько потребуется, например, три.

В результате предыдущий пример примет следующий вид:

class MyThread

{

public int Count;

public Thread Thrd;

public MyThread(string name)

{

Count = 0;

Thrd = new Thread(this.Run);

Thrd.Name = name; //задать имя потока

Thrd.Start(); //начать поток

}

// Точка входа в поток

void Run()

{

Console.WriteLine(Thrd.Name + " начат.");

do

{

Thread.Sleep (500);

Console.WriteLine("В потоке " + Thrd.Name + ", Count = " + Count);

Count++;

}

while(Count < 10);

Console.WriteLine(Thrd.Name + " завершен.");

}

}

class MultiThreadlmproved

{

static void Main()

{

Console.WriteLine("Основной поток начат.");

// Сконструировать три потока

MyThread mtl = new MyThread("Потомок #1");

MyThread mt2 = new MyThread("Потомок #2");

MyThread mt3 = new MyThread("Потомок #3");

do

{

Console.Write(".");

Thread.Sleep (100);

}

while (mt1.Thrd.IsAlive && mt2.Thrd.IsAlive && mt3.Thrd.IsAlive);

Console.WriteLine("Основной поток завершен.");

}

}

Еще один способ отслеживания момента окончания состоит в вызове метода Join (). Метод Join() ожидает до тех пор, пока поток, для которого он был вызван, не завершится. Его имя отражает принцип ожидания до тех пор, пока вызывающий поток не присоединится к вызванному методу. Если же данный поток не был начат, то генерируется исключение ThreadStateException. В других формах метода Join() можно указать максимальный период времени, в течение которого следует ожидать завершения указанного потока. Для использования метода Join(), необходимо в предыдущем примере изменить метод Main() как показано ниже:

// Использовать метод Join() для ожидания до тех пор, пока потоки не завершатся

static void Main()

{

Console.WriteLine("Основной поток начат.");

// Сконструировать три потока

MyThread mtl = new MyThread("Потомок #1");

MyThread mt2 = new MyThread("Потомок #2");

MyThread mt3 = new MyThread("Потомок #3");

mt1.Thrd.Join();

Console.WriteLine("Потомок #1 присоединен.");

mt2.Thrd.Join();

Console.WriteLine("Потомок #2 присоединен.");

mt3.Thrd.Join();

Console.WriteLine("Потомок #3 присоединен.");

Console.WriteLine("Основной поток завершен.");

}

Как видите, выполнение потоков завершилось после возврата из последовательного ряда вызовов метода Join ().