Добавил:
t.me Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

2 семестр / Литература / Язык программирования С++. Краткий курс. Страуструп

.pdf
Скачиваний:
10
Добавлен:
16.07.2023
Размер:
31.34 Mб
Скачать

15

Параллельные

вычисления

+ +

Все следуетупрощать до тех пор,

пока это возможно, но не более того.

-А. Эйнштейн

Введение Задания и потоки

+ + + + +

Передача аргументов

 

Возврат результатов

 

Совместное использование данных

Ожидание событий

 

Обмен информацией

с заданиями

future И promise

 

packaged_task async() + Советы

15.1.

Введение

Параллелизм

-

выполнение

нескольких

задач

одновременно

-

широко

используется

для

повышения

пропускной

способности

(путем

использования

нескольких

процессоров

для

единого

вычисления)

или

для

снижения

времени

отклика

(позволяя

одной

части

программы

выполняться,

в

то

время

как

дру­

гая

ожидает

ответа).

Все

современные

языки

программирования

поддержива­

ют эту возможность.

Поддержка,

предоставляемая

стандартной

библиотекой

С++,

представляет

собой

переносимый

и

безопасный

с

точки

зрения

типов

вариант того, что использовалось в С++ более поддерживается современным оборудованием.

20 лет и почти повсеместно Поддержка стандартной би­

блиотеки,

в

первую

очередь,

направлена

на

поддержку

параллелизма

на

си­

стемном

уровне,

а

не

на

непосредственное

предоставление

сложных

моделей

параллелизма

более

высокого уровня;

эти

модели

могут

быть

предоставлены

272

Глава

15.

Параллельные

вычисления

в

виде

библиотек,

созданных

с

использованием

средств

стандартной

библио­

теки.

Стандартная

библиотека

непосредственно

поддерживает

одновременное

выполнение

нескольких

потоков

в

одном

адресном

пространстве.

Для

этого

С++

предоставляет

подходящую

модель памяти

и

набор

атомарных

опера­

ций.

Атомарные

операции

обеспечивают

возможность

программирования

без

применения

блокировок

[16].

Данная

модель

памяти

гарантирует,

что,

пока

программист

избегает

гонок

данных

(неконтролируемого

одновременного

досrупа к изменяемым данным), все работает так,

вно ожидать. Однако большинство пользователей

как можно было бы

наи­

видят параллелизм

толь­

ко

с

точки

зрения

стандартной

библиотеки

и

библиотек,

построенных

на

ее

основе. В

этом

разделе

кратко

рассмотрены

примеры

основных

средств

под­

держки

параллелизма

стандартной

библиотеки:

thread,

mu

tex,

операции

lock

(),

packaged

_

task

и

future.

Эти

средства

построены

непосредствен­

но

на

примитивах,

предлагаемых

операционными

системами, и

не

приводят

к

снижению

производительности

по

сравнению

с

их

непосредственным

приме­

нением.

Они

также

не

обещают

какого-либо

существенного

улучшения

про­

изводительности

по

сравнению

с

тем,

что

предлагает

операционная

система.

Не

считайте

параллелизм

панацеей.

Если

задача

может

быть

выполнена

последовательно,

часто

проще

и быстрее

именно

так

и

посrупить.

В

качестве альтернативы

использованию

явных

возможностей

параллелиз­

ма зачасrую можно прибегнуть к параллельному алгоритму, вать несколько механизмов повышения производительности

чтобы (§ 12.9,

использо­ §14.3.1 ).

15.2.

Задания

и

потоки

но

Вычисление, которое потенциально может быть

выполнено одновремен­

с другими вычислениями, называется заданием

(task). Поток (thread) -

это

системное

представление

задания

в

программе.

Задание,

которое

долж­

но

выполняться

одновременно

с

другими

заданиями,

запускается

на

выпол­

нение

путем

создания

объекта

std::

thread

(описан

в

заголовочном

файле

<thread>) с заданием в качестве аргумента. функцию или функциональный объект:

Задание

представляет

собой

void f struct

(); F

11 11

Функция Функциональный

объект

void

};

operator()

();

//Оператор

вызова

в

F

(§6.3.2)

void

user ()

{

 

 

thread

tl

{f};

//

f()

выполняется

в

отдельном

потоке

274

Глава

15.

Параллельные

вычисления

15.3.

Передача

арrументов

Как

правило,

для

работы

заданию

требуются

данные.

Мы

можем

легко

пе­

редавать

их

(или

указатели,

или

ссылки

на данные)

в

качестве

аргументов.

Рассмотрим

следующий код:

void f(vector<douЫe>&

struct F

v);

//Функция, работающая с

v

/ / Функциональный объект,

работающий

с

v

vector<douЫe>&

v;

 

F(vector<douЫe>&

vv)

void operator()

();

 

};

:v(vv}

( }

// Оператор

приложения;

§6.3.2

int

main()

vector<douЫe> vector<douЫe>

some_vec (1,2,3,4,5,6,7,8,9};

vec2

(10,11,12,13,14};

11 f (some_ vec) работает в отдельном

thread tl (f,ref(some_vec) };

потоке:

// F(vec2)

() работает

thread t2

{F{vec2}};

в

отдельном

потоке:

tl.join(); t2. join ();

Очевидно,

что

F{

vec2}

сохраняет

ссылку

на

вектор-аргумент

в

F.

Теперь

F

может

использовать

этот вектор,

и,

будем

надеяться,

никакая

другая

задача

не

будет

обращаться

к

vec2,

пока

выполняется

F

(передача

vec2

по

значению

устраняет

этот

риск).

Инициализация с

шаблона thread

помощью {f,

ref (sorne_vec) } использует конструктор

с переменным

количеством аргументов, который может

принимать

произвольную

последовательность

аргументов

(§7.4).

ref

()

-

это

функция

типа

из

<f

unctional>,

которая,

к

сожалению,

необходима

для

того,

чтобы

шаблон

переменной

рассматривал

sorne_

vec

как

ссылку,

а

не

как

объект.

Без

ref

()

аргумент

sorne

vec

будет

передаваться

по

значению.

Ком­

пилятор

проверяет,

может

ли

первый

аргумент

быть

вызван

с

переданными

аргументами,

и

создает

необходимый

функциональный

объект

для

передачи

потоку.

Таким

образом,

если

F::

operator

() ()

и

f

()

выполняют

один

и

тот

же

алгоритм,

то

работа

двух

заданий

грубо

эквивалентна:

в

обоих

случаях

создается

функциональный

объект

для

выполнения thread.

276

Глава

15.

Параллельные

вычисления

15.5.

Совместное

испоnьэование

данных

Иногда

задания

должны

совместно

использовать

некоторые

данные.

В этом

случае

доступ

к

ним

должен

быть

синхронизирован,

чтобы

одновре­

менно

к

ним

могло

обращаться

не

более

одного

задания.

Опытные

програм­

мисты

рассматривают

это

как

упрощение

(например,

не

возникает никаких

проблем

со

многими

заданиями,

одновременно

считывающими

неизменяе­

мые

данные),

тем

не

менее

мы

рассмотрим,

как

обеспечить

выполнение

ус­

ловия,

чтобы

одновременно

доступ

к

заданному

набору

объектов

имело

не

более одного задания.

 

 

Основополагающим

элементом решения этой задачи является

(mutua\ exc\usion object -

объект взаимоисключения, мьютекс).

mu tex Поток

thread

захватывает mutex

с

помощью

операции

lock

():

mutex

m;

 

int

sh;

 

void

f

()

 

{

 

 

 

 

scoped_lock

 

sh

+=

7;

lck

{m};

//

УправляЮllQ-fй мьютекс

 

//

Совместно используемые данные

 

//

Захват

мьютекса

 

/ /

Работа

с совместно используемыми

данными

//

Неявное

освобождение мьютекса

 

Тип

lck

выводится

как

scoped_lock<mutex>

(§6.2.3).

Конструктор

scoped

_

lock

захватывает

мьютекс

(вызовом

m.

lock

()).Если

другой

поток

уже

захватил

мьютекс,

наш

поток

ожидает

("блокируется"),

пока

другой

по­

ток

не

завершит

свой

доступ

к

данным.

Как

только

другой

поток

завершит

свой

доступ

к

общим

данным,

scoped

lock

освобождает

мьютекс

(вызовом

m. unlock ()).Когда

мьютекс освобождается, потоки, ожидающие его, возоб­

новляют выполнение

("пробуждаются"). Средства взаимного исключения и

блокировки

находятся

в заголовочном

файле

<mutex>.

Обратите внимание дескрипторы ресурсов,

на использование идиомы

такие как scoped_lock и

RAII (§5.3). Использовать

unique_lock (§15.6), про­

ще и

намного

безопаснее,

чем

явно

блокировать

и

разблокировать

мьютексы.

Соответствие

между

совместно

используемыми

данными

и

мьютексом

является

обычным:

программист

просто

должен

знать,

какой

мьютекс

каким

данным

соответствует.

Очевидно,

что

это

чревато

ошибками,

и

в

равной

сте­

пени

очевидно,

что

мы

будем

пытаться

прояснить

соответствие

с

помощью

различных

языковых

средств.

Например:

class

Record

{ puЫic: mutex rm; 11 ...

};