Опубликован: 23.04.2013 | Уровень: для всех | Доступ: платный
Лекция 5:

Потоки и параллельные вычисления

< Лекция 4 || Лекция 5: 1234 || Лекция 6 >
Аннотация: В данной лекции мы рассмотрим ряд средств, позволяющих создавать на языке C# параллельные программы, которые на многоядерном компьютере выполняются эффективнее (быстрее), чем на одноядерном компьютере.

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

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

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

Пространство имен Threading

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

Класс Thread

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

При программировании на C# поток - это объект класса Thread. Давайте рассмотрим, какие операции можно выполнять над этими объектами, какие свойства можно задавать для них.

Объявление объектов класса Thread

Объекты этого класса объявляются, также как и все другие объекты C#, никаких особенностей в объявлении нет. Вот пример объявления двух потоков:

Thread thread_No;
Thread thread_Yes;

Создание потоков - объектов класса Thread

Как обычно, объекты создаются конструктором класса. При создании потоков необходимо задать код, который будет выполняться потоком. Этот код должен быть методом класса. Зачастую, потоку передается не только метод, но и объект, вызывающий метод. Например, в проекте может быть класс Works с методом Work. В клиентском классе, создающем поток, можно создать объект worker класса Work и при создании потока передать ему квалифицированный вызов - worker.Work. Это хороший стиль, облегчающий построение потоко-безопасного приложения, поскольку метод Work будет работать с полями переданного ему объекта, и не будет конфликтовать с другим потоком, работающим с тем же методом Work, но вызванным другим объектом - another_worker.

С точки зрения операционной системы передаваемый потоку метод представляет модуль, выполняемый потоком.

Метод, который будет исполняться потоком, необходимо передать конструктору класса Thread. Поскольку конструктору необходимо передать метод, то соответствующий аргумент конструктора должен иметь функциональный тип и задаваться делегатом, описывающим сигнатуру метода. У класса Thread есть четыре конструктора. Простейший из них имеет один аргумент, тип которого задается делегатом ThreadStart. Этому типу соответствуют все методы, не имеющие аргументов и являющиеся процедурами, - методы типа void M() {…}. Такие методы не являются экзотикой, - они характерны для объектного стиля программирования, когда вся входная и выходная информация метода передается через поля класса.

Если все же методу, выполняемому в потоке, необходимо передать информацию, то можно использовать конструктор класса Thread с одним аргументом, тип которого задается делегатом ParameterizedThreadStart. Этот класс задан следующим образом:

public delegate void ParameterizedThreadStart(Object obj)

Этому типу соответствуют все методы с одним аргументом, являющиеся процедурами. Фактически этот класс является универсальным, позволяющим использовать его для всех методов, имеющих аргументы. Понятно, что предусмотреть все возможные сигнатуры, возникающие в практических задачах, невозможно, поэтому приходится идти на компромисс. Если у метода, передаваемого потоку, один аргумент некоторого типа Т, то он соответствует сигнатуре делегата, поскольку тип object является родителем любого типа. В реализации метода достаточно будет выполнить явное приведение типа object к типу Т. Если же у метода n (n > 1) аргументов, то в этом случае необходимо создать специальный класс (структуру) S_class, описывающий требуемое методу множество аргументов. После этого можно описать метод, передаваемый потоку, как метод с одним аргументом типа S_class, что и позволит передать этот метод конструктору потока.

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

Объявление

Thread  my_thread;

позволяет объявить объект, задающий поток. Создать сам объект можно, вызвав конструктор класса Thread:

my_thread = new Thread(my_object.my_method);

передав конструктору метод класса. Все будет синтаксически корректно, если метод my_method не имеет аргументов или имеет один аргумент.

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

Приведу пример, иллюстрирующий рассмотренные способы создания и запуска потоков. Начнем с создания класса Works:

/// <summary>
    /// Демо класса, в котором методы работают с полями класса
    /// Нет необходимости передавать методу аргументы
    /// Методы соответствуют делегату ThreadStart
    /// 
    /// </summary>
    class Works
    {
        string worker;
        string job;
        string mark;
        string res;
        public Works(string worker, string job, string mark)
        {
            this.worker = worker;
            this.job = job;
            this.mark = mark;
        }
        public string Res
        {
            get { return res; }
        }
        /// <summary>
        /// Метод, вызванный объектом класса,
        /// может быть передан потоку на выполнение
        /// </summary>
        public void Work()
        {         
            res = String.Format(
            "Работник {0} выполнил задание: <{1}> с оценкой {2}!",
            worker, job, mark);            
        }
    }

В процедуре Main демонстрируются разные способы создания и запуска трех потоков:

static void Main(string[] args)
{
  Thread t = new Thread(delegate() { Info("Петров", 22); });
  // Thread t = new Thread(()=> { Info("Петров", 22); });
   t.Start();
   t.Join();
   Console.WriteLine("Подтверждаю, Main");
   Works worker1 = new Works("Петров", "проект с потоками", "отлично");
   Works worker2 = new Works("Сергеев", "интерфейс проекта ", "хорошо");
        {
            Thread w1 = new Thread(worker1.Work);
            Thread w2 = new Thread(worker2.Work);
            w1.Start();
            w2.Start();
            Console.WriteLine(worker1.Res);
            Console.WriteLine(worker2.Res);
        }
        static void Info(string fio, int age)
        {
            Console.WriteLine("Фамилия:  " + fio + "  Возраст: " + age); 
        } 
}

При создании потока t конструктору передается анонимный метод, представляющий вызов метода Info, которому передаются требуемые аргументы. Заметьте, никаких преобразований аргументов в этом случае не требуется, как и не требуется создания дополнительных классов.

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

Потоки w1 и w2 выполняют один и тот же метод Work класса Works, и на многоядерном компьютере будут выполняться параллельно. Никаких конфликтов не возникает, поскольку метод вызывается разными объектами класса Works, у каждого из которых своя память для хранения полей класса.

Результаты работы процедуры Main показаны на рис. 4.1

Результаты создания и запуска потоков

Рис. 4.1. Результаты создания и запуска потоков

Запуск потока

Создания объекта, задающего поток, еще не достаточно, чтобы поток начал выполняться. Для запуска потока на выполнение необходимо вызвать метод Start в форме my_thread.Start(), либо в форме my_thread.Start(my_object), если методу my_method необходимо передать информацию. Заметьте, фактический аргумент my_object, передаваемый методу, исполняемому потоком, передается не в момент создания потока, а в момент его запуска на выполнение. Другая ситуация имеет место, когда потоку передается анонимный метод. Как показано в предыдущем примере, анонимный метод вызывает метод Info, передавая ему фактические параметры.

Следует понимать, что вызов метода Start не означает, что запущенный метод my_method непосредственно начнет выполняться. Вызов метода Start является указанием операционной системе на перевод потока my_thread из состояния "создание" в состояние "готовность", так что поток станет в очередь на выполнение. Если потоку повезет, и в момент запуска он окажется первым в очереди и найдется свободный процессор, то метод начнет непосредственно выполняться, иначе он будет ждать, пока до него не дойдет очередь.

Когда запущенный на выполнение метод my_method завершает свою работу, то заканчивает свою жизнь и соответствующий поток, для него нельзя повторно вызвать метод Start. для повторного запуска метода нужно создать новый поток. "Мавр сделал свое дело, - мавр должен уйти".

Состояние "ожидание"

Выполняемый поток операционная система периодически может переводить в состояние "ожидание", предоставляя процессор другим потокам. Но поток сам может потребовать перевода его в это состояние. Причины для этого могут быть разные, - чаще всего это делается в интересах синхронизации совместной работы потоков. Рассмотрим два метода класса Thread, используемые для этих целей. Статический метод Sleep позволяет потоку "уснуть" на некоторое время. У этого метода две перегруженные реализации, - обе с одним аргументом - dt. Если задать аргумент dt типа int, то поток засыпает на dt миллисекунд, после чего готов выполнять свою работу. Часто задается значение этого аргумента, равное нулю. В этом случае поток добровольно позволяет другим потокам выполнять свою работу, а сам становится в конец очереди, - пример бескорыстия ради общих интересов. Аргумент dt может быть объектом класса TimeSpan. У этого класса несколько конструкторов. Если вызвать конструктор с одним аргументом, то время сна будет задаваться в тиках, если задавать три аргумента, то можно время задать в часах, минутах, секундах; четыре аргумента позволяют задавать и миллисекунды.

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

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

Нетерпеливый поток может вызывать метод Join не как процедуру, а как булевскую функцию с одним аргументом dt, задающим время, в течение которого вызывающий поток ждет завершения работы дочернего потока. Если в указанное время дочерний поток завершается, то функция возвращает значение true, иначе - false.

< Лекция 4 || Лекция 5: 1234 || Лекция 6 >
Алексей Рыжков
Алексей Рыжков

не хватает одного параметра:

static void Main(string[] args)
        {
            x = new int[n];
            Print(Sample1,"original");
            Print(Sample1P, "paralel");
            Console.Read();
        }

Никита Белов
Никита Белов

Выставил оценки курса и заданий, начал писать замечания. После нажатия кнопки "Enter" окно отзыва пропало, открыть его снова не могу. Кнопка "Удалить комментарий" в разделе "Мнения" не работает. Как мне отредактировать недописанный отзыв?

Анатолий Кирсанов
Анатолий Кирсанов
Россия, Тамбов, Российский Новый Университет, 2012
Алексей Горячих
Алексей Горячих
Россия