Я досить добре знайомий з компонентами 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
Тепер, за ієрархією, від найвищого до найнижчого рівня:
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 фокусується на двох з них.
Основні функції, необхідні для асинхронної логіки:
- Завдання (логіка, упакована як деякий об'єкт-функтор), яка буде «десь».
- Фактичний обробляє вузол - потік, процес і т. Д. Який запускає такі функції, коли вони їм надаються. Подивіться на шаблон дизайну «Command», щоб отримати уявлення про те, як це робить основний пул робочих потоків.
- Оброблювач результату. комусь потрібен цей результат, і йому потрібен об'єкт, який отримає його для них. Для ООП і інших причин будь очікування або синхронізація повинні виконуватися в 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 - це мікрофон; Але не тільки мікрофон, це мікрофон, підключений до одного дроту до динаміка, який ви тримаєте. Ви можете знати, хто на іншому кінці, але вам не потрібно це знати - ви просто віддаєте його і чекаєте, поки інша сторона нічого не скаже.