Многозадачность и многопоточность в языке Перфолента.Net
Каждый программист хочет, что бы его программа была производительной и имела отзывчивый интерфейс пользователя. В современных программах для этого используют параллельный запуск нескольких методов на нескольких ядрах многоядерных процессоров. О том как это делается на языке Перфолента вы и узнаете в этой статье.
Будьте осторожны! Не правильное использование многопоточного программирования может привести к неожиданным проблемам!
С тех пор, как рост частоты процессоров почти остановился, наращивание вычислительной мощности компьютеров стало осуществляться за счет увеличения числа вычислительных ядер в процессоре. Есть еще многопроцессорные компьютеры, но они стоят дороже и продаются в значительно меньших количествах. Но привело ли увеличение числа процессорных ядер к увеличению скорости работы программ? Это вопрос не простой.
На первых порах программы могли выполняться только в одном потоке команд, а значит могли загрузить только одно ядро процессора. На нескольких ядрах процессора могут одновременно выполняться несколько программ. Производительность выполнения нескольких программ повышается пропорционально числу ядер. Прекрасно!
Однако, пользователь в один момент времени чаще всего работает только с одной программой, по мере необходимости переключаясь с одной программы на другую. Вот и выходит, что производительность для пользователя почти такая же, как и у компьютера, имеющего процессор с одним ядром! Не порядок!
Многозадачность, т.е. способность выполнять несколько программ одновременно, появилась в компьютерах значительно раньше, чем появились двух и более ядерные процессоры. Но в рамках одной программы запуск нескольких задач одновременно имел мало смысла, т.к. на одном ядре выполнение каждой из задач замедлялось пропорционально их числу. Пользователю начинало казаться, что компьютер «тормозит».
Появление многоядерных процессоров сделало очень привлекательным запуск некоторых частей одной программы параллельно друг с другом, т.к. в этом случае пользователь реально ощущает, что реакция на его действия происходит быстрее. К примеру, на одном ядре работает часть программы рисующая на мониторе пользовательский интерфейс и реагирующая на действия мыши и клавиатуры, а на других ядрах идет получение и расчет данных необходимых пользователю.
Все бы хорошо, но на практике оказалось, что писать программы, части которых работают параллельно друг с другом, оказалось сложнее, чем обычные программы, состоящие из одного потока команд. Несколько потоков команд начинают конкурировать друг с другом за «ресурсы». Появляются такие не хорошие нюансы, как грязное чтение, потерянная запись и взаимная блокировка. Программисту необходимо заботиться о синхронизации действий разных потоков команд, что бы они не портили данные друг друга и поставляли результат своих действий вовремя.
Лирическое отступление: Классическая история. Мистер Икс проверил свою семейную кредитную карту и понял, что имеющихся средств достаточно для покупки нового 100 дюймового телевизора, о котором они так давно мечтали с женой. Предвкушая удовольствие от будущего сюрприза жене, он сел на свою новенькую машину и поехал в любимый магазин электроники. А в это время, его любимая жена, миссис Икс, купила в одном интернет-бюро путешествий путевки на семейный отдых у моря. Какой же сюрприз ожидал её мужа, когда кассир сообщил ему, что на семейной карте нет средств.
Имеет ли эта история отношение к многопоточному программированию? Ещё как! Мистер и миссис Икс одновременно и независимо друг от друга распоряжались общим ресурсом, не используя при этом средств синхронизации своих действий между собой! То же самое могут делать и программные потоки в компьютере, произвольно изменяя общие данные, что вполне может привести к фатальным последствиям.
Все современные языки программирования предлагают программисту разнообразные способы запуска нескольких потоков команд в рамках одной программы и разнообразные средства для решения задач синхронизации. Не забывайте о синхронизации!
Язык программирования Перфолента.Net так же имеет богатый инструментарий для создания многопоточных программ, использующих все имеющиеся ядра процессора. Тема многозадачности и многопоточности очень обширна, поэтому в этой статье мы рассмотрим только базовые возможности и конструкции языка. Любому программисту стоит прочесть побольше книг на эту тему и написать несколько многопоточных программ-примеров для того, чтобы разобраться с особенностями их архитектуры.
Содержание
Терминология.
Договоримся о некоторых используемых в дальнейшем терминах.
Процесс – это программа, запущенная пользователем или другой программой. Обычно процесс можно ассоциировать с запускаемым файлом, имеющим расширение EXE. Работающий процесс изолирован от других процессов и не может напрямую обращаться к их данным. Для общения процессов между собой, они должны использовать методы, предоставляемые операционной системой для меж процессного взаимодействия.
Программный поток – это отдельный поток команд, запущенный в рамках процесса, однако имеющий возможность выполняться на разных ядрах с основным потоком и другими программными потоками, запущенными в рамках этого процесса. Обычно программный поток можно ассоциировать с процедурой какого-либо объекта программы.
Основной программный поток – это программный поток в рамках процесса, который был запущен первым. Изначально является потоком переднего плана. Обычно, время жизни основного потока равно времени жизни программы, но в случае запуска дополнительных программных потоков переднего плана это может оказаться не так.
Программный поток переднего плана – это программный поток, который имеет право на самостоятельное выполнение. Процесс не будет завершен, пока выполняется хотя бы один поток переднего плана.
Фоновый программный поток – это программный поток, который будет автоматически прерван операционной системой, если в рамках процесса не осталось ни одного потока переднего плана.
Пул программных потоков – это несколько программных потоков, которые могут быть использованы множество раз и которые поддерживаются и распределяются средой выполнения. Создание и удаление программного потока — это не дешевая операция для операционной системы с точки зрения производительности, поэтому повторное использование завершившихся потоков повышает общую производительность программы.
Задание – это объект, который получает программный поток из пула и позволяет контролировать работу этого потока.
На уровне операционной системы.
Многозадачностью и многопоточностью управляет операционная система. Именно она распределяет время процессорных ядер между желающими выполняться потоками команд.
Операционная система Windows состоит из разных компонентов, часть из которых создана на основе технологии COM, например, диалоговые окна открытия и сохранения файлов, выбора шрифтов и цвета, диалог выбора принтера, формы Windows Forms и т.д., используют компоненты построенные по этой технологии. Технология COM требует определения, в каком окружении выполняется компонент, в однопоточной модели или в многопоточной модели. Поэтому в программе на языке Перфолента, использующей какие-либо COM компоненты или соответствующие возможности операционной системы Windows, требуется правильно задать потоковую модель.
Атрибут определения потоковой модели.
Для задания потоковой модели используются атрибуты ОднопоточнаяМодель и МногопоточнаяМодель, применяемые к методу Старт модуля Программа.
По умолчанию, если атрибут не задан, используется однопоточная модель.
Программа ТестАтрибута &МногопоточнаяМодель //задали модель основного программного потока Процедура Старт КонецПроцедуры КонецПрограммы
Умнику на заметку: Модель определяется для компонентов COM используемых в программе (в том числе тех, которые вызываются операционной системой во время работы программы). Определяемая модель не относится к управляемому коду программы. Это значит, что даже указав для программы однопоточную модель, вы по-прежнему можете использовать многопоточность при вызовах управляемого кода. Однако помните, что в таком случае попытка вызвать однопоточный COM компонент возможно приведет к краху программы или не правильной её работе.
На уровне Net Framework.
Язык программирования Перфолента основан на технологии .Net и поэтому все основные возможности языка по работе с многопоточностью определяются возможностями Net Framework и полностью основаны на них.
В этой статье мы рассмотрим основные классы, позволяющие создавать процессы и программные потоки и управлять ими.
Все эти классы находятся в стандартной библиотеке, т.е. в программе должна присутствовать директива #ИспользоватьСтандартнуюБиблиотеку.
Класс Процесс.
Класс Процесс – предназначен для создания и запуска нового процесса. Возможно ожидание завершения запущенного процесса, определение кода его завершения, чтения потоков вывода и ошибок, а также принудительное прерывание работы процесса в случае необходимости. Класс имеет множество свойств и методов с описанием которых можно ознакомиться в файле справки и в примерах, имеющихся в дистрибутиве поставки. Здесь мы рассмотрим только самый простой пример.
//запустим Блокнот Использовать Проц = Новый Процесс.Старт("Notepad.exe") //будем наблюдать за процессом 10 секунд с паузой по 2 секунды Для Инд=1 По 5 //если блокнот еще не закрыл пользователь, //то прочитаем объем используемой физической памяти Если НЕ Проц.Завершен //обновим информацию о процессе перед чтением свойств Проц.Обновить //выведем информацию о работе процесса Сообщить($"Выделено физической памяти: {Проц.ВыделеноПамяти} байт."); Сообщить($"Выделено виртуальной памяти: {Проц.ВыделеноВиртуальнойПамяти} байт."); Сообщить($"Время процессора: {Проц.ОбщееВремяПроцессора} секунд."); //подождем 2 секунды ЭтаПрограмма.Пауза(2000) Иначе Прервать КонецЕсли КонецЦикла //закроем главное окно Блокнота, но оно может и не закрыться, если пользователь не захочет Проц.ЗакрытьГлавноеОкно //освободим процесс Блокнота от нашего контроля Проц.Освободить //здесь оператор использовать сам вызовет метод Завершитель на захваченной переменной Проц КонецИспользовать
Класс ПрограммныйПоток.
Класс ПрограммныйПоток – предназначен для непосредственного создания и запуска нового программного потока. Позволяет задать любые доступные свойства программного потока, такие как имя потока, признак того, что поток является фоновым и т.д., а также управлять взаимодействием нескольких программных потоков обеспечивая синхронизацию их совместной работы. Класс имеет множество свойств и методов с описанием которых можно ознакомиться в файле справки и в примерах, имеющихся в дистрибутиве поставки. Здесь мы рассмотрим только самый простой пример.
//создадим программный поток П1 = Новый ПрограммныйПоток(ПолучитьДелегат(,МояПроцедура,"ПроцедураБезПараметров")) Сообщить("Начальное состояние: "+П1.Состояние+" "+П1.СостояниеСтр); Сообщить("Начальный приоритет: "+П1.Приоритет+" "+П1.ПриоритетСтр); //запустим выполнение указанной при создании потока процедуры П1.Старт(); //заснем на 3 секунды //во время паузы будет работать только поток ЭтаПрограмма.Пауза(3000); //прерываем поток... в процедуре возникнет исключение... П1.Прервать(); //проверим состояние потока после прерывания Сообщить("Состояние потока: "+П1.Состояние+" "+П1.СостояниеСтр);
//… //где-то ниже по коду Процедура МояПроцедура Попытка //в этом бесконечном цикле программный поток делает свою полезную работу Цикл ВыводСтроки "Работает... " ЭтаПрограмма.Пауза(500) КонецЦикла Исключение Ош //программное прерывание потока должно вызвать исключение!!!! Сообщить("Ошибка в процедуре потока: "+Ош.ОписаниеОшибки) КонецПопытки КонецПроцедуры
Модуль МенеджерФоновыхЗаданий и класс ФоновоеЗадание.
Модуль МенеджерФоновыхЗаданий и класс ФоновоеЗадание – предназначены для запуска на выполнение множества фоновых заданий, для которых программные потоки выбираются из пула. Эти классы имеют функциональность аналогичную одноименным объектам платформы 1С, однако не являются их точной копией из-за различия языков и архитектуры среды выполнения. Подробности реализации свойств и методов этих классов смотрите в файле справки. Одним из основных отличий является необходимость получения делегата выполняемого метода, вместо указания имени метода строкой.
//создадим и заполним массив параметров МассивПараметров = Новый Массив{1, 2} //тип делегата задавать обязательно, т.к. именно от делегата зависит число параметров Задача = МенеджерФоновыхЗаданий.Выполнить( ПолучитьДелегат(, ВыполнитьДолгийЦикл, "ДействиеПроц<КонтроллерЗадания,Массив>"), МассивПараметров, "Это Ключ задания", "Это Наименование задания") //пусть задание поработает 3 секунды ЭтаПрограмма.Пауза(3000) //отменим задание и подождем его завершения Задача.Отменить Задача.ОжидатьЗавершенияВыполнения //проверим состояние задания ВыводСтроки "Состояние задания: "+Задача.СостояниеСтр //... //где-то ниже по коду Процедура ВыполнитьДолгийЦикл(Контроллер тип КонтроллерЗадания, МассивПараметров тип Массив) ВыводСтроки "Попали в процедуру... Параметры = "+МассивПараметров.Представление Цикл //будем использовать контроллер для отмены Если Контроллер.ОтменаЗапрошена Возврат КонецЕсли //выполняем основную работу ЭтаПрограмма.Пауза(1000) Сигнал КонецЦикла КонецПроцедуры
Класс ПараллельныеДействия.
Класс ПараллельныеДействия – предназначен для запуска нескольких параллельно выполняющихся программных потоков с ожиданием выполнения всех из них. Это может быть набор различных процедур, запускаемых параллельно, а может быть одна процедура, запускаемая одновременно в нескольких потоках как имитация цикла, итерации которого выполняются параллельно.
Метод Выполнить позволяет запустить на параллельное выполнение несколько процедур без параметров. Управление из метода вернется тогда, когда завершится выполнение всех запущенных процедур. Это значит, что время выполнения метода зависит от самой медленной из запущенных процедур.
Процедура Старт // 2 процедуры будут по возможности выполняться параллельно...
ПараллельныеДействия.Выполнить( ПолучитьДелегат(,СкопироватьФайлы,"ДействиеПроц"), ПолучитьДелегат(,ОтображатьСостояниеКопирования,"ДействиеПроц") ) ВыводСтроки "Все процедуры выполнились..."
КонецПроцедуры
Процедура СкопироватьФайлы() //код копирования файлов КонецПроцедуры
Процедура ОтображатьСостояниеКопирования() //код вывода состояния на экран КонецПроцедуры
Метод Для позволяет запустить аналог цикла Для, итерации которого по возможности выполняются параллельно. Почему по возможности? Потому, что итераций цикла может быть намного больше, чем доступно ядер процессора. А это значит, что чем больше ядер имеет процессор, тем быстрее закончится цикл. Код, реализующий итерацию цикла, должен быть не зависимым от других итераций, т.к. порядок их выполнения в общем случае не определен.
Процедура Старт ПараллельныеДействия.Для(1, 5, ПолучитьДелегат(, ТелоЦикла, "ДействиеПроц<Целое>"))
КонецПроцедуры
Процедура ТелоЦикла (Инд тип Целое) //код тела цикла для итерации Инд КонецПроцедуры
Метод ДляКаждого позволяет запустить аналог цикла Для Каждого, итерации которого по возможности выполняются параллельно. По возможности, потому, что итераций цикла может быть намного больше, чем доступно ядер процессора. А это значит, что чем больше ядер имеет процессор, тем быстрее закончится цикл. Код, реализующий итерацию цикла, должен быть не зависимым от других итераций, т.к. порядок их выполнения в общем случае не определен.
Процедура Старт мас = Новый Массив<Целое>{1, 2, 3, 4, 5}
ПараллельныеДействия.ДляКаждого<Целое>(мас, ПолучитьДелегат(, ТелоЦикла, "ДействиеПроц<Целое>"))
КонецПроцедуры
Процедура ТелоЦикла (Значение тип Целое) //код тела цикла для итерации, в которую попадают значения из коллекции КонецПроцедуры
В данном примере в качестве коллекции использовался массив целых чисел, поэтому и цикл был определен, как цикл по элементам типа Целое: ДляКаждого<Целое>. Если бы в массиве были элементы типа ТекстовыйДокумент, то и метод имел бы такой же тип: ДляКаждого<ТекстовыйДокумент>. То же самое относится к типу параметра Значение процедуры тела цикла и к типу делегата, который выглядел бы как: "ДействиеПроц<ТекстовыйДокумент>".
Это описание методов Для и ДляКаждого не является исчерпывающим. Для получения более подробной информации следует изучить справочные сведения по ним. Например, использование класса КонтроллерЦикла, позволяет отменять работу одной или всех итераций цикла, узнавать результат завершения цикла и устанавливать дополнительные параметры, влияющие на выполнение цикла.
Класс Таймер.
Класс Таймер – реализует периодический многопоточный вызов обработчиков события через указанный интервал времени. Каждый вызов обработчика события происходит в отдельном программном потоке из пула потоков.
Поле Ном тип Целое = 0 Поле ОбъектБлокировки тип Объект = Новый Объект
Процедура Старт
Тм1 = Новый Таймер //используем событие Тик2, чтобы знать, какой таймер его вызвал ДобавитьОбработчик Тм1.Тик2, ПолучитьДелегат(, Тик) Тм1.Интервал = 1000 Тм1.Тэг = "Таймер 1" Тм1.Старт
Тм2 = Новый Таймер //используем событие Тик2, чтобы знать, какой таймер его вызвал ДобавитьОбработчик Тм2.Тик2, ПолучитьДелегат(, Тик) Тм2.Интервал = 2000 Тм2.Тэг = "Таймер 2" Тм2.Старт
//отследим 20 тиков Пока Ном < 20 ЭтаПрограмма.Пауза(100) КонецЦикла
КонецПроцедуры
//Обработчик события таймера
Процедура Тик(Тм тип Таймер) //установим блокировку, т.к. к полю Ном могут одновременно обращаться два программных потока разных таймеров Блокировка ОбъектБлокировки Ном++ ВыводСтроки "Тик "+Ном+" время старта "+Тм.ВремяСтарта+" "+Тм.Тэг+" в потоке "+ПрограммныйПоток.ИдентификаторТекущегоПотока КонецБлокировки КонецПроцедуры
Метод ПодключитьОбработчикОжидания.
Метод ПодключитьОбработчикОжидания глобального контекста – подключает процедуру без параметров, на которую указывает переданный делегат, к таймеру, который через указанный интервал времени делает вызов процедуры в отдельном программном потоке из пула потоков. В отличие от платформы 1С, где вызов обработчика, подключенного этим методом, будет осуществляться только в тот момент, когда программа не выполняет никаких действий, в языке Перфолента вызов обработчика будет осуществляться даже тогда, когда текущий программный поток приостановлен. Внутренне эти методы используют класс Таймер.
//Подключим процедуру Бип, которая будет вызываться каждую секунду ПодключитьОбработчикОжидания(ПолучитьДелегат(, Бип), 1)
//Когда вызовы процедуры Бип больше не нужны, отключим её ОтключитьОбработчикОжидания(ПолучитьДелегат(, Бип))
//Где-то ниже по коду Процедура Бип ВыводСтроки "Бип! Прошла 1 секунда! Вызов в программном потоке "+ПрограммныйПоток.ИдентификаторТекущегоПотока КонецПроцедуры
Методы НачатьХХХ.
У многих классов стандартной библиотеки есть методы, начинающиеся со слова Начать, которые запускают асинхронное выполнение каких-либо длительных операций. Эти операции выполняются в отдельном программном потоке из пула потоков.
Примером таких классов могут быть TCPСервер, позволяющий организовать многопоточную обработку входящих запросов при помощи метода НачатьЦиклОбработкиСоединений, либо классы ТекстовыйДокумент и ДвоичныеДанные, позволяющие асинхронно записать свое содержимое на диск при помощи метода НачатьЗапись.
//создадим и запустим сервер, на порту 5005 Сервер = Новый TCPСервер(5005) Сервер.Старт
//создадим объект ОписаниеОповещения ДопПараметр = "Текст доп. параметра" //а можно сюда, например, Структуру с кучей параметров передать ОписаниеОповещения = Новый ОписаниеОповещения(ПолучитьДелегат(, ОбработатьЗапрос, "ПроцедураСПараметрами3"), ДопПараметр, ПолучитьДелегат(, ОбработатьОшибку, "ПроцедураСПараметрами2"))
//этот метод запускает в асинхронном режиме цикл принятия соединения //обработка соединений и ошибок будет происходить в процедурах, указанных в объекте ОписаниеОповещения Сервер.НачатьЦиклОбработкиСоединений(ОписаниеОповещения)
Если при написании кода вы понимаете, что вызов необходимого метода объекта может на длительное время приостановить выполнение программы, посмотрите в документации, нет ли у этого объекта аналогичного метода, имя которого начинается со слова Начать. Аналогично поступайте при написании собственных классов. Для методов, которые могут выполняться долго, создавайте асинхронный вариант, давая ему имя вида НачатьХХХ.
Деструкторы выполняются в отдельном потоке.
Т.к. деструкторы объектов выполняются в отдельном потоке, то необходимо соблюдать в их коде правила многопоточного обращения к общим данным.
Деструктор
Блокировка ОбщийОбъектБлокировки ОбщееЧислоОбъектов-- //эту переменную могут использовать другие потоки КонецБлокировки
КонецДеструктора
Умнику на заметку: При программировании на языке Перфолента деструкторы могут понадобиться очень редко, т.к. сборщик мусора сам хорошо справляется с уничтожением объектов и очисткой занимаемой ими памяти. Из-за этого деструкторы часто называют финализерами (или финализаторами), чтобы подчеркнуть их необязательность. Используйте деструкторы только тогда, когда необходимо убедиться, что объект действительно освободил все занятые им не управляемые ресурсы.
О синхронизации.
Оператор Блокировка.
Оператор Блокировка является блочным оператором и защищает блок кода от одновременного входа в него нескольких программных потоков. Обычно приходится защищать те блоки операторов, в которых изменяются данные доступные любому потоку. Наиболее опасной можно считать операцию, при которой последовательно происходят чтение, изменение и запись данных в доступный нескольким программным потокам ресурс (переменную, поле или свойство объекта).
Без блокировки одновременного доступа нескольких потоков к общим данным возможны такие проблемы:
- Грязное чтение – когда прочитанные данные либо не соответствуют тому, что ожидалось, либо являются полностью испорченными;
- Потерянная запись – когда только что записанные данные перезаписаны другим потоком, но текущий поток не подозревает об этом;
- Грязная запись – когда в результате одновременной записи данные оказываются необратимо испорчены;
Однако, блокировку также надо применять осторожно, не допуская взаимной блокировки потоков.
- Взаимная блокировка – это ситуация, когда два или более программных потока бесконечно ожидают освобождения ресурсов, заблокированных друг другом.
Рассмотрим пример использования оператора Блокировка:
Допустим, что у нашего объекта есть поле, к которому обращаются несколько программных потоков:
Поле ОбщееЧислоДетей тип Целое = 0
И есть метод, в котором эти обращения происходят. Мы знаем, что метод вызывается из разных программных потоков в не предсказуемое время и в не предсказуемом порядке.
Процедура ДобавитьДетейИПроверитьПревышение(ЧислоНовыхДетей тип Целое) Блокировка ОбъектБлокировки //нам необходим объект, обращения к которому отслеживаются Было = ОбщееЧислоДетей //прочитали значение Стало = Было + ЧислоНовыхДетей //изменили значение ОбщееЧислоДетей = Стало //записали новое значение Если Стало > 100 //тут мы уверены, что на момент проверки ОбщееЧислоДетей не изменилось Сообщить("Превышение!") КонецЕсли КонецБлокировки КонецПроцедуры
Как видим, мы защитили операции с полем ОбщееЧислоДетей от одновременного изменения несколькими программными потоками с помощь блочного оператора Блокировка.
Однако, обратим особое внимание на то, что нам понадобился объект блокировки ОбъектБлокировки, который используется для выявления обращений к данному участку кода программы из разных программных потоков и блокирования одновременного выполнения защищаемого кода разными потоками.
Мы можем использовать ссылку на любой объект в качестве объекта блокировки, однако необходимо учитывать особенности доступа к этому объекту из защищаемого кода и другие особенности реализации оператора Блокировка:
- Объект блокировки должен быть членом класса, которому принадлежат защищаемый член и метод содержащий оператор Блокировка;
- Если защищаемый член является общим для класса (отмечен атрибутом ОбщийДляКласса), то и объект блокировки должен быть общим для класса;
- Если защищаемый член принадлежит экземпляру объекта, то и объект блокировки должен принадлежать экземпляру объекта;
- Объект блокировки не может иметь значение Неопределенно;
- Значение объекта блокировки внутри тела оператора Блокировка изменять нельзя;
- Не следует менять значение объекта блокировки во время работы нескольких программных потоков, которые могут сделать обращение к нему.
- Не следует использовать в качестве объекта блокировки ключевое слово ЭтотОбъект, что ведет к взаимным блокировкам;
- Не следует использовать в качестве объекта блокировки значения типов Тип и Строка, что ведет к взаимным блокировкам;
- Не допускается переход внутрь тела оператора Блокировка с помощью оператора Перейти или другими способами;
- Покинуть тело оператора Блокировка можно любым доступным способом, в том числе вызовом исключения;
В нашем случае защищаемое поле принадлежит экземпляру класса, поэтому создадим аналогичное поле для объекта блокировки и сразу присвоим ему новый объект:
Поле ОбъектБлокировки тип Объект = Новый Объект
Оператор Блокировка решает большинство задач синхронизации возникающих при написании программ, однако платформа Net Framework поддерживает множество других способов синхронизации, изучить которые было бы крайне полезно для любого программиста. Большинство этих способов реализовано в пространстве имен System.Threading.
Продолжение следует ...
В этой статье описаны далеко не все возможности языка программирования Перфолента в области многозадачности и многопоточных вычислений, а только те, которые полностью реализованы, протестированы и готовы к применению. Возможности, находящиеся в разработке, будут описаны в следующих статьях.