Таблица 22.9. Символы-заполнители специального формата промежутка времени Символ-заполнитель Назначение d, dd, ddd, dddd, ddddd, dddddd, ddddddd Целые дни. Если указано несколько символов-заполнителей d, то отображается, по крайней мере, указанное количество цифр с начальными нулями, если требуется h, hh Часы (не считая тех, что составляют часть целого дня). Если указано hh, то отображаются две цифры с начальными нулями, если требуется m, mm Минуты (не считая тех, что составляют часть целого часа). Если указано mm, то отображаются две цифры с начальными нулями, если требуется s, ss Секунды (не считая тех, что составляют часть целой минуты). Если указано ss, то отображаются две цифры с начальными нулями, если требуется f, ff, fff, ffff, fffff, ffffff, fffffff Дробные доли секунды. Количество символов-заполнителей f обозначает точность представления, а остальные цифры отбрасываются F, FF, FFF, FFFF, FFFFF, FFFFFF, FFFFFFF Дробные доли секунды. Количество символов-заполнителей F обозначает точность представления, а остальные цифры отбрасываются и конечные нули не отображаются
В приведенной ниже программе демонстрируется форматирование объектов типа TimeSpan на примере отображения времени, которое приблизительно требуется для вывода на экран 1000 целых значений в цикле for. // Отформатировать объект типа TimeSpan. using System; class TimeSpanDemo { static void Main() { DateTime start = DateTime.Now; // Вывести числа от 1 до 1000. for(int i = 1; i <= 1000; i++) { Console.Write(i + " "); if((i % 10) == 0) Console.WriteLine(); } Console.WriteLine(); DateTime end = DateTime.Now; TimeSpan span = end - start; Console.WriteLine("Время выполнения: {0:c}", span); Console.WriteLine("Время выполнения: {0:g}", span); Console.WriteLine("Время выполнения: {0:G}", span); Console.WriteLine("Время выполнения: 0.{0:fff} секунды", span); } }
Выполнение этой программы приводит к следующему результату, который и в этом случае зависит от конкретных настроек языковых и региональных параметров локализации базового программного обеспечения, а также от загрузки системы за дачами и ее быстродействия. 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 Время выполнения: 00:00:00.0140000 Время выполнения: 0:00:00.014 Время выполнения: 0:00:00:00.0140000 Время выполнения: 0.014 секунды Форматирование перечислений
В C# допускается также форматировать значения, определяемые в перечислении. Вообще говоря, значения из перечисления могут отображаться как по имени, так и по значению. Спецификаторы формата перечислений сведены в табл. 22.10. Обратите особое внимание на форматы G и F. Перед перечислениями, которые должны пред ставлять битовые поля, следует указывать атрибут Flags. Как правило, в битовых по лях хранятся значения, обозначающие отдельные двоичные разряды и упорядоченные по степени числа 2. При наличии атрибута Flags имена всех битовых составляющих форматируемого значения, если, конечно, это действительное значение, отображают ся с помощью спецификатора G. А с помощью спецификатора F отображаются имена всех битовых составляющих форматируемого значения, если оно составляется путем логического сложения по ИЛИ двух иди более полей, определяемых в перечислении.
Таблица 22.10. Спецификаторы формата перечислений Спецификатор Назначение D Отображает значение в виде десятичного целого числа d То же, что и D F Отображает имя значения. Если это значение можно создать путем логического сложения по ИЛИ двух или более полей, определенных в перечислении, то данный спецификатор отображает имена всех битовых составляющих заданного значения, причем независимо от того, задан атрибут Flags или нет f То же, что и F G Отображает имя значения. Если перед форматируемым перечислением указывается атрибут Flags, то данный спецификатор отображает имена всех битовых составляющих заданного значения, если, конечно, это допустимое значение g То же, что и G X Отображает значение в виде шестнадцатеричного целого числа. Для отображения как минимум восьми цифр форматируемое значение дополняется (при необходимости) начальными нулями X То же, что и X
В приведенной ниже программе демонстрируется применение спецификаторов формата перечислений. // Отформатировать перечисление. using System; class EnumFmtDemo { enum Direction { North, South, East, West } [Flags] enum Status { Ready=0x1, OffLine=0x2, Waiting=0x4, TransmitOK=0x8, ReceiveOK=0x10, OnLine=0x20 } static void Main() { Direction d = Direction.West; Console.WriteLine("{0:G}", d); Console.WriteLine("{0:F}", d); Console.WriteLine("{0:D}", d); Console.WriteLine("{0:X}", d); Status s = Status.Ready | Status.TransmitOK; Console.WriteLine("{0:G}", s); Console.WriteLine("{0:F}", s); Console.WriteLine("{0:D}", s); Console.WriteLine("{0:X}", s); } }
Ниже приведен результат выполнения этой программы. West West 3 00000003 Ready, TransmitOK Ready, TransmitOK 9 00000009
ГЛАВА 23. Многопоточное программирование. Часть первая: основы
Среди многих замечательных свойств языка С# особое место принадлежит поддержке многопоточного про граммирования. Многопоточная программа состоит из двух иди более частей, выполняемых параллельно. Каж дая часть такой программы называется потоком и опреде ляет отдельный путь выполнения команд. Таким образом, многопоточная обработка является особой формой много задачности. Многопоточное программирование опирается на це лый ряд средств, предусмотренных для этой цели в самом языке С#, а также на классы, определенные в среде .NET Framework. Благодаря встроенной в C# поддержке много поточной обработки сводятся к минимуму или вообще устраняются многие трудности, связанные с организацией многопоточной обработки в других языках программиро вания. Как станет ясно из дальнейшего, поддержка в C# многопоточной обработки четко организована и проста для понимания. С выпуском версии 4.0 в среде .NET Framework появи лись два важных дополнения, имеющих отношение к мно гопоточным приложениям. Первым из них является TPL (Task Parallel Library — Библиотека распараллеливания за дач), а вторым — PLINQ (Parallel LINQ — Параллельный язык интегрированных запросов). Оба дополнения поддер живают параллельное программирование и позволяют ис пользовать преимущества, предоставляемые многопроцес сорными (многоядерными) компьютерами в отношении обработки данных. Кроме того, библиотека TPL упрощает создание многопоточных приложений и управление ими. В силу этого многопоточная обработка, опирающаяся на 23 TPL, рекомендуется теперь как основной подход к разработке многопоточных прило жений. Тем не менее накопленный опыт создания исходной многопоточной подсисте мы по-прежнему имеет значение по целому ряду причин. Во-первых, уже существует немалый объем унаследованного кода, в котором применяется первоначальный под ход к многопоточной обработке. Если приходится работать с таким кодом или сопро вождать его, то нужно знать, как работает исходная многопоточная система. Во-вторых, в коде, опирающемся на TPL, могут по-прежнему использоваться элементы исходной многопоточной системы, и особенно ее средства синхронизации. И в-третьих, несмо тря на то что сама библиотека TPL основывается на абстракции, называемой задачей, она по-прежнему неявно опирается на потоки и потоковые средства, описываемые в этой главе. Поэтому для полного усвоения и применения TPL потребуются твердые знания материала, излагаемого в этой главе. И наконец, следует особо подчеркнуть, что многопоточная обработка представляет собой довольно обширную тему, и поэтому подробное ее изложение выходит за рам ки этой книги. В этой и последующей главах представлен лишь беглый обзор данной темы и демонстрируется ряд основополагающих методик. Следовательно, материал этих глав может служить введением в эту важную тему и основанием для дальнейшего ее самостоятельного изучения. Основы многопоточной обработки
Различают две разновидности многозадачности: на основе процессов и на основе потоков. В связи с этим важно понимать отличия между ними. Процесс фактически представляет собой исполняемую программу. Поэтому многозадачность на основе про цессов — это средство, благодаря которому на компьютере могут параллельно выпол няться две программы и более. Так, многозадачность на основе процессов позволяет одновременно выполнять программы текстового редактора, электронных таблиц и просмотра содержимого в Интернете. При организации многозадачности на основе процессов программа является наименьшей единицей кода, выполнение которой мо жет координировать планировщик задач. Поток представляет собой координируемую единицу исполняемого кода. Своим происхождением этот термин обязан понятию "поток исполнения". При организации многозадачности на основе потоков у каждого процесса должен быть по крайней мере один поток, хотя их может быть и больше. Это означает, что в одной программе одно временно могут решаться две задачи и больше. Например, текст может форматиро ваться в редакторе текста одновременно с его выводом на печать, при условии, что оба эти действия выполняются в двух отдельных потоках. Отличия в многозадачности на основе процессов и потоков могут быть сведены к следующему: многозадачность на основе процессов организуется для параллельного выполнения программ, а многозадачность на основе потоков — для параллельного вы полнения отдельных частей одной программы. Главное преимущество многопоточной обработки заключается в том, что она по зволяет писать программы, которые работают очень эффективно благодаря возможно сти выгодно использовать время простоя, неизбежно возникающее в ходе выполнения большинства программ. Как известно, большинство устройств ввода-вывода, будь то устройства, подключенные к сетевым портам, накопители на дисках или клавиатура, работают намного медленнее, чем центральный процессор (ЦП). Поэтому большую часть своего времени программе приходится ожидать отправки данных на устройство ввода-вывода или приема информации из него. А благодаря многопоточной обра ботке программа может решать какую-нибудь другую задачу во время вынужденно го простоя. Например, в то время как одна часть программы отправляет файл через соединение с Интернетом, другая ее часть может выполнять чтение текстовой инфор мации, вводимой с клавиатуры, а третья — осуществлять буферизацию очередного блока отправляемых данных. Поток может находиться в одном из нескольких состояний. В целом, поток может быть выполняющимся; готовым к выполнению, как только он получит время и ресурсы ЦП; приостановленным, т:е. временно не выполняющимся; возобновленным в дальней шем; заблокированным в ожидании ресурсов для своего выполнения; а также завершен ным, когда его выполнение окончено и не может быть возобновлено. В среде .NET Framework определены две разновидности потоков: приоритетный и фоновый. По умолчанию создаваемый поток автоматически становится приоритет ным, но его можно сделать фоновым. Единственное отличие приоритетных потоков от фоновых заключается в том, что фоновый поток автоматически завершается, если в его процессе остановлены все приоритетные потоки. В связи с организацией многозадачности на основе потоков возникает потребность в особого рода режиме, который называется синхронизацией и позволяет координиро вать выполнение потоков вполне определенным образом. Для такой синхронизации в C# предусмотрена отдельная подсистема, основные средства которой рассматривают ся в этой главе. Все процессы состоят хотя бы из одного потока, который обычно называют основ ным, поскольку именно с него начинается выполнение программы. Следовательно, в основном потоке выполнялись все приведенные ранее примеры программ. Из основ ного потока можно создать другие потоки. В языке C# и среде .NET Framework поддерживаются обе разновидности многоза дачности: на основе процессов и на основе потоков. Поэтому средствами C# можно создавать как процессы, так и потоки, а также управлять и теми и другими. Для того чтобы начать новый процесс, от программирующего требуется совсем немно го усилий, поскольку каждый предыдущий процесс совершенно обособлен от по следующего. Намного более важной оказывается поддержка в C# многопоточной обработки, благодаря которой упрощается написание высокопроизводительных, многопоточных программ на C# по сравнению с некоторыми другими языками про граммирования. Классы, поддерживающие многопоточное программирование, определены в про странстве имен System.Threading. Поэтому любая многопоточная программа на C# включает в себя следующую строку кода. using System.Threading; Класс Thread Система многопоточной обработки основывается на классе Thread, который ин капсулирует поток исполнения. Класс Thread является герметичным, т.е. он не может наследоваться. В классе Thread определен ряд методов и свойств, предназначенных для управления потоками. На протяжении всей этой главы будут рассмотрены наи более часто используемые члены данного класса. Создание и запуск потока Для создания потока достаточно получить экземпляр объекта типа Thread, т.е. класса, определенного в пространстве имен System.Threading. Ниже приведена простейшая форма конструктора класса Thread: public Thread(ThreadStart запуск) где запуск — это имя метода, вызываемого с целью начать выполнение потока, a ThreadStart — делегат, определенный в среде .NET Framework, как показано ниже. public delegate void ThreadStart() Следовательно, метод, указываемый в качестве точки входа в поток, должен иметь возвращаемый тип void и не принимать никаких аргументов. Вновь созданный новый поток не начнет выполняться до тех пор, пока не будет вызван его метод Start(), определяемый в классе Thread. Существуют две формы объявления метода Start(). Ниже приведена одна из них. public 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. Обрати те внимание на вызов статического метода Sleep(), определенного в классе Thread. Этот метод обусловливает приостановление того потока, из которого он был вызван, на определенный период времени, указываемый в миллисекундах. Когда приостанав ливается один поток, может выполняться другой. В данной программе используется следующая форма метода Sleep(): public static void Sleep(int миллисекунд