Учебное пособие (1077022), страница 24
Текст из файла (страница 24)
Внастоящее время именно этот класс является рекомендуемым средствомдля работы с потоками. Класс Task основан на классе ThreadPool, онобладает всеми достоинствами ThreadPool, и при этом содержит удобныйдля прикладного программиста набор методов для работы с потоками.193В данном разделе представлены примеры работы с классами Thread иTask (класс ThreadPool не рассматривается), а также пример параллельногопоиска в массиве данных на основе класса Task.9.1 Использование класса ThreadПример использования класса Thread:/// <summary>/// Пример запуска потоков/// </summary>public static void ThreadExample(){Console.WriteLine("Пример использования \"старого\" вариантамногопоточности с использованием класса Thread:");const int n = 10;Thread[] threads = new Thread[n];for (int i = 0; i < n; i++){threads[i] = new Thread(ThreadRun);}for (int i = 0; i < n; i++){threads[i].Start("Поток №" + i.ToString());}}В данном примере создается массив из 10 элементов threads объектовкласса Thread.
В цикле проводится инициализация элементов массива, длячего и создаются объекты класса Thread.Конструктор класса Thread принимает параметр делегатного типа,который соответствует делегату ParameterizedThreadStart:public delegate void ParameterizedThreadStart(object obj);Это означает, что в конструктор класса Thread нужно передать метод,который принимает один параметр типа object и возвращает void. Вданном примере таким методом будет ThreadRun.Далее в цикле запускаются потоки.
Для этого вызывается метод Startдля каждого из элементов массива потоков. В качестве параметра в методStart передается объект класса object, который подставляется при запуске194потока в делегатный параметр ParameterizedThreadStart. В нашем примерестрока «"Поток №" + i.ToString()» будет передана в качестве параметраметоду ThreadRun.Рассмотрим более детально метод ThreadRun, который вызывается впотоке:/// <summary>/// Метод, запускаемый в потоке/// </summary>/// <param name="param"></param>public static void ThreadRun(object param){Random r = new Random();int delay = r.Next(3000, 10000);Thread.Sleep(delay);Console.WriteLine(param.ToString());}В методе создается объект r класса Random, применяющийся длягенерации случайных чисел.
Метод r.Next возвращает случайное число вдиапазоне от 3000 до 10000 – это задержка в миллисекундах, котораясоответствует диапазону от 3 до 10 секунд.Полученное случайное время задержки передается в качествепараметра статическому методу Thread.Sleep, выполняющему задержкутекущего потока на заданное количество миллисекунд.После задержки в консоль выводится сообщение, переданное в методThreadRun в качестве параметра.Таким образом, параллельно запускаются 10 экземпляров методаThreadRun, каждый из которых через случайное время в диапазоне от 3 до10 секунд выводит в консоль сообщение, переданное в метод в качествепараметра при инициализации потока.Результаты вывода в консоль (после завершения работы всехпотоков):Примериспользованиякласса Thread:Поток №7Поток №8"старого"вариантамногопоточностисиспользованием195Поток №9Поток №5Поток №6Поток №1Поток №0Поток №2Поток №3Поток №4Сообщения выводятся в консоль в случайном порядке, что связано сослучайным временем задержки в каждом потоке.Отметим, что, хотя класс Thread позволяет успешно решать задачупараллельногозапускапотоков,егоиспользованиесвязаносопределенными неудобствами для прикладного программиста.
Всепараметры передаются в метод потока через единственный параметр типаobject. Поэтому, как правило, необходимо создавать отдельный класс,который будет содержать как входные, так и выходные параметры потока,передавать объект данного класса в качестве параметра в метод потока, вметоде потока осуществлять приведение типа к требуемому классу, ивозвращать через данный класс результаты работы потока, так как классThread не имеет специальных способов возврата результатов работы ввызывающий поток.Также следует отметить, что аналогичные затруднения возникают прииспользовании класса ThreadPool.9.2 Использование класса TaskРазработчики .NET учли рассмотренные выше сложности припроектировании класса Task.Реализуем рассмотренный ранее пример с использованием классаTask:/// <summary>/// Пример запуска потоков с использованием Task/// </summary>public static void TaskExample()196{Console.WriteLine("Пример использования \"нового\" вариантамногопоточности с использованием класса Task:");const int n = 10;for (int i = 0; i < n; i++){Task.Factory.StartNew((object param) =>{Random r = new Random();int delay = r.Next(3000, 10000);Thread.Sleep(delay);Console.WriteLine(param.ToString());},"Поток №" + i.ToString());}}В данном примере не нужно создавать массив.
Класс Task имеетстатическое свойство Factory («фабрика»). Здесь предполагается, что это«фабрика» для создания объектов (применяется шаблон объектноориентированного проектирования «фабрика»). У «фабрики» есть методсоздания и запуска потока StartNew. В данном примере используетсяследующая перегрузка метода StartNew:public Task StartNew(Action<object> action, object state);ПервымпараметромметодаStartNewявляетсяметод,соответствующий делегату Action<object>, то есть, как и в случаеиспользования класса Thread метод принимающий параметр типа object ивозвращающий void.В рассматриваемом примере первому параметру соответствуетлямбда-выражение, представляющее собой аналог метода ThreadRun прииспользовании класса Thread:(object param) =>{Random r = new Random();int delay = r.Next(3000, 10000);Thread.Sleep(delay);Console.WriteLine(param.ToString());}197Второй параметр метода StartNew – параметр типа object, который призапуске потока подставляется в делегатный параметр.
Здесь второмупараметру соответствует строка «"Поток №" + i.ToString()», которая призапуске потока подставляется в параметр лямбда-выражения «(objectparam) => ».Результаты вывода в консоль (после завершения работы всехпотоков):Примериспользования"нового"вариантамногопоточностисиспользованиемкласса Task:Поток №0Поток №5Поток №1Поток №3Поток №2Поток №7Поток №6Поток №4Поток №9Поток №8Как и в предыдущем случае сообщения выводятся в консоль вслучайном порядке.Код решения задачи с использованием класса Task получился болеекомпактным, метод потока задается с помощью лямбда-выражения.Однако более важное преимущество класса Task – возможность возвратарезультата выполнения потока.9.3 Возвратрезультатавыполненияпотокасиспользованием класса TaskРешим более сложную задачу с применением класса Task.
Заполниммассив числами от 1 до 100 и найдем в нем числа, которые без остаткаделятся на 3. При этом массив разделим на 10 подмассивов (посоответствующим десяткам) и будем выполнять поиск многопоточно.198Результаты поиска по 10 подмассивам объединим в единый массиврезультатов.Для решения данной задачи необходимо решить подзадачу несвязанную с потоками – разделение массива на подмассивы, для чегосоздадим вспомогательный класс для хранения диапазонов:/// <summary>/// Хранение минимального и максимального значений диапазона/// </summary>public class MinMax{public int Min {get; set;}public int Max {get; set;}public MinMax(int pmin, int pmax){this.Min = pmin;this.Max = pmax;}}Разделение массива на подмассивы реализуется с помощью методаDivideSubArrays статического класса SubArrays.
Код приведен ниже идетально откомментирован:/// <summary>/// Класс для деления массива на последовательности/// </summary>public static class SubArrays{/// <summary>/// Деление массива на последовательности/// </summary>/// <param name="beginIndex">Начальный индекс массива</param>/// <param name="endIndex">Конечный индекс массива</param>/// <param name="subArraysCount">Требуемое количествоподмассивов</param>/// <returns>Список пар с индексами подмассивов</returns>public static List<MinMax> DivideSubArrays(int beginIndex, int endIndex, int subArraysCount){//Результирующий список пар с индексами подмассивовList<MinMax> result = new List<MinMax>();//Если число элементов в массиве слишком мало для деления//то возвращается массив целикомif ((endIndex - beginIndex) <= subArraysCount)199{result.Add(new MinMax(0, (endIndex - beginIndex)));}else{//Размер подмассиваint delta = (endIndex - beginIndex) / subArraysCount;//Начало отсчетаint currentBegin = beginIndex;//Пока размер подмассива укладывается в оставшуюся//последовательностьwhile ((endIndex - currentBegin) >= 2 * delta){//Формируем подмассив на основе начала//последовательностиresult.Add(new MinMax(currentBegin, currentBegin + delta));//Сдвигаем начало последовательности//вперед на размер подмассиваcurrentBegin += delta;}//Оставшийся фрагмент массиваresult.Add(new MinMax(currentBegin, endIndex));}//Возврат списка результатовreturn result;}}В результате работы метод DivideSubArrays возвращает списокобъектов класса MinMax, которые хранят начальный и конечный индексысоответствующих диапазонов массива.На этом рассмотрение вспомогательных классов завершено, и мыпереходимкметодуArrayThreadExample,осуществляющемумногопоточный поиск в массиве:/// <summary>/// Многопоточный поиск в массиве/// </summary>public static void ArrayThreadExample(int ArrayLength, int ThreadCount, int Divider){//Результирующий список чиселList<int> Result = new List<int>();//Создание и заполнение временного списка данныхList<int> tempList = new List<int>();200for (int i = 0; i < ArrayLength; i++) tempList.Add(i + 1);//Деление списка на фрагменты//для параллельного запуска в потокахList<MinMax> arrayDivList =SubArrays.DivideSubArrays(0, ArrayLength, ThreadCount);int count = arrayDivList.Count;//Вывод диапазонов деления исходного массиваfor (int i = 0; i < count; i++){//Вывод результатов, найденных в каждом потокеConsole.WriteLine("Диапазон " + i.ToString() + ": " +arrayDivList[i].Min + " - " +arrayDivList[i].Max);}Console.WriteLine();//Создание таймераStopwatch timer = new Stopwatch();//Запуск таймераtimer.Start();//Количество потоков соответствует количеству фрагментов массиваTask<List<int>>[] tasks = new Task<List<int>>[count];//Запуск потоковfor (int i = 0; i < count; i++){//Создание временного списка, чтобы потоки//не работали параллельно с одной коллекциейList<int> tempTaskList =tempList.GetRange(arrayDivList[i].Min,arrayDivList[i].Max - arrayDivList[i].Min);tasks[i] = new Task<List<int>>(//Метод, который будет выполняться в потокеArrayThreadTask,//Параметры потока передаются в виде кортежа,//чтобы не создавать временный классnew Tuple<List<int>, int>(tempTaskList, Divider));//Запуск потокаtasks[i].Start();}//Ожидание завершения всех потоковTask.WaitAll(tasks);//Остановка таймера201timer.Stop();//Объединение результатов полученных из разных потоковfor (int i = 0; i < count; i++){//Вывод результатов, найденных в каждом потокеConsole.Write("Поток " + i.ToString() + ": ");foreach (var x in tasks[i].Result)Console.Write(x.ToString() + " ");Console.WriteLine();//Добавление результатов конкретного потока//в общий массив результатовResult.AddRange(tasks[i].Result);}//Вывод общего массива результатовConsole.WriteLine("\nМассив из {0} элементов обработан {1}потоками за {2}.
Найдено: {3}", ArrayLength,count, timer.Elapsed, Result.Count);foreach (int i in Result)Console.Write(i.ToString().PadRight(5));Console.WriteLine();}Метод принимает три параметра: int ArrayLength – число элементов в массиве; в рассматриваемомпримере оно равно 100; int ThreadCount – число потоков для одновременного поиска; врассматриваемом примере составляет 10; int Divider – искомый делитель; в рассматриваемом примереравен 3.В начале работы метода создается переменная Result типа List<int>.















