Установка и начала использования библиотеки MPI
Установка библиотеки MPICH2 под Windows
Для того чтобы начать работу с библиотекой MPICH2, необходимо скачать совместимую с используемой операционной системой версию продукта здесь. Для ОС Windows – это установочный пакет формата MSI, поэтому инсталляция библиотеки проходит стандартным образом. Важно, что установку в этом случае надо проводить для всех пользователей системы.
Теперь необходимо добавить два основных исполняемых файла библиотеки mpiexec.exe и smpd.exe в список правил брандмауэра. Это необходимо, поскольку, при организации кластера используется сеть и доступ к каждому узлу сети должен быть разрешен для компонентов MPI. Конкретные настройки зависят от типа используемого брандмауэра.
На следующем этапе необходимо создать пользователя в системе, от имени которого будут исполняться компоненты библиотеки. Важно, что данный пользователь обязательно должен иметь свой пароль, т.к. MPICH2 не позволяет зарегистрировать исполняющего пользователя с пустым паролем. Регистрация осуществляется с помощью компонента wmpiregister.exe, находящегося в папке bin библиотеки и имеющего понятный оконный интерфейс:
Установка практически завершена. Осталось проверить правильность всех сделанных настроек. Для этой цели в папке examples есть примеры программ с параллельными алгоритмами. Для запуска можно использовать компонент wmpiexec.exe, который использует оконный интерфейс, который не нуждается в дополнительных комментариях.
Проверка работоспособности
В качестве IDE для разработки используется MVS 2005. Напишем программу, которая будет привествовать этот мир от имени разных новорожденных процессов процессов. Для этого используется пустой проект (empty project) с изменением некоторых настроек проекта.
#include «stdio.h»
#include «mpi.h»
#include «stdlib.h»
#include «math.h»
int ProcNum;
int ProcRank;
int main( int argc, char *argv[]) <
MPI_Status status;
MPI_Init(&argc, &argv);
MPI_Comm_rank(MPI_COMM_WORLD, &ProcRank);
MPI_Comm_size(MPI_COMM_WORLD, &ProcNum);
Компилируем его и запускаем полученный бинарник через wmpiexec на 4 процессах.
Как видим, мир поприветствовал каждый рождённый процесс.
Я умышленно приводил код без каких либо комментариев, а только с целью демонстрации работы библиотеки. В будущем я планирую посвятить статью списку функций MPI. Также интересной является тема избыточной параллельности и, вообще, вопрос когда стоит распарарллеливать приложение, а когда нет. Эти исследования также будут приведены позже. Поэтому у меня возник основной вопрос — сфера применимости в web технологиях? Пока мой интерес к параллельным вычислениям вызван другой проблемой: ускорение моделирования разного рода процессов.
Обмен данными с использованием MPI. Работа с библиотекой MPI на примере Intel® MPI Library
В этом посте мы расскажем об организации обмена данными с помощью MPI на примере библиотеки Intel MPI Library. Думаем, что эта информация будет интересна любому, кто хочет познакомиться с областью параллельных высокопроизводительных вычислений на практике.
Мы приведем краткое описание того, как организован обмен данными в параллельных приложениях на основе MPI, а также ссылки на внешние источники с более подробным описанием. В практической части вы найдете описание всех этапов разработки демонстрационного MPI-приложения «Hello World», начиная с настройки необходимого окружения и заканчивая запуском самой программы.
MPI (Message Passing Interface)
MPI — интерфейс передачи сообщений между процессами, выполняющими одну задачу. Он предназначен, в первую очередь, для систем с распределенной памятью (MPP) в отличие от, например, OpenMP. Распределенная (кластерная) система, как правило, представляет собой набор вычислительных узлов, соединенных высокопроизводительными каналами связи (например, InfiniBand).
MPI является наиболее распространенным стандартом интерфейса передачи данных в параллельном программировании. Стандартизацией MPI занимается MPI Forum. Существуют реализации MPI под большинство современных платформ, операционных систем и языков. MPI широко применяется при решении различных задач вычислительной физики, фармацевтики, материаловедения, генетики и других областей знаний.
Параллельная программа с точки зрения MPI — это набор процессов, запущенных на разных вычислительных узлах. Каждый процесс порождается на основе одного и того же программного кода.
Основная операция в MPI — это передача сообщений. В MPI реализованы практически все основные коммуникационные шаблоны: двухточечные (point-to-point), коллективные (collective) и односторонние (one-sided).
Работа с MPI
Рассмотрим на живом примере, как устроена типичная MPI-программа. В качестве демонстрационного приложения возьмем исходный код примера, поставляемого с библиотекой Intel MPI Library. Прежде чем запустить нашу первую MPI-программу, необходимо подготовить и настроить рабочую среду для экспериментов.
Настройка кластерного окружения
Для экспериментов нам понадобится пара вычислительный узлов (желательно со схожими характеристиками). Если под руками нет двух серверов, всегда можно воспользоваться cloud-сервисами.
Для демонстрации я выбрал сервис Amazon Elastic Compute Cloud (Amazon EC2). Новым пользователям Amazon предоставляет пробный год бесплатного использования серверами начального уровня.
Работа с Amazon EC2 интуитивно понятна. В случае возникновения вопросов, можно обратиться к подробной документации (на англ.). При желании можно использовать любой другой аналогичный сервис.
Создаем два рабочих виртуальных сервера. В консоли управления выбираем EC2 Virtual Servers in the Cloud, затем Launch Instance (под «Instance» подразумевается экземпляр виртуального сервера).
Следующим шагом выбираем операционную систему. Intel MPI Library поддерживает как Linux, так и Windows. Для первого знакомства с MPI выберем OC Linux. Выбираем Red Hat Enterprise Linux 6.6 64-bit или SLES11.3/12.0.
Выбираем Instance Type (тип сервера). Для экспериментов нам подойдет t2.micro (1 vCPUs, 2.5 GHz, Intel Xeon processor family, 1 GiB оперативной памяти). Как недавно зарегистрировавшемуся пользователю, мне такой тип можно было использовать бесплатно — пометка «Free tier eligible». Задаем Number of instances: 2 (количество виртуальных серверов).
После того, как сервис предложит нам запустить Launch Instances (настроенные виртуальные сервера), сохраняем SSH-ключи, которые понадобятся для связи с виртуальными серверами извне. Состояние виртуальных серверов и IP адреса для связи с серверами локального компьютера можно отслеживать в консоли управления.
Важный момент: в настройках Network & Security / Security Groups необходимо создать правило, которым мы откроем порты для TCP соединений, — это нужно для менеджера MPI-процессов. Правило может выглядеть так:
Type: Custom TCP Rule
Protocol: TCP
Port Range: 1024-65535
Source: 0.0.0.0/0
В целях безопасности можно задать и более строгое правило, но для нашего демонстрационного примера достаточно этого.
Здесь можно прочитать инструкции о том, как связаться с виртуальными серверами с локального компьютера (на англ.).
Для связи с рабочими серверами c компьютера на Windows я использовал Putty, для передачи файлов — WinSCP. Здесь можно прочитать инструкции по их настройке для работы с сервисами Amazon (на англ.).
Настройка MPI библиотеки
300МБ). При желании можно использовать другие реализации MPI, например, MPICH. Последняя доступная версия Intel MPI Library на момент написания статьи 5.0.3.048, ее и возьмем для экспериментов.
Установим Intel MPI Library, следуя инструкциям встроенного инсталлятора (могут потребоваться привилегии суперпользователя).
Выполним установку на каждом из хостов с идентичным установочным путем на обоих узлах. Более стандартным способом развертывания MPI является установка в сетевое хранилище, доступное на каждом из рабочих узлов, но описание настройки подобного хранилища выходит за рамки статьи, поэтому ограничимся более простым вариантом.
Для компиляции демонстрационной MPI-программы воспользуемся GNU C компилятором (gcc).
В стандартном наборе программ RHEL образа от Amazon его нет, поэтому необходимо его установить:
$ scp test.exe ip-172-31-47-24:/home/ec2-user/intel/impi/5.0.3.048/test/
Утилита ‘mpirun’ — это программа из состава Intel MPI Library, предназначенная для запуска MPI-приложений. Это своего рода «запускальщик». Именно эта программа отвечает за запуск экземляра MPI-программы на каждом из узлов, перечисленных в ее аргументах.
Касательно опций, ‘-ppn’ — количество запускаемых процессов на каждый узел, ‘-n’ — общее число запускаемых процессов, ‘-hosts’ — список узлов, где будет запущено указанное приложение, последний аргумент — путь к исполняемому файлу (это может быть и приложение без MPI).
В нашем примере с запуском утилиты hostname мы должны получить ее вывод (название вычислительного узла) с обоих виртуальных серверов, тогда можно утверждать, что менеджер MPI-процессов работает корректно.
«Hello World» с использованием MPI
В качестве демонстрационного MPI-приложения мы взяли test.c из стандартного набора примеров Intel MPI Library.
Демонстрационное MPI-приложение cобирает с каждого из параллельно запущенных MPI-процессов некоторую информацию о процессе и вычислительном узле, на котором он запущен, и распечатывает эту информацию на головном узле.
Рассмотрим подробнее основные составляющие типичной MPI-программы.
Подключение заголовочного файла mpi.h, который содержит объявления основных MPI-функций и констант.
Если для компиляции нашего приложения мы используем специальные скрипты из состава Intel MPI Library (mpicc, mpiicc и т.д.), то путь до mpi.h прописывается автоматически. В противном случае, путь до папки include придется задать при компиляции.
Вызов MPI_Init() необходим для инициализации среды исполнения MPI-программы. После этого вызова можно использовать остальные MPI-функции.
Последним вызовом в MPI программе является MPI_Finalize(). В случае успешного завершения MPI-программы каждый из запущенных MPI-процессов делает вызов MPI_Finalize(), в котором осуществляется чистка внутренних MPI-ресурсов. Вызов любой MPI-функции после MPI_Finalize() недопустим.
Чтобы описать остальные части нашей MPI-программы необходимо рассмотреть основные термины используемые в MPI-программировании.
MPI-программа — это набор процессов, которые могут посылать друг другу сообщения посредством различных MPI-функций. Каждый процесс имеет специальный идентификатор — ранг (rank). Ранг процесса может использоваться в различных операциях посылки MPI-сообщений, например, ранг можно указать в качестве идентификатора получателя сообщения.
Кроме того в MPI существуют специальные объекты, называемые коммуникаторами (communicator), описывающие группы процессов. Каждый процесс в рамках одного коммуникатора имеет уникальный ранг. Один и тот же процесс может относиться к разным коммуникаторам и, соответственно, может иметь разные ранги в рамках разных коммуникаторов. Каждая операция пересылки данных в MPI должна выполняться в рамках какого-то коммуникатора. По умолчанию всегда создается коммуникатор MPI_COMM_WORLD, в который входят все имеющиеся процессы.
MPI_Comm_size() вызов запишет в переменную size (размер) текущего MPI_COMM_WORLD коммуникатора (общее количество процессов, которое мы указали с mpirun опцией ‘-n’).
MPI_Comm_rank() запишет в переменную rank (ранг) текущего MPI-процесса в рамках коммуникатора MPI_COMM_WORLD.
Вызов MPI_Get_processor_name() запишет в переменную name строковой идентификатор (название) вычислительного узла, на котором был запущен соответствующий процесс.
Собранная информация (ранг процесса, размерность MPI_COMM_WORLD, название процессора) далее посылается со всех ненулевых рангов на нулевой с помощью функции MPI_Send():
MPI_Send() функция имеет следующий формат:
MPI_Send(buf, count, type, dest, tag, comm)
buf — адрес буфера памяти, в котором располагаются пересылаемые данные;
count — количество элементов данных в сообщении;
type — тип элементов данных пересылаемого сообщения;
dest — ранг процесса-получателя сообщения;
tag — специальный тег для идентификации сообщений;
comm — коммуникатор, в рамках которого выполняется посылка сообщения.
Более подробное описание функции MPI_Send() и ее аргументов, а также других MPI-функций можно найти в MPI-стандарте (язык документации — английский).
На нулевом ранге принимаются сообщения, посланные остальными рангами, и печатаются на экран:
Для наглядности нулевой ранг дополнительно печатает свои данные наподобие тех, что он принял с удаленных рангов.
MPI_Recv() функция имеет следующий формат:
MPI_Recv(buf, count, type, source, tag, comm, status)
buf, count, type — буфер памяти для приема сообщения;
source — ранг процесса, от которого должен быть выполнен прием сообщения;
tag — тег принимаемого сообщения;
comm — коммуникатор, в рамках которого выполняется прием данных;
status — указатель на специальную MPI-структуру данных, которая содержит информацию о результате выполнения операции приема данных.
В данной статье мы не будем углубляться в тонкости работы функций MPI_Send()/MPI_Recv(). Описание различных типов MPI-операций и тонкостей их работы — тема отдельной статьи. Отметим только, что нулевой ранг в нашей программе будет принимать сообщения от других процессов строго в определенной последовательности, начиная с первого ранга и по нарастающей (это определяется полем source в функции MPI_Recv(), которое изменяется от 1 до size).
Описанные функции MPI_Send()/MPI_Recv() — это пример так называемых двухточечных (point-to-point) MPI-операций. В таких операциях один ранг обменивается сообщениями с другим в рамках определенного коммуникатора. Существуют также коллективные (collective) MPI-операции, в которых в обмене данными могут участвовать более двух рангов. Коллективные MPI-операции — это тема для отдельной (и, возможно, не одной) статьи.
В результате работы нашей демонстрационной MPI-программы мы получим:
Вас заинтересовало рассказанное в этом посте и вы хотели бы принять участие в развитии технологии MPI? Команда разработчиков Intel MPI Library (г.Нижний Новгород) в данный момент активно ищет инженеров-соратников. Дополнительную информацию можно посмотреть на официальном сайте компании Intel и на сайте BrainStorage.
И, напоследок, небольшой опрос по поводу возможных тем для будущих публикаций, посвященных высокопроизводительным вычислениям.
Часть 1. MPI — Введение и первая программа
Введение. Зачем все это?
В этом цикле статей речь пойдет о параллельном программировании.
В бой. Введение
Довольно часто самые сложные алгоритмы требуют огромного количества вычислительных ресурсов в реальных задачах, когда программист пишет код в стандартном его понимании процедурного или Объектно Ориентированного Программирования(ООП), то для особо требовательных алгоритмических задач, которые работают с большим количеством данных и требуют минимизировать время выполнения задачи, необходимо производить оптимизацию.
В основном используют 2 типа оптимизации, либо их смесь: векторизация и распараллеливание
вычислений. Чем же они отличаются?
Вычисления производятся на процессоре, процессор пользуется специальными «хранилищами» данных называемыми регистрами. Регистры процессора напрямую подключены к логическим элементам и требуют гораздо меньшее время для выполнения операций над данными, чем данные из оперативной памяти, а тем более на жестком диске, так как для последних довольно большую часть времени занимает пересылка данных. Так же в процессорах существует область памяти называемая Кэшем, в нем хранятся те значения, которые в данный момент участвуют в вычислениях или будут участвовать в них в ближайшее время, то есть самые важные данные.
Задача оптимизации алгоритма сводится к тому, чтобы правильно выстроить последовательность операций и оптимально разместить данные в Кэше, минимизировав количество возможных пересылок данных из памяти.
Параллелизация же позволяет пользоваться не только вычислительными возможностями одного процессора, а множеством процессоров, в том числе и в системах с распределенной памятью, и т.п. В подобных системах как правило намного больше вычислительных мощностей, но этим всем нужно уметь управлять и правильно организовывать параллельную работу на конкретных потоках.
Основная часть
Если сильно упростить, то первая модель берет разные исходные коды программы и выполняет их на разных потоках, а вторая модель берет один исходный код и запускает его на всех выделенных потоках. Программы написанные под стиль MIMD довольно сложно отлаживаются из-за своей архитектуры, поэтому чаще используется модель SPMD. В MPI возможно использовать и то и то, но по стандарту(в зависимости от реализации, разумеется) используется модель SPMD.
Установка
В командной строке вводим следующие команды:
Первая команда обновляет менеджер пакетов, вторая устанавливает компилятор GCC, если его нет в системе, третья программа устанавливает компилятор через который мы собственно и будем работать с C\С++&MPI кодом.
Первые шаги.
Для понимания того что происходит дальше нужно определиться с несколькими терминами:
В данном примере не будем использовать пересылку данных, а лишь ознакомимся с основами процедур MPI.ения возможности использования большинства процедур MPI в любой программе должны быть следующие процедуры:
Первая процедура предназначения для инициализации параллельной части программы, все другие процедуры, кроме некоторых особых, могут быть вызваны только после вызова процедуры MPI_Init, принимает эта функция аргументы командной строки, через которые система может передавать процессу некоторые параметры запуска. Вторая процедура же предназначена для завершения параллельной части программы.
Для того чтобы проверить работу программы реализуем самую примитивную программку на С++ с применением MPI.
Чтобы запустить эту программу сохраним нашу запись в файле с любым названием и расширением *.cpp, после чего выполним следующие действия в консоли (В моем случае код лежит в файле main.cpp):
В этой краткой статье мы на примере наипростейшей программы научились запускать файлы C++ с MPI кодом и разобрались что за зверь вообще MPI и с чем его едят. В дальнейших туториалах мы рассмотрим уже более полезные программы и наконец перейдем ко коммуникации между процессами.
Использование задач с несколькими экземплярами для запуска приложений с интерфейсом передачи сообщений в пакетной службе
Общие сведения о задачах с несколькими экземплярами
В пакетной службе каждая задача обычно выполняется на одном вычислительном узле: вы отправляете несколько задач в задание, и пакетная служба планирует выполнение каждой задачи на узле. Тем не менее, если настроить параметры использования нескольких экземпляров задачи, вы сообщите пакетной службе вместо этого создать одну основную задачу и несколько подзадач, которые выполняются на нескольких узлах.
При отправке задачи с несколькими экземплярами в задание пакетная служба выполняет определенные действия.
Требования к задачам с несколькими экземплярами
При выполнении многоэкземплярной задачи требуется, чтобы в пуле был включен обмен данными между узлами и отключено параллельное выполнение задач. Чтобы отключить параллельное выполнение задач, задайте для свойства CloudPool.TaskSlotsPerNode значение 1.
Пакетная служба ограничит размера пула, для которого включен обмен данными между узлами.
Если запустить многоэкземплярную задачу в пуле с отключенным обменом данными между узлами или со значением параметра taskSlotsPerNode больше 1, задача не будет запланирована. Вместо этого она будет постоянно находиться в состоянии «Активно».
Установка MPI с помощью StartTask
Для выполнения приложений MPI с помощью задачи с несколькими экземплярами на вычислительных узлах в пуле сначала необходимо установить реализацию MPI (например, MS-MPI или Intel MPI). Это хорошая возможность воспользоваться задачей StartTask, которая выполняется каждый раз при присоединении узла к пулу или его перезапуске. В этом фрагменте кода создается задача StartTask, указывающая пакет установки MS-MPI в качестве файла ресурсов. Командная строка задачи запуска выполняется после скачивания файла ресурсов на узел. В этом случае командная строка выполняет автоматическую установку MS-MPI.
Удаленный доступ к памяти (RDMA)
Если для пула пакетной службы выбран размер с поддержкой RDMA, например А9, то приложение MPI может воспользоваться преимуществами сети RDMA Azure с удаленным доступом к памяти, обеспечивающей высокую производительность и низкие задержки.
Найдите размеры, помеченные «С поддержкой доступа RDMA» в поле Размеры виртуальных машин в Azure (для пулов VirtualMachineConfiguration) или Размеры для облачных служб (для пулов CloudServicesConfiguration ).
Чтобы воспользоваться преимуществами RDMA на вычислительных узлах Linux, на узлах необходимо использовать Intel MPI.
Теперь после рассмотрения требований к пулу и установки пакета MPI самое время перейти к созданию задачи с несколькими экземплярами. В этом фрагменте кода мы создадим стандартную задачу CloudTask, а затем настроим ее свойство MultiInstanceSettings. Как упоминалось выше, многоэкземплярная задача не относится к особому типу задач, а является стандартной задачей пакетной службы, настраиваемой с помощью параметров для множественных экземпляров.
Основная задача и подзадачи
При создании параметров для множественных экземпляров для задачи нужно указать число вычислительных узлов, которые должны выполнять задачу. При отправке задачи в задание пакетная служба создает одну основную задачу и такое количество подзадач, которое вместе с основной задачей совпадает с указанным количеством узлов.
Этим задачам назначается целочисленный идентификатор в диапазоне от 0 до numberOfInstances – 1. Задача с идентификатором 0 — это основная задача, а все остальные идентификаторы назначаются подзадачам. Например, при создании следующих параметров нескольких экземпляров для задачи основная задача будет иметь идентификатор 0, а подзадачи — в диапазоне от 1 до 9.
Главный узел
Когда вы отправляете многоэкземплярную задачу, пакетная служба назначает один из вычислительных узлов главным и планирует выполнение основной задачи на нем. Выполнение подзадач она планирует на остальных вычислительных узлах, выделенных для многоэкземплярной задачи.
команду координации
Команду координации выполняют основная задача и подзадачи.
Вызов команды координации блокируется: пакетная служба не выполнит команду приложения, пока команда координации не будет успешно возвращена для всех подзадач. Таким образом, команда координации должна запустить все необходимые фоновые службы. Убедитесь, что они готовы к использованию, а затем выполните выход. К примеру, эта команда координации для решения с использованием MS-MPI версии 7 запускает службу SMPD на узле, а затем завершает работу:
Команда приложения
Так как команда mpiexec.exe приложений MS-MPI использует переменную CCP_NODES по умолчанию (см. раздел Переменные среды), в примере командной строки приложения выше она исключена.
Переменные среды
Пакетная служба создает несколько переменных среды для конкретных задач с несколькими экземплярами на вычислительных узлах, выделенных для каждой задачи с несколькими экземплярами. Командные строки координации и приложения могут ссылаться на эти переменные среды, как и сценарии и программы, выполняемые с их помощью.
Приведенные ниже переменные среды создаются пакетной службой для использования многоэкземплярными задачами.
Полную информацию об этих и других переменных среды вычислительных узлов пакетной службы, включая их содержимое и видимость, доступны в разделе Переменные среды вычислительных узлов.
Фрагмент кода Linux MPI для пакетной службы содержит пример использования некоторых из этих переменных среды.
Файлы ресурсов
Для многоэкземплярных задач есть два набора файлов ресурсов: общие файлы ресурсов, которые скачивают все задачи (основная задача и подзадачи), и файлы ресурсов, указанные непосредственно для многоэкземплярной задачи, которые скачивает только основная задача.
Срок действия задачи
Срок действия основной задачи определяет срок действия всей задачи с несколькими экземплярами. При выходе из основной задачи завершаются все подзадачи. Код выхода основной задачи — это код выхода самой задачи с несколькими экземплярами. На его основе можно определить, как завершилась задача (успешно или со сбоем), т. е. нужно ли выполнять повторные попытки.
Если любая из подзадач завершается неудачно, например, выполняется выход с ненулевым кодом возврата, происходит сбой всей задачи с несколькими экземплярами. Затем задача с несколькими экземплярами завершается и выполняется повторно, пока не будет достигнут предел повтора.
При удалении задачи с несколькими экземплярами пакетная служба удаляет основную задачу и все подзадачи. Все каталоги и файлы подзадач удаляются из вычислительных узлов также, как и при стандартной задаче.
Свойства TaskConstraints для задачи с несколькими экземплярами (включая MaxTaskRetryCount, MaxWallClockTime и RetentionTime), обрабатываются как свойства для стандартной задачи и применяются к основной задаче и всем подзадачам. Однако при изменении свойства RetentionTime после добавления задачи с несколькими экземплярами в задание это изменение будет применено только к основной задаче, а все подзадачи продолжат использовать исходное значение RetentionTime.
Если последняя задача является частью многоэкземплярной задачи, в списке последних задач вычислительного узла отображается идентификатор подзадачи.
Получение сведений о подзадачах
В следующем фрагменте кода показано, как получить сведения о подзадачах, а также запросить содержимое файла из узлов, на которых выполняются эти подзадачи.
Пример кода
В примере кода MultiInstanceTasks в GitHub демонстрируется использование задачи с несколькими экземплярами для запуска приложения MS-MPI на вычислительных узлах пакетной службы. Чтобы запустить пример кода, выполните указанные ниже действия.
Подготовка
Выполнение
Откройте решение MultiInstanceTasks в Visual Studio 2019. Файл решения MultiInstanceTasks.sln находится в следующем расположении:
Введите данные своей учетной записи пакетной службы и учетной записи хранения в AccountSettings.settings в проекте Microsoft.Azure.Batch.Samples.Common.
Создайте и запустите решение MultiInstanceTasks, чтобы выполнить пример приложения MPI на вычислительных узлах в пуле пакетной службы.
Необязательно: используйте портал Azure или Batch Explorer, чтобы изучить пример пула, задания и задачи («MultiInstanceSamplePool», «MultiInstanceSampleJob», «MultiInstanceSampleTask») перед тем, как удалить ресурсы.
Вы можете скачать Visual Studio Community бесплатно, если у вас нет Visual Studio.







