C - що таке std

Я досить добре знайомий з компонентами std :: thread. std :: async і std :: future стандартної стандартної бібліотеки (наприклад, див. Ця відповідь), які прямолінійні.

Однак я не можу зрозуміти, що означає std :: promise. що він робить і в яких ситуаціях його найкраще використовувати. У самому стандартному документі не міститься багато інформації за межами його синопсиса, і жоден з них не просто. thread.

Може хто-небудь, будь ласка, дайте короткий, короткий приклад ситуації, коли потрібно std :: promise і де це саме ідіоматичне рішення?

У словах [futures.state] std :: future - це асинхронний повертається об'єкт ( «об'єкт, який читає результати із загального стану»), а std :: promise - це асинхронний постачальник ( «об'єкт, який дає результат До загального стану") , тобто обіцянку - це те, на що ви встановили результат, щоб ви могли отримати його з майбутнього.

Асинхронний постачальник - це те, що спочатку створює загальний стан, на яке посилається майбутнє. std :: promise - один з видів асинхронного провайдера, std :: packaged_task - інший, а внутрішня деталь std :: async - інша. Кожен з них може створити загальний стан і надати вам std :: future який розділяє це стан, і може зробити стан готовим.

std :: async - це службова утиліта більш високого рівня, яка дає вам асинхронний об'єкт результату і внутрішньо піклується про створення асинхронного провайдера і забезпеченні готовності загального стану при завершенні завдання. Ви можете емулювати його за допомогою std :: packaged_task (або std :: bind і std :: promise) і std :: thread але безпечніше і простіше використовувати std :: async.

std :: promise - трохи нижчий рівень, оскільки якщо ви хочете передати асинхронний результат в майбутнє, але код, який робить результат готовий, не може бути завершений в одну функцію, яка підходить для переходу до std :: async. Наприклад, у вас може бути масив з декількох promise s і пов'язаних з ним future s і мати один потік, який виконує кілька обчислень і задає результат по кожному обіцянці. async тільки дозволить вам повернути один результат, щоб повернути кілька, вам потрібно буде кілька разів викликати async виклик, що може привести до збою ресурсів.

У C ++ є дві різні, хоча і пов'язані з ними поняття: Асинхронне обчислення (функція, яка називається десь ще) і паралельне виконання (потік. Щось, що працює одночасно). Ці два є ортогональними поняттями. Асинхронне обчислення - це просто відмінний виклик функції, тоді як потік - це контекст виконання. Теми корисні самі по собі, але для цілей цього обговорення я буду розглядати їх як деталь реалізації.

Існує ієрархія абстракції для асинхронного обчислення. Наприклад, припустимо, що у нас є функція, яка приймає деякі аргументи:

По-перше, у нас є шаблон std :: future. який являє собою майбутнє значення типу T Значення може бути отримано через функцію-член get (). яка ефективно синхронізує програму, чекаючи результату. В якості альтернативи майбутнє підтримує wait_for (). яке може використовуватися для перевірки того, чи доступний результат вже. Ф'ючерси слід розглядати як асинхронну заміну заміни для звичайних типів повернення. Для нашої зразкової функції ми очікуємо std :: future.

Тепер, за ієрархією, від найвищого до найнижчого рівня:

std :: async. найзручніший і прямий спосіб виконати асинхронне обчислення - через шаблон функції async. який негайно повертає відповідне майбутнє:

У нас дуже мало контролю над деталями. Зокрема, ми навіть не знаємо, чи виконується функція одночасно, по черзі після get () або будь-якої іншої чорною магією. Однак результат легко виходить при необхідності:

Тепер ми можемо розглянути, як реалізувати щось на зразок async. але таким чином, щоб ми контролювали. Наприклад, ми можемо наполягати на тому, щоб функція виконувалася в окремому потоці. Ми вже знаємо, що ми можемо надати окремий потік за допомогою класу std :: thread.

Наступний нижній рівень абстракції робить саме це: std :: packaged_task. Це шаблон, який обгортає функцію і надає майбутнє для значення, що повертається функції, але сам об'єкт є викликається, а виклик - за бажанням користувача. Ми можемо налаштувати його таким чином:

Майбутнє буде готове після виклику завдання і завершення виклику. Це ідеальна робота для окремого потоку. Ми просто повинні обов'язково перемістити завдання в потік:

Потік запускається негайно. Ми можемо або detach його, або join нього в кінці області дії, або всякий раз (наприклад, використовуючи scoped_thread Anthony Williams, яка дійсно повинна бути в стандартній бібліотеці). Однак подробиці використання std :: thread не стосуються нас; Просто обов'язково приєднаєтесь або від'єднайте їх. Важливим є те, що всякий раз, коли закінчується виклик функції, наш результат готовий:

Тепер ми опустилися до найнижчого рівня: як реалізувати пакетну завдання? Саме тут приходить std :: promise. Обіцянка - це будівельний блок для спілкування з майбутнім. Основні етапи:

Зухвалий потік дає обіцянку.

Зухвалий потік отримує майбутнє від обіцянки.

Обіцянка разом з аргументами функцій переноситься в окремий потік.

Новий потік виконує функцію, і заповнення виконує обіцянку.

Результат повертає вихідний потік.

Наприклад, ось наша власна «упакована завдання»:

Використання цього шаблону в основному таке ж, як і для std :: packaged_task. Зверніть увагу, що переміщення всієї завдання наближає виконання обіцянки. У більш складних ситуаціях можна також явно переміщати об'єкт обіцянки в новий потік і зробити його функціональним аргументом функції потоку, але обгортка завдань, подібна вище, здається більш гнучким і менш нав'язливим рішенням.

виконання винятків

Обіцянки тісно пов'язані з винятками. Інтерфейсу з обіцяного недостатньо, щоб повністю передати його стан, тому виключення кидаються щоразу, коли операція по обіцянці не має сенсу. Всі виключення мають тип std :: future_error. який відбувається з std :: logic_error. Перш за все, опис деяких обмежень:

За замовчуванням побудоване обіцянку неактивно. Неактивні обіцянки можуть померти без наслідків.

Обіцянка стає активним, коли майбутнє виходить через get_future (). Однак можна отримати тільки одне майбутнє!

Обіцянка має виконуватися або за допомогою set_value () або з установкою set_exception () повинна бути встановлена ​​до set_exception () терміну його життя, якщо її майбутнє повинно бути використано. Задоволене обіцянку може померти без наслідків, і get () стане доступним в майбутньому. Обіцянка з виключенням підвищить збережене виняток при виклику get () в майбутньому. Якщо обіцянку не вмирає ні за значенням, ні щодо виключення, виклик get () в майбутньому призведе до виключення «зламаного обіцянки».

Ось невелика серія тестів, що демонструє ці різні виняткові дії. По-перше, палять:

Тепер про тести.

Випадок 1: Неактивне обіцянку

Випадок 2: активні обіцянки, невикористані

Випадок 3: Занадто багато ф'ючерсів

Випадок 4: Задоволене обіцянку

Випадок 5: Занадто багато задоволення

Таке ж виняток set_value якщо існує більше одного з значень set_value або set_exception.

Випадок 6: Виключення

Випадок 7: Зламане обіцянку

C ++ розбиває реалізацію ф'ючерсів на набір невеликих блоків

Std. prom - одна з цих частин.

Обіцянка - це засіб для передачі значення, що повертається (або виключення) з потоку, що виконує функцію, в потік, який використовує функцію майбутнього.

Майбутнім є об'єкт синхронізації, побудований навколо приймає кінця обіцяного каналу.

Отже, якщо ви хочете використовувати майбутнє, ви отримаєте обіцянку, яку ви використовуєте для отримання результату асинхронної обробки.

Приклад зі сторінки:

У грубому наближенні ви можете розглядати std :: promise як інший кінець std :: future (це невірно. Але для ілюстрації ви можете думати так, як якщо б це було). Споживчий кінець каналу зв'язку буде використовувати std :: future для використання даних із загального стану, тоді як потік виробника буде використовувати std :: promise для запису в загальний стан.

std :: promise - канал або шлях для інформації, що підлягає поверненню з функції async. std :: future - це механізм синхронізації, який змушує абонента чекати, поки не буде повернуто значення, що повертається, передане в std :: promise (це означає, що його значення встановлено всередині функції).

У асинхронної обробки є 3 основних об'єкта. В даний час C ++ 11 фокусується на двох з них.

Основні функції, необхідні для асинхронної логіки:

  1. Завдання (логіка, упакована як деякий об'єкт-функтор), яка буде «десь».
  2. Фактичний обробляє вузол - потік, процес і т. Д. Який запускає такі функції, коли вони їм надаються. Подивіться на шаблон дизайну «Command», щоб отримати уявлення про те, як це робить основний пул робочих потоків.
  3. Оброблювач результату. комусь потрібен цей результат, і йому потрібен об'єкт, який отримає його для них. Для ООП і інших причин будь очікування або синхронізація повинні виконуватися в API цього дескриптора.

C ++ 11 викликає те, про що я говорю в (1) std :: promise. а також в (3) std :: future. std :: thread - це єдине, що публічно надається для (2). Це сумно, тому що реальним програмами необхідно управляти ресурсами потоків і пам'яті, і більшість із них захочуть запускати завдання в пулах потоків, а не створювати і знищувати нитка для кожної маленької завдання (що майже завжди викликає непотрібні образи продуктивності сам по собі і може легко створювати ресурси Голодування ще гірше).

В якості кінцевої точки для пари обіцянку / майбутнє створюється std. get_future () а std :: future (створена з std. get_future () з використанням get_future ()) - це інша кінцева точка. Це простий метод з одним пострілом, що забезпечує можливість синхронізації двох потоків, оскільки один потік надає дані іншому потоку через повідомлення.

Ви можете думати про це, оскільки один потік створює обіцянку надати дані, а інший потік збирає обіцянку в майбутньому. Цей механізм можна використовувати тільки один раз.

Механізм обіцянки / майбутнього - це тільки один напрямок, з потоку, який використовує метод set_value () для std :: promise set_value () для потоку, який використовує get () для std :: future для отримання даних. Виняток генерується, якщо метод get () майбутнього називається більш ніж один раз.

Якщо потік з std :: promise set_value () не використав set_value () для виконання своєї обіцянки, тоді, коли другий потік викликає get () з std :: future для збору обіцянки, другий потік переходить в стан очікування, поки Обіцянка виконується першим потоком за допомогою std :: promise set_value () коли він використовує метод set_value () для відправки даних.

Одна замітка про це прикладі - затримки, додані в різних місцях. Ці затримки були додані тільки для того, щоб переконатися, що різні повідомлення, відправлені на консоль з використанням std :: cout. будуть ясними і що текст з декількох потоків не буде перемішано.

Перша частина main () створює три додаткових потоку і використовує std :: promise і std :: future для відправки даних між потоками. Цікавим моментом є те, що основний потік запускає потік, T2, який буде чекати даних з основного потоку, щось робити, а потім відправляти дані в третій потік T3, який потім щось зробить і відправить дані назад в Основний потік.

Друга частина main () створює два потоки і набір черг, щоб дозволити кілька повідомлень з основного потоку кожному з двох створених потоків. Ми не можемо використовувати std :: promise і std :: future для цього, тому що обіцянка / майбутній дует - один постріл і не може використовуватися багаторазово.

Джерелом для класу Sync_queue є Stroustrup's C ++ Programming Language: 4th Edition.

Це просте додаток створює наступний висновок.

Обіцянка - це інший кінець дроту.

Уявіть, що вам потрібно отримати значення future. обчислюється async способом. Однак ви не хочете, щоб він був обчислений в одному потоці, і ви навіть не створювали потік «зараз» - можливо, ваше програмне забезпечення було розроблено для вибору потоку з пула, тому ви не знаєте, хто буде Виконати завершення обчислення в кінці.

Тепер, що ви передаєте цього (поки невідомому) потоку / класу / суті? Ви не проходьте повз future. так як це результат. Ви хочете передати щось, що пов'язано з future і яке представляє інший кінець дроту. тому ви просто будете запитувати future без будь-яких знань про те, хто насправді щось вирахує / напише.

Це promise. Це ручка, пов'язана з вашим future. Якщо future - динамік. і за допомогою get () ви починаєте слухати доти, поки не з'явиться якийсь звук, promise - це мікрофон; Але не тільки мікрофон, це мікрофон, підключений до одного дроту до динаміка, який ви тримаєте. Ви можете знати, хто на іншому кінці, але вам не потрібно це знати - ви просто віддаєте його і чекаєте, поки інша сторона нічого не скаже.