Типовые модели параллельных приложений
Существуют следующие распространенные модели параллельных приложений:
- модель делегирования ("управляющий-рабочий");
- сеть с равноправными узлами;
- конвейер ("производители-потребители");
Каждая модель характеризуется собственной декомпозицией работ, которая определяет, кто отвечает за порождение подзадач и при каких условиях они создаются, какие информационные зависимости между подзадачами существуют.
Модель | Описание |
---|---|
Модель делегирования | Центральный поток ("управляющий") создает "рабочие" потоки и назначает каждому из них задачу. Управляющий поток ожидает завершения работы потоков, собирает результаты. |
Модель с равноправными узлами | Нет центрального узла, все потоки участвуют в обработке. |
Конвейер | Конвейерный подход применяется для поэтапной обработки потока входных данных. Входные данные обрабатываются строго последовательно. |
Модель делегирования
В модели делегирования выделяется один центральный поток (менеджер, управляющий, мастер) и несколько рабочих потоков. Управляющий поток запускает рабочие потоки, передает им все необходимые данные, контролирует работу и обрабатывает результаты после их завершения.
К модели делегирования относится так называемая схема "fork-join". Этап "fork" (разветвление) – делегирование полномочий рабочим потокам: создание, запуск, передача параметров. Этап "join" (присоединение) – ожидание завершения работы потоков.
Самым простым способом реализации схемы fork-join является применение шаблона Parallel.Invoke:
Parallel.Invoke(worker1, worker2, worker3);
Шаблон параллельно запускает рабочие элементы и блокирует основной поток до завершения работы.
Если в главном потоке выполняется какая-либо работа параллельно с рабочими потоками, то можно использовать объекты Task и встроенные механизмы ожидания.
Task t1 = Task.Factory.StartNew(..); Task t2 = Task.Factory.StartNew(..); Task t3 = Task.Factory.StartNew(..); // Параллельная работа центрального узла fManager(); // Ожидаем завершения работы Task.WaitAll(t1, t2, t3); // Обрабатываем результаты fResults();
Объект синхронизации CountdownEvent позволяет выполнять координацию работы управляющего и рабочих потоков. Объект обладает внутренним счетчиком, который устанавливается при инициализации объекта. При завершении работы или достижении какого-либо этапа рабочий поток вызывает метод Signal, уменьшающий внутренний счетчик. Когда внутренний счетчик становится равным нулю, объект сигнализирует потоку, ожидающему с помощью метода Wait.
static void Main() { int N = 5; CountdownEvent ev = new CountdownEvent(N); Thread[] workers = new Thread[N]; for(int i=0; i<N; i++) { int y = i; workers[i] = new Thread(() => { DoSomeWork1(y); ev.Signal(); DoSomeWork2(y); }); workers[i].Start(); } while(!ev.IsSet) { ev.Wait(TimeSpan.FromSeconds(5)); Console.WriteLine("{0}-рабочих закончили", ev.CurrentCount); SaveCurrentResults(); } }
При инициализации объекта указывается начальное значение внутреннего счетчика объекта. Каждый рабочий поток при завершении работы уменьшает значение счетчика с помощью метода Signal. Уменьшение счетчика осуществляется потокобезопасно. Когда значение счетчика уменьшится до нуля, объект синхронизации сигнализирует ожидающему главному потоку, при этом главный поток разблокируется.
Кроме блокирующего ожидания с помощью метода Wait, также можно использовать перегрузку, принимающую в качестве аргумента интервал в миллисекундах, в течение которого поток блокируется. Текущее значение внутреннего счетчика можно узнать с помощью свойства CurrentCount, свойство IsSet позволяет определить установлен ли сигнал или нет.
Объект Barrier инкапсулирует барьерную синхронизацию с возможностью нескольких этапов. При барьерной синхронизации несколько участников могут параллельно выполнять свою работу. При завершении работы участники дожидаются остальных. После завершения работы всех участников выполняется финальный обработчик на данном этапе. После завершения работы финального обработчика начинается следующий этап, и работа участников возобновляется.