Главная страница
Навигация по странице:

  • Свойство IsBackground

  • Приоритеты потоков

  • Синхронизация

  • Справочник по C# Герберт Шилдт ббк 32. 973. 26018 75 Ш57 удк 681 07 Издательский дом "Вильямс" Зав редакцией


    Скачать 5.05 Mb.
    НазваниеСправочник по C# Герберт Шилдт ббк 32. 973. 26018 75 Ш57 удк 681 07 Издательский дом "Вильямс" Зав редакцией
    АнкорC #.pdf
    Дата08.12.2017
    Размер5.05 Mb.
    Формат файлаpdf
    Имя файлаC #.pdf
    ТипСправочник
    #10795
    страница39 из 52
    1   ...   35   36   37   38   39   40   41   42   ...   52
    584
    Часть II. Библиотека C# указанный поток и подождать, когда тот присоединится к вызвавшему его методу. Если заданный поток даже не стартовал, будет сгенерировано исключение типа
    ThreadStateException
    . Другие форматы метода
    Join()
    позволяют указать максимальный объем времени, в течение которого вы собираетесь ожидать завершения выполнения потока.
    Рассмотрим программу, которая для получения гарантии того, что основной поток завершится последним, использует метод
    Join()
    :
    // Использование метода Join(). using System; using System.Threading; class MyThread { public int count; public Thread thrd; public MyThread(string name) { count = 0; thrd = new Thread(new ThreadStart(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 + " завершен.");
    }
    }
    // Используем метод Join() для ожидания завершения потоков. class JoinThreads { public static void Main() {
    Console.WriteLine("Основной поток стартовал.");
    //
    Создаем три потока.
    MyThread mt1 = 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 присоединен.");

    Глава 21. Многопоточное программирование
    585
    Console.WriteLine("Основной поток завершен.");
    }
    }
    Ниже приведены результаты выполнения этой программы. Не забывайте, что результаты, полученные вами, могут слегка отличаться от приведенных здесь.
    Основной поток стартовал.
    Потомок #1 стартовал.
    Потомок #2 стартовал.
    Потомок #3 стартовал. В потоке
    Потомок #1, count = 0 В потоке
    Потомок #2, count = 0 В потоке
    Потомок #3, count = 0 В потоке
    Потомок #1, count = 1 В потоке
    Потомок #2, count = 1 В потоке
    Потомок #3, count = 1 В потоке
    Потомок #1, count = 2 В потоке
    Потомок #2, count = 2 В потоке
    Потомок #3, count = 2 В потоке
    Потомок #1, count = 3 В потоке
    Потомок #2, count = 3 В потоке
    Потомок #3, count = 3 В потоке
    Потомок #1, count = 4 В потоке
    Потомок #2, count = 4 В потоке
    Потомок #3, count = 4 В потоке
    Потомок #1, count = 5 В потоке
    Потомок #2, count = 5 В потоке
    Потомок #3, count = 5 В потоке
    Потомок #1, count = 6 В потоке
    Потомок #2, count = 6 В потоке
    Потомок #3, count = 6 В потоке
    Потомок #1, count = 7 В потоке
    Потомок #2, count = 7 В потоке
    Потомок #3, count = 7 В потоке
    Потомок #1, count = 8 В потоке
    Потомок #2, count = 9 В потоке
    Потомок #3, count = 8 В потоке
    Потомок #1, count = 9
    Потомок #1 завершен.
    В потоке Потомок #2, count = 9
    Потомок #2 завершен.
    В потоке Потомок #3, count = 9
    Потомок #3 завершен.
    Потомок #1 присоединен.
    Потомок #2 присоединен.
    Потомок #3 присоединен.
    Основной поток завершен.
    Нетрудно убедиться в том, что после выхода из методов
    Join()
    выполнение потоков завершено.
    Свойство IsBackground
    Как упоминалось выше, среда .NET Framework определяет два типа потоков: высокоприоритетные и фоновые. Единственное различие между ними состоит в том, что процесс не завершится до тех пор, пока не завершится выполнение всех его высоко-

    586
    Часть II. Библиотека C# приоритетных потоков, при этом фоновые потоки заканчиваются автоматически после завершения всех высокоприоритетных потоков. По умолчанию любой поток создается как высокоприоритетный. При необходимости его можно сделать фоновым с помощью свойства
    IsBackground
    , которое определено в классе
    Thread следующим образом: public bool IsBackground { get; set; }
    Чтобы перевести поток в категорию фоновых, достаточно присвоить свойству
    IsBackground значение true
    . Значение false означает, что соответствующий поток является высокоприоритетным.
    Приоритеты потоков
    Каждый поток имеет определенный приоритет. Приоритет потока, в частности, определяет, какой объем процессорного времени получает поток. В общем случае низкоприоритетным потокам выделяется немного времени ЦП, а высокоприоритетным — побольше. Нетрудно догадаться, что объем времени ЦП, выделяемый потоку, в значительной степени влияет на его выполнение и взаимодействие с другими потоками, выполняющимися в данный момент в системе.
    Важно понимать, что существуют и другие факторы (помимо приоритета потока), которые влияют на объем времени ЦП, выделяемый потоку. Например, если высокоприоритетный поток находится в состоянии ожидания некоторого ресурса, например вводимых с клавиатуры данных, он будет заблокирован, и в это время сможет работать поток с более низким приоритетом. Поэтому в подобной ситуации низкоприоритетный поток может получить более продолжительный доступ к ЦП, чем высокоприоритетный.
    После старта дочерний поток получает стандартное значение приоритета. Его можно изменить с помощью свойства Priority, которое является членом класса
    Thread
    . Общий формат его использования таков: public ThreadPriority Priority{ get; set; }
    Здесь ThreadPriority — перечисление, которое определяет следующие пять значений приоритета:
    ThreadPriority.Highest
    ThreadPriority.AboveNormal
    ThreadPriority.Normal
    ThreadPriority.BelowNormal
    ThreadPriority.Lowest
    По умолчанию потоку присваивается значение приоритета
    ThreadPriority.Normal.
    Чтобы понять, как приоритеты влияют на выполнение потоков, воспользуемся примером, в котором выполняются два потока, причем с разными приоритетами. Эти потоки создаются как экземпляры класса
    MyThread
    . Метод run()
    содержит цикл, в котором подсчитывается количество итераций. Цикл останавливается либо на счете 1 000 000 000, либо в тот момент, когда статическая переменная stop примет значение true
    . До входа в этот цикл переменная stop установлена равной false
    . Первый поток, “досчитав” до 1 000 000 000, устанавливает переменную stop равной значению true
    . Это заставляет второй поток отказаться от следующего кванта времени (объем процессорного времени, выделяемого потоку). На каждом проходе цикла содержимое строковой переменной currentName сравнивается с именем выполняющегося потока. Если они не совпадают, значит, имело место переключение задач. При каждом переключении

    Глава 21. Многопоточное программирование
    587 задач отображается имя нового потока, а переменной currentName присваивается имя текущего потока. Это позволяет наблюдать за тем, как часто каждый из потоков получает доступ к ЦП. По завершении выполнения обоих потоков отображается количество итераций, выполненных в каждом цикле.
    // Демонстрация использования приоритетов потоков. using System; using System.Threading; class MyThread { public int count; public Thread thrd; static bool stop = false; static string currentName;
    /*
    Создаем новый поток. Обратите внимание на то, что этот конструктор в действительности не запускает потоки на выполнение. */ public MyThread(string name) { count = 0; thrd = new Thread(new ThreadStart(this.run)); thrd.Name = name; currentName = name;
    }
    //
    Начинаем выполнение нового потока. void run() {
    Console.WriteLine("Поток " + thrd.Name + " стартовал."); do
    { count++; if(currentName != thrd.Name) { currentName
    = thrd.Name;
    Console.WriteLine("В потоке " + currentName);
    }
    } while(stop == false && count < 1000000000); stop = true;
    Console.WriteLine("Поток " + thrd.Name + " завершен.");
    }
    } class PriorityDemo { public static void Main() {
    MyThread mt1 = new MyThread("с высоким приоритетом");
    MyThread mt2 = new MyThread("с низким приоритетом");
    //
    Устанавливаем приоритеты. mt1.thrd.Priority = ThreadPriority.AboveNormal; mt2.thrd.Priority = ThreadPriority.BelowNormal;
    //
    Запускаем потоки на выполнение. mt1.thrd.Start(); mt2.thrd.Start();

    588
    Часть II. Библиотека C# mt1.thrd.Join(); mt2.thrd.Join();
    Console.WriteLine();
    Console.WriteLine("Поток " + mt1.thrd.Name +
    " досчитал до " + mt1.count);
    Console.WriteLine("Поток " + mt2.thrd.Name +
    " досчитал до " + mt2.count);
    }
    }
    Вот как выглядят результаты выполнения этой программы на компьютере с 1 - гигагерцевым процессором Pentium, на котором установлена ОС Windows 2000:
    Поток с высоким приоритетом стартовал.
    Б потоке с высоким приоритетом
    Поток с низким приоритетом стартовал.
    В потоке с низким приоритетом
    В потоке с высоким приоритетом
    В потоке с низким приоритетом
    В потоке с высоким приоритетом
    В потоке с низким приоритетом
    В потоке с высоким приоритетом
    В потоке с низким приоритетом
    В потоке с высоким приоритетом
    В потоке с низким приоритетом
    В потоке с высоким приоритетом
    Поток с высоким приоритетом завершен.
    Поток с низким приоритетом завершен.
    Поток с высоким приоритетом досчитал до 1000000000 Поток с низким приоритетом досчитал до 25600064
    Судя по полученным результатам, поток с высоким приоритетом получил приблизительно 98% процессорного времени. Конечно же, результаты, полученные на вашем компьютере, могут отличаться от приведенных, поскольку они зависят от быстродействия ЦП и количества их задач, выполняемых в системе. Результаты работы программы зависят также от версии Windows.
    Поскольку многопоточный код в различных средах может вести себя по-разному, никогда не следует полагаться на характеристики выполнения программы, достигнутые в одной среде. Например, в предыдущем примере было бы ошибкой предполагать, что низкоприоритетный поток будет всегда выполняться в течение хотя бы небольшого отрезка времени перед тем, как завершится поток с высоким приоритетом. Например, в другой среде высокоприоритетный поток может завершиться еще до того, как поток с низким приоритетом хотя бы раз использует свой квант времени.
    Синхронизация
    При использовании в программе нескольких потоков иногда необходимо координировать их выполнение.
    Процесс координации потоков называется синхронизацией. К синхронизации прибегают в тех случаях, когда двум или большему числу потоков необходимо получить доступ к общему ресурсу, который в каждый момент времени может использовать только один поток. Например, когда один поток выводит данные в файл, в это самое время второй поток должен быть лишен возможности выполнять

    Глава 21. Многопоточное программирование
    589 аналогичные действия. Синхронизация необходима и в других ситуациях. Например, один поток ожидает, пока не произойдет событие, “судьба” которого зависит от другого потока.
    В этом случае необходимо иметь средство, которое бы удерживало первый поток в состоянии приостановки до тех пор, пока не произойдет ожидаемое им событие. После этого “спящий” поток должен возобновить выполнение.
    В основе синхронизации лежит понятие блокировки, т.е. управление доступом к некоторому блоку кода в объекте. На то время, когда объект заблокирован одним потоком, никакой другой поток не может получить доступ к заблокированному блоку кода. Когда поток снимет блокировку, объект станет доступным для использования другим потоком.
    Средство блокировки встроено в язык C#, поэтому доступ ко всем объектам может быть синхронизирован. Синхронизация поддерживается ключевым словом lock
    Поскольку синхронизация заложена в C# с самого начала разработки языка, применять ее довольно легко.
    Формат использования инструкции lock таков: lock(
    object
    ) {
    //
    Инструкции, подлежащие синхронизации.
    }
    Здесь параметр
    object
    представляет собой ссылку на синхронизируемый объект.
    Если нужно синхронизировать только один элемент, фигурные скобки можно опустить,
    Инструкция lock гарантирует, что указанный блок кода, защищенный блокировкой для данного объекта, может быть использован только потоком, который получает эту блокировку. Все другие потоки остаются заблокированными до тех пор, пока блокировка не будет снята. А снята она будет лишь при выходе из этого блока.
    В следующей программе демонстрируется синхронизация посредством управления доступом к методу sumIt()
    , который суммирует элементы массива целочисленного типа:
    // Использование инструкции lock для синхронизации
    // доступа к объекту. using System; using System.Threading; class SumArray { int sum; public int sumIt(int[] nums) { lock(this) { // Блокировка всего метода. sum
    =0;
    //
    Начальное значение суммы. for(int i=0; i < nums.Length; i++) { sum
    += nums[i];
    Console.WriteLine(
    "Промежуточная сумма для потока " +
    Thread.CurrentThread.Name
    +
    " равна " + sum);
    Thread.Sleep(10);
    //
    Разрешено переключение задач.
    } return sum;
    }
    }
    } class MyThread {

    590
    Часть II. Библиотека C# public Thread thrd; int[] a; int answer;
    /*
    Создаем один объект класса SumArray для всех экземпляров класса MyThread. */ static SumArray sa = new SumArray();
    //
    Создаем новый поток. public MyThread(string name, int[] nums) { a = nums; thrd = new Thread(new ThreadStart(this.run)); thrd.Name = name; thrd.Start();
    //
    Запускаем поток на выполнение.
    }
    //
    Начало выполнения нового потока. void run() {
    Console.WriteLine(thrd.Name + " стартовал."); answer = sa.sumIt(a);
    Console.WriteLine("Сумма для потока " + thrd.Name +
    " равна " + answer);
    Console.WriteLine(thrd.Name + " завершен.");
    }
    } class Sync { public static void Main() { int[] a = {1, 2, 3, 4, 5};
    MyThread mt1 = new MyThread("Потомок #1", a);
    MyThread mt2 = new MyThread("Потомок #2", a); mt1.thrd.Join(); mt2.thrd.Join();
    }
    }
    Результаты выполнения программы таковы:
    Потомок #1 стартовал.
    Потомок #2 стартовал.
    Промежуточная сумма для потока Потомок #1 равна 1
    Промежуточная сумма для потока Потомок #1 равна 3
    Промежуточная сумма для потока Потомок #1 равна 6
    Промежуточная сумма для потока Потомок #1 равна 10
    Промежуточная сумма для потока Потомок #1 равна 15
    Промежуточная сумма для потока Потомок #2 равна 1
    Сумма для потока Потомок #1 равна 15
    Потомок #1 завершен.
    Промежуточная сумма для потока Потомок #2 равна 3
    Промежуточная сумма для потока Потомок #2 равна 6
    Промежуточная сумма для потока Потомок #2 равна 10
    Промежуточная сумма для потока Потомок #2 равна 15
    Сумма для потока Потомок #2 равна 15
    Потомок #2 завершен.

    Глава 21. Многопоточное программирование
    591
    Как видно по результатам, оба потока правильно подсчитали сумму чисел.
    Эту программу стоит рассмотреть подробнее. В ней создается три класса. Первому присвоено имя
    SumArray
    . В нем определен метод sumIt()
    , который суммирует элементы целочисленного массива. Во втором классе,
    MyThread
    , используется static
    -объект sa типа
    SumArray
    . Таким образом, все объекты типа
    MyThread совместно используют только один объект типа
    SumArray
    . Этот объект служит для получения суммы целочисленных элементов массива. Обратите внимание на то, что промежуточная сумма хранится в поле sum класса
    SumArray
    . Следовательно, если два потока будут использовать метод sumIt()
    одновременно, каждый из них попытается хранить в поле sum
    "свою" промежуточную сумму. Поскольку это приведет к ошибочным результатам, доступ к методу sumIt()
    необходимо синхронизировать. Наконец, упомянутые два потока создаются в классе Sync, который дает им “путевку в жизнь”, т.е. направляет на вычисление суммы элементов целочисленного массива.
    В методе sumIt()
    инструкция lock предотвращает одновременное его использование различными потоками. Обратите внимание на то, что в этой инструкции блокировки в качестве синхронизируемого объекта служит ссылка this
    . Обычно именно так выполнятся инструкция lock
    , если блокируется вызывающий объект. Метод
    Sleep()
    вызывается специально для того, чтобы разрешить переключение задач (если оно возможно), но в данном случае это невозможно. Поскольку код метода sumIt()
    заблокирован, он доступен одновременно только для одного потока. Следовательно, после того, как начнет выполняться второй поток, он не войдет в метод sumIt()
    до тех пор, пока его (метод) полностью не отработает первый дочерний поток. Тем самым гарантируется получение корректного результата.
    Чтобы до конца понять действие инструкции lock
    , попробуйте удалить ее из тела метода sumIt()
    . В этом случае метод sumIt()
    окажется без синхронизации, и любые потоки смогут использовать его одновременно для одного и того же объекта. Дело в том, что в поле sum хранится промежуточная сумма, и содержимое этого поля будет изменяться каждым потоком, который вызывает метод sumIt()
    . Таким образом, если два потока одновременно вызовут метод sumIt()
    для одного и того же объекта, будут получены некорректные результаты, поскольку без синхронизации переменная sum будет отражать смешанный результат суммирования обоих потоков. Вот, например, каким может быть результат выполнения этой программы после удаления инструкции lock из метода sumIt()
    :
    Потомок #1 стартовал.
    Промежуточная сумма для потока Потомок #1 равна 1
    Потомок #2 стартовал.
    Промежуточная сумма для потока Потомок #2 равна 1
    Промежуточная сумма для потока Потомок #1 равна 3
    Промежуточная сумма для потока Потомок #2 равна 5
    Промежуточная сумма для потока Потомок #1 равна 8
    Промежуточная сумма для потока Потомок #2 равна 11
    Промежуточная сумма для потока Потомок #1 равна 15
    Промежуточная сумма для потока Потомок #2 равна 19
    Промежуточная сумма для потока Потомок #1 равна 24
    Промежуточная сумма для потока Потомок #2 равна 29
    Сумма для потока Потомок #2 равна 29
    Потомок #2 завершен.
    Сумма для потока Потомок #1 равна 29
    Потомок #1 завершен.
    Судя по результатам, оба дочерних потока используют метод sumIt()
    одновременно для одного и того же объекта, поэтому значение поля sum неверно. Итак, подытожим действие инструкции lock

    1   ...   35   36   37   38   39   40   41   42   ...   52


    написать администратору сайта