Листинг 8.6. Отделение потока GUI от потока задачи

std::thread task_thread;

std::atomic task_cancelled(false);

void gui_thread() {

 while (true) {

  event_data event = get_event();

  if (event.type == quit)

   break;

  process(event);

 }

}

void task() {

 while (!task_complete() && !task_cancelled) {

  do_next_operation();

 }

 if (task_cancelled) {

  perform_cleanup();

 } else {

  post_gui_event(task_complete);

 }

}

void process(event_data const& event) {

 switch(event.type) {

 case start_task:

  task_cancelled = false;

  task_thread = std::thread(task);

  break;

 case stop_task:

  task_cancelled = true;

  task_thread.join();

  break;

 case task_complete:

  task_thread.join();

  display_results();

  break;

 default:

  //...

 }

}

В результате такого разделения обязанностей поток пользовательского интерфейса всегда будет своевременно реагировать на события, даже если задача занимает много времени. Быстрота реакции часто является основной характеристикой приложения с точки зрения пользователя — с приложением, которое полностью зависает на время выполнения некоторой операции (неважно, какой именно), работать неприятно. За счет выделения специального потока для обработки событий пользовательский интерфейс может сам обрабатывать относящиеся к нему сообщения (например, изменение размера или перерисовка окна), не прерывая длительной операции, но передавая адресованные ей сообщения, если таковые поступают.

До сих пор в этой главе мы говорили о том, что следует учитывать при проектировании параллельного кода. Поначалу количество разных факторов может привести в изумление, но постепенно они войдут в плоть и кровь и станут вашей второй натурой. Если описанные выше соображения внове для вас, то, надеюсь, они станут яснее после того, как мы рассмотрим конкретные примеры многопоточного кода.

<p>8.5. Проектирование параллельного кода на практике</p>

В какой мере следует учитывать описанные выше факторы при проектировании, зависит от конкретной задачи. Для демонстрации мы рассмотрим реализацию параллельных версий трех функций из стандартной библиотеки С++. При этом у нас будет знакомая платформа, на которой можно изучать новые вещи. Попутно мы получим работоспособные версии функций, которые можно будет применить при распараллеливании более крупной программы.

Я ставил себе задачей продемонстрировать определенные приёмы, а не написать самый оптимальный код. Реализации, в которых лучше используется имеющееся оборудование, можно найти в академической литературе по параллельным алгоритмам или в специализированных многопоточных библиотеках типа Intel Threading Building Blocks[20].

Концептуально простейшим параллельным алгоритмом является параллельная версия std::for_each, с которой я и начну.

<p>8.5.1. Параллельная реализация <code>std::for_each</code></p>

Идея std::for_each проста — этот алгоритм вызывает предоставленную пользователем функцию для каждого элемента диапазона. Различие между параллельной и последовательной реализацией std::for_each заключается, прежде всего, в порядке вызовов функции. Стандартная версия std::for_each вызывает функцию сначала для первого элемента диапазона, затем для второго и так далее, тогда как параллельная версия не дает гарантий относительно порядка обработки элементов, они даже могут (и хочется надеяться, будут) обрабатываться параллельно.

Для реализации параллельной версии нужно всего лишь разбить диапазон на участки, которые будут обрабатываться каждым потоком. Количество элементов известно заранее, поэтому такое разбиение можно произвести до начала работы (см. раздел 8.1.1). Мы будем предполагать, что это единственная исполняемая параллельная задача, поэтому вычислить количество требуемых потоков можно с помощью функции std::thread::hardware_concurrency(). Мы также знаем, что элементы можно обрабатывать абсолютно независимо, поэтому для предотвращения ложного разделения (см. раздел 8.2.3) имеет смысл использовать соседние блоки.

Перейти на страницу:

Похожие книги