Васильев А.Н. Основы программирования на C#. Васильев А. Н. Программирование
Скачать 5.54 Mb.
|
313 Результат выполнения программы может быть таким, как показано ниже: Результат выполнения программы (из листинга После создания символьного массива symbs он заполняется символом ƍ*ƍ (звездочка. В целочисленную переменную first записывается значение первого элемента массива, а в переменную second записывается значение последнего элемента массива. Первый поток создается на основе лямбда-выражения, определяющего метод, который выполняется при запуске первого потока на выполнение. В этом методе символьная переменная start получает начальное значение ƍəƍ, после чего запускается формально бесконечный цикл. В телеоператора цикла, при истинности условия second>first, выполняются следующие команды Текущее значение переменной start присваивается элементу массива с индексом second (команда symbs[second]=start ). • Вследствие выполнения команды start-- переменная start значением получает предыдущую (по отношению к текущему значению) букву из алфавита Командой second-- значение переменной second уменьшается на единицу Чтобы поток не выполнил работу слишком быстро, командой Thread.Sleep(100) выполняем небольшую задержку в выполнении потока. Если же при проверке условия second>first оно окажется ложным, тов блоке условного оператора командой Thread. CurrentThread.Abort() завершается выполнение потока. Здесь мыс помощью статического свойства CurrentThread класса Thread получили ссылку на объект потока, в котором выполняется метод, и из этого объекта вызвали метод Abort(), в результате чего поток прекращает работу. Второй поток выполняется похожим образом, но элементы заполняются сначала до конца, и для заполнения используются латинские буквы Глава Оба потока выполняются до тех пор, пока значение переменной first меньше значения переменной second. Значение переменной first увеличивается в процессе заполнения массива, а значение переменной second уменьшается. Параметры программы подобраны так, что наиболее вероятный сценарий следующий в какой-то момент значения переменных и second станут одинаковыми, и потоки завершат свое выполнение. При этом элемент, индекс которого равен совпадающим значениям переменных first и second, останется со старым значением ни один из потоков не присвоит новое значение этому элементу. В следующей программе создается двумерный числовой массив, и этот массив построчно заполняется. Заполнение каждой строки выполняется отдельным потоком. Объектные переменные, через которые реализуются ссылки на потоки, организованы в массив. Это общая идея. Далее рассмотрим способ ее реализации. Интересующий нас программный код представлен в листинге Листинг 6.9. Массив объектных переменных для потоков System; using System.Threading; // Ƚɥɚɜɧɵɣ ɤɥɚɫɫ: class ThreadArrayDemo{ // Ƚɥɚɜɧɵɣ ɦɟɬɨɞ: static void Main(){ // Ⱦɜɭɦɟɪɧɵɣ ɦɚɫɫɢɜ: int[,] nums=new int[6,9]; // Ɇɚɫɫɢɜ ɢɡ ɨɛɴɟɤɬɧɵɯ ɩɟɪɟɦɟɧɧɵɯ ɞɥɹ ɩɨɬɨɤɨɜ: Thread[] t=new Thread[nums.GetLength(0)]; // ɉɟɪɟɛɨɪ ɷɥɟɦɟɧɬɨɜ ɦɚɫɫɢɜɚ: for(int i=0;i Ʌɨɤɚɥɶɧɚɹ ɩɟɪɟɦɟɧɧɚɹ ɞɥɹ ɰɢɤɥɚ: int p=i; // ɋɨɡɞɚɧɢɟ ɨɛɴɟɤɬɚ ɩɨɬɨɤɚ: t[i]=new Thread(()=>{ // ɉɟɪɟɛɨɪ ɷɥɟɦɟɧɬɨɜ ɜ ɫɬɪɨɤɟ Многопоточное программирование // ɰɟɥɨɱɢɫɥɟɧɧɨɝɨ ɦɚɫɫɢɜɚ: for(int j=0;j ɗɥɟɦɟɧɬɭ ɩɪɢɫɜɚɢɜɚɟɬɫɹ ɡɧɚɱɟɧɢɟ: nums[p,j]=(p+1)*(j+1); // ɉɪɢɨɫɬɚɧɨɜɤɚ ɜɵɩɨɥɧɟɧɢɹ ɩɨɬɨɤɚ: Thread.Sleep(100); } }); // Ɂɚɩɭɫɤ ɩɨɬɨɤɚ ɧɚ ɜɵɩɨɥɧɟɧɢɟ: t[i].Start(); } // Ɉɠɢɞɚɧɢɟ ɡɚɜɟɪɲɟɧɢɹ ɞɨɱɟɪɧɢɯ ɩɨɬɨɤɨɜ: for(int i=0;i } // Ɉɬɨɛɪɚɠɟɧɢɟ ɫɨɞɟɪɠɢɦɨɝɨ ɞɜɭɦɟɪɧɨɝɨ ɰɟɥɨɱɢɫɥɟɧɧɨɝɨ // ɦɚɫɫɢɜɚ: for(int i=0;i Ǝ{0,-4}Ǝ,nums[i,j]); } Console.WriteLine(); } Результат выполнения программы представлен ниже: Результат выполнения программы (из листинга 6.9) 1 2 3 4 5 6 7 8 9 2 4 6 8 10 12 14 16 18 3 6 9 12 15 18 21 24 27 Глава 6 316 4 8 12 16 20 24 28 32 36 5 10 15 20 25 30 35 40 45 6 12 18 24 30 36 42 48 Мы создаем двумерный целочисленный массив nums. Еще мы создаем массив t из объектных переменных класса Thread. Размер массива равен количеству строк в массиве nums (вычисляется инструкцией nums.GetLength(0) ). После этого перебираются элементы массива t, и на каждой итерации цикла при фиксированном индексе i выполняются такие команды Объявляется локальная целочисленная переменная p , ив качестве значения ей присваивается текущее значение индекса i . Важно, что переменная объявляется внутри цикла. Поэтому переменная создается (под нее выделяется место в памяти) за каждый цикл заново. По идее, область доступности и время существования этой переменной определяются циклом. Но как мы увидим далее, на эту переменную будет ссылаться метод, реализуемый через лямбда-выражение используется при создании потока. Поэтому переменная p будет существовать, пока работает поток. Но самое важное то, что у каждого потока будет своя переменная p • Создается объект потока. При создании объекта класса Thread (объект потока, ссылка на который записывается в элемент t[i] ) аргументом конструктору передается лямбда-выражение, определяющее метод, выполняемый в потоке. Этим методом запускается оператор цикла, в котором переменная j пробегает значения второго индекса для элементов массива nums , при условии, что первый индекс равен. Командой nums[p,j]=(p+1)*(j+1) соответствующему элементу присваивается значение (то есть значение элемента равно произведению порядкового номера строки на порядковый номер столбца. После присваивания значения элементу целочисленного массива командой Thread.Sleep(100) делается небольшая пауза в выполнении потока После создания объекта потока командой t[i].Start() этот поток запускается на выполнение. Таким образом, после выполнения оператора цикла будут запущены дочерние потоки. Количество дочерних потоков равно количеству строк в двумерном массиве. Ссылки на объекты потоков записаны в массив t. Каждый поток заполняет одну строку двумерного массива Многопоточное программирование ПОДРОБНОСТИ bНесложно заметить, что переменная p фактически дублирует переменную в операторе цикла. Почему нельзя было вместо p использовать Переменная p для каждого цикла своя. Переменная i для всех циклов общая. Если бы в лямбда-выражении вместо переменной использовалась переменная i , то методы, выполняемые враз- ных потоках, ссылались бы на одну и туже переменную i , которая изменяет свое значение в процессе выполнения оператора цикла. Выполнение кода становится непредсказуемым — точнее, предсказуемо заканчивается ошибкой. Далее по плану содержимое массива должно отображаться в консольном окне. Но для этого главный поток должен дождаться окончания выполнения дочерних потоков. Поэтому запускается оператор цикла, в котором перебираются все элементы из массива t. На каждой итерации цикла для данного индекса i проверяется условие t[i].IsAlive. Если оно истинно (поток продолжает работу, то ожидается завершение потока (команда t[i].Join()). После того как все дочерние потоки завершили работу (это означает, что двумерный массив заполнен, с помощью вложенных операторов цикла отображается содержимое двумерного массива nums. { i НАЗ А МЕТКУ Инструкция {0,-4} в строке форматирования в методе означает, что под соответствующий аргумент при отображении в консольном окне отводится 4 позиции, а выравнивание выполняется полевому краю (поскольку второе число отрицательное). В программе из листинга 6.10 также заполняется двумерный целочисленный массив. Нона этот раз для заполнения массива используется специальный метод. При запуске метода ему передается ссылка на массив который следует заполнить, индекс строки (начиная с которой заполняется массив) и ссылка на объект для генерирования случайных чисел. Если указанная для заполнения строка массива не является последней, то метод запускает дочерний поток, в котором вызывается этот же метод, но заполнять он должен следующую строку. Таким образом, в результате вызова метода построчно заполняется двумерный массив начиная стой строки, которая указана при вызове метода, и до последней строки массива включительно. Каждая строка заполняется отдельным потоком Глава Теперь рассмотрим программный код, представленный ниже. Листинг 6.10. Создание потока в потоке System; using System.Threading; // Ƚɥɚɜɧɵɣ ɤɥɚɫɫ: class ThreadInThreadDemo{ // ɋɬɚɬɢɱɟɫɤɢɣ ɦɟɬɨɞ ɞɥɹ ɡɚɩɨɥɧɟɧɢɹ ɞɜɭɦɟɪɧɨɝɨ ɦɚɫɫɢɜɚ // ɫɥɭɱɚɣɧɵɦɢ ɱɢɫɥɚɦɢ: static void fill(int[,] a,int k,Random rnd){ // Ɉɛɴɟɤɬɧɚɹ ɩɟɪɟɦɟɧɧɚɹ ɞɥɹ ɩɨɬɨɤɚ: Thread t=null; // ȿɫɥɢ ɧɟ ɩɨɫɥɟɞɧɹɹ ɫɬɪɨɤɚ: if(k ɋɨɡɞɚɧɢɟ ɨɛɴɟɤɬɚ ɩɨɬɨɤɚ: t=new Thread(()=>{ // ȼɵɡɨɜ ɜ ɩɨɬɨɤɟ ɦɟɬɨɞɚ: fill(a,k+1,rnd); }); // Ɂɚɩɭɫɤ ɩɨɬɨɤɚ ɧɚ ɜɵɩɨɥɧɟɧɢɟ: t.Start(); } // Ɂɚɩɨɥɧɟɧɢɟ ɫɬɪɨɤɢ ɜ ɦɚɫɫɢɜɟ: for(int m=0;m Ɂɧɚɱɟɧɢɟ ɷɥɟɦɟɧɬɚ - ɫɥɭɱɚɣɧɨɟ ɱɢɫɥɨ: a[k,m]=k*10+rnd.Next(10); } // ȿɫɥɢ ɨɛɴɟɤɬ ɩɨɬɨɤɚ ɫɭɳɟɫɬɜɭɟɬ ɢ ɩɨɬɨɤ ɟɳɟ // ɜɵɩɨɥɧɹɟɬɫɹ, ɬɨ ɧɟɨɛɯɨɞɢɦɨ ɞɨɠɞɚɬɶɫɹ // ɡɚɜɟɪɲɟɧɢɹ ɩɨɬɨɤɚ: if(t!=null&&t.IsAlive) t.Join(); } Многопоточное программирование // Ƚɥɚɜɧɵɣ ɦɟɬɨɞ: static void Main(){ // ɋɨɡɞɚɟɬɫɹ ɞɜɭɦɟɪɧɵɣ ɦɚɫɫɢɜ: int[,] nums=new int[6,9]; // Ɉɛɴɟɤɬ ɞɥɹ ɝɟɧɟɪɢɪɨɜɚɧɢɹ ɫɥɭɱɚɣɧɵɯ ɱɢɫɟɥ: Random rnd=new Random(); // Ɂɚɩɨɥɧɟɧɢɟ ɦɚɫɫɢɜɚ: fill(nums,0,rnd); // Ɉɬɨɛɪɚɠɟɧɢɟ ɫɨɞɟɪɠɢɦɨɝɨ ɦɚɫɫɢɜɚ: for(int i=0;i Ǝ{0,-4}Ǝ,nums[i,j]); } Console.WriteLine(); } Результат выполнения программы может быть таким: Результат выполнения программы (из листинга 6.10) 7 0 6 5 4 5 9 7 1 13 15 18 18 16 19 11 10 13 21 20 20 21 28 24 25 23 22 30 34 30 39 36 36 35 38 31 43 49 48 45 49 49 42 40 46 55 59 51 52 59 58 55 54 В главном классе программы, кроме метода Main(), также описывается статический метод fill() стремя аргументами ссылка на двумерный целочисленный массив a, целочисленный индекс строки k (начиная с которой должен заполняться массив) и ссылка rnd на объект класса (с помощью которого будут генерироваться случайные числа. В теле метода объявляется объектная переменная t класса Thread. Глава Ее начальное значение равно null (пустая ссылка. В условном операторе проверяется условие k . Поэтому первая строка (индекс k равен) заполняется случайными числами от 0 до 9, вторая строка индекс равен 1) заполняется случайными числами от 10 до 19, третья строка (индекс k равен 2) заполняется случайными числами от 20 дои так далее. После заполнения строки числами в условном операторе проверяется условие t!=null&&t.IsAlive. Если оно истинно, то вследствие команды t.Join() метод ожидает завершения выполнения потока, после чего работа метода завершается ПОДРОБНО СТ ИВ условном операторе проверяется условие Состоит оно в том, что переменная t содержит непустую ссылку попросту это означает, что дочерний поток был создан и запущен, и при этом поток, на объект которого ссылается переменная t , еще выполняется. Важно то, что использован оператор логического и && , работающий по упрощенной схеме если первое условие t!=null ложно, то второе условие t.IsAlive проверяться не будет. Проверка условия t.IsAlive при ложном условии t!=null приводит к ошибке. В главном методе программы создается двумерный массив nums и объект класса Random для генерирования случайных чисел. Массив заполняется при выполнении команды fill(nums,0,rnd), после чего мы проверяем содержимое массива с помощью вложенных операторов цикла Многопоточное программирование НАЗ А МЕТКУ Метод fill() ожидает завершения потока, если такой запускался при вызове метода. Поэтому завершение выполнения метода fill() означает, что запущенный методом поток также завершен. В дочернем потоке вызывается метод fill() , который может запустить еще один потоки так далее. Нов силу того же обстоятельства метод fill() ожидает завершения дочернего потока, запущенного методом) получается, что каждый дочерний поток ожидает завершения запущенного из него дочернего потока. В следующем примере дочерний поток используется для вычисления суммы бесконечного ряда ∑ n=1 ∞ n 2 /n! (точное значение этой бесконечной суммы равно 2 e ≈ 5,43656, где через e ≈ 2,71828 обозначено основание натурального логарифма. Программа выполняет вычисления следующим образом. В главном методе объявляется переменная с начальным нулевым значением. Создается и запускается дочерний поток с бесконечным циклом. Там последовательно вычисляются слагаемые, и каждое вычисленное слагаемое прибавляется к текущему значению переменной, выделенной для запоминания значения суммы. При этом главный поток на время приостанавливает работу. После паузы из главного потока завершается выполнение дочернего потока, а вычисленное значение для суммы отображается в консольном окне. Программный код, в котором реализован описанный подход, представлен в листинге Листинг 6.11. Вычисление суммы System; using System.Threading; // Ƚɥɚɜɧɵɣ ɤɥɚɫɫ: class SumCalcDemo{ // Ƚɥɚɜɧɵɣ ɦɟɬɨɞ: static void Main(){ // ɉɟɪɟɦɟɧɧɚɹ ɞɥɹ ɡɚɩɢɫɢ ɡɧɚɱɟɧɢɹ ɫɭɦɦɵ: double s=0; Console.WriteLine( Ǝȼɵɱɢɫɥɟɧɢɟ ɫɭɦɦɵƎ); // Ʉɨɧɬɪɨɥɶɧɨɟ ɡɧɚɱɟɧɢɟ ɞɥɹ ɫɭɦɦɵ: Console.WriteLine( ƎɄɨɧɬɪɨɥɶɧɨɟ ɡɧɚɱɟɧɢɟ: {0}Ǝ,2*Math.E); Глава 6 322 // ɋɨɡɞɚɧɢɟ ɨɛɴɟɤɬɚ ɩɨɬɨɤɚ: Thread calc=new Thread(()=>{ // ɂɧɞɟɤɫɧɚɹ ɩɟɪɟɦɟɧɧɚɹ: int n=1; // Ⱦɨɛɚɜɤɚ ɤ ɫɭɦɦɟ: double q=1; // Ȼɟɫɤɨɧɟɱɧɵɣ ɰɢɤɥ: do{ // ɉɪɢɛɚɜɥɟɧɢɟ ɫɥɚɝɚɟɦɨɝɨ ɤ ɫɭɦɦɟ: s+=q; // ɇɨɜɨɟ ɡɧɚɱɟɧɢɟ ɞɥɹ ɢɧɞɟɤɫɧɨɣ ɩɟɪɟɦɟɧɧɨɣ: n++; // ȼɵɱɢɫɥɟɧɢɟ ɞɨɛɚɜɤɢ ɤ ɫɭɦɦɟ // ɞɥɹ ɫɥɟɞɭɸɳɟɝɨ ɰɢɤɥɚ: q=n*n; for(int k=1;k<=n;k++){ q/=k; } // ɉɪɢɨɫɬɚɧɨɜɤɚ ɜ ɜɵɩɨɥɧɟɧɢɢ ɩɨɬɨɤɚ: Thread.Sleep(100); }while(true); }); // Ɂɚɩɭɫɤ ɩɨɬɨɤɚ ɧɚ ɜɵɩɨɥɧɟɧɢɟ: calc.Start(); // ɉɪɢɨɫɬɚɧɨɜɤɚ ɜɵɩɨɥɧɟɧɢɹ ɝɥɚɜɧɨɝɨ ɩɨɬɨɤɚ: Thread.Sleep(1000); // Ɂɚɜɟɪɲɟɧɢɟ ɜɵɩɨɥɧɟɧɢɹ ɞɨɱɟɪɧɟɝɨ ɩɨɬɨɤɚ: calc.Abort(); // Ɉɬɨɛɪɚɠɟɧɢɟ ɪɟɡɭɥɶɬɚɬɚ ɜɵɱɢɫɥɟɧɢɣ: Console.WriteLine( Ǝȼɵɱɢɫɥɟɧɧɨɟ ɡɧɚɱɟɧɢɟ: {0}Ǝ,s); } } Многопоточное программирование 323 Результат выполнения программы может быть таким, как показано ниже: Результат выполнения программы (из листинга 6.11) ȼɵɱɢɫɥɟɧɢɟ ɫɭɦɦɵ Ʉɨɧɬɪɨɥɶɧɨɟ ɡɧɚɱɟɧɢɟ: 5,43656365691809 ȼɵɱɢɫɥɟɧɧɨɟ ɡɧɚɱɟɧɢɟ: Думается, программный код особых комментариев не требует. Отметим лишь несколько обстоятельств. Во-первых, для сравнения приводится точный результат для суммы. Для этого мы использовали статическую константу E (основание натурального логарифма) из класса Math пространство имен System). Во-вторых, слагаемые для суммы вычислялись в лоб, без всяких упрощений и оптимизаций, строго в соответствии с формулой для слагаемого суммы. В-третьих, в дочернем потоке между прибавлением слагаемых к сумме выполнялась небольшая пауза. Делалось это с простой целью чтобы дочерний поток не выполнялся слишком быстро и можно было проследить, как время вычисления суммы влияет на результат. Резюме Друзья! Пацаки! Вот ваш носок, спасибо. из к/ф «Кин-дза-дза» • Многопоточное программирование подразумевает, что некоторые части программы или блоки программного кода выполняются одновременно. Для создания потока (запуска потока на выполнение) необходимо определить программный код, который будет выполняться в потоке, и запустить программный код на выполнение в режиме дочернего потока Создание потока подразумевает создание объекта потока на основе класса Thread из пространства имен System.Threading . Аргументом конструктору класса Thread передается ссылка на экземпляр делегата. Делегат подразумевает работу с методами, которые не имеют аргументов и не возвращают результат. Для запуска потока на выполнение из объекта потока вызывается метод Start() Глава 6 324 • Существует ряд свойств и методов, полезных при работе с потоками. Статический метод Sleep() класса Thread позволяет выполнить временную приостановку в выполнении потока, из которого вызывается метод. Время (в миллисекундах) задержки в выполнении потока указывается в качестве аргумента при вызове метода. Метод Join() используется, если необходимо дождаться выполнения потока, из объекта которого вызван метод. С помощью метода можно завершить выполнение потока. Свойство IsAlive позволяет определить, выполняется ли поток, для объекта которого запрашивается свойство. Свойство Name определяет название потока, а свойство определяет приоритет проекта. Статическое свойство CurrentThread результатом возвращает ссылку на объект потока, из которого запрашивается свойство По умолчанию дочерние потоки являются приоритетными, поэтому при завершении главного потока работа дочерних потоков продолжается. Можно создать фоновый поток, который автоматически завершается при завершении главного потока. Для этого свойству IsBackground объекта потока необходимо присвоить значение по умолчанию это свойство имеет значение false ). • При выполнении потоков, если они обращаются к одному общему ресурсу, выполняется синхронизация потоков. Один из способов решения этой задачи состоит в блокировке объекта, к которому обращаются потоки. Если использовать инструкцию lock , после которой в круглых скобках указать объект, то этот объект будет заблокирован для других потоков, пока выполняется выделенный блок программного кода. |