std::future
Настало время запилить для нашего FreeRTOS примитивы синхронизации чуть посложнее, чем мьютекс.
Да, срочно понадобился std::future. Потоки у нас уже есть, нужно уметь и считать в параллель. Что же, позволю себе - просто 30 секунд или одну минуту - маленькую историческую справку дать. Вы не против?
Наверняка олды помнят, что впервые future появился в boost. В 2008 оно называлось boost::unique_future и многие видели в хидере, что автора звали Anthony Williams. Есть мнение, ну, некоторые говорят, что это псевдоним Антона Полухина, но, скорее всего, это два разных человека. Хотя я лично их вместе никогда не видел...
Ладно, я тоже думал, что future придумал Anthony, но археологические раскопки убедительно показывают, что у победы много отцов, а только мои проекты - сироты.
Осведомленные источники утверждают, что решающий и непоправимый успех предприятия нанес Howard E. Hinnant с предложением N2094 Multithreading API for C++0X - A Layered Approach.
С этим сложно спорить, это действительно мощный документ, но сам он в главе Futures пишет:
Предложение в этой части разделяет точку зрения, изложенную в N2096 с некоторыми незначительными изменениями.
Интересно, его предложение действительно датировано девятым сентября 2006 года, а за два дня до этого седьмого сентября 2006 вышло предложение N2096 Transporting Values and Exceptions between Threads некого Peter Dimov.
Судя по всему, мистер Williams просто слил, в хорошем смысле слова, оба предложения, добавил свое - N2276 Thread Pools and Futures, взболтал и получил практически законченную концепцию future.
Более того, он это и не скрывал. В документе N2561: An Asynchronous Future Value прямо указывается:
В этом предложении предпринята попытка объединить три статьи: N2094, N2185, N2276. И постараемся не вляпаться ни во что, напоминающее пулы потоков или запуск тасок.
Отдавая дань уважения первописателю, посмотрим, чего же именно предлагал месье Dimov.
Большинство существующих вариантов для возврата значения из функции, выполняемой в отдельном потоке, привязывают механизм передачи к конкретному API многопоточности. Здесь предлагается другой подход. Опишем универсальный компонент future<R> следующим образом:
𝚝𝚎𝚖𝚙𝚕𝚊𝚝𝚎<𝚌𝚕𝚊𝚜𝚜 𝚁>
𝚌𝚕𝚊𝚜𝚜 𝚏𝚞𝚝𝚞𝚛𝚎 {
𝚙𝚞𝚋𝚕𝚒𝚌:
𝚏𝚞𝚝𝚞𝚛𝚎(); // создает пустой объект
𝚋𝚘𝚘𝚕 𝚛𝚎𝚊𝚍𝚢() 𝚌𝚘𝚗𝚜𝚝 𝚝𝚑𝚛𝚘𝚠(); // запрашивает информацию о том, содержит ли будущее значение или исключение
𝚟𝚘𝚒𝚍 𝚠𝚊𝚒𝚝(); // ждет готовности объекта
𝚋𝚘𝚘𝚕 𝚝𝚒𝚖𝚎𝚍_𝚠𝚊𝚒𝚝( 𝚝𝚒𝚖𝚎𝚜𝚙𝚎𝚌 𝚌𝚘𝚗𝚜𝚝 & 𝚊𝚋𝚜𝚝𝚒𝚖𝚎 ); // тоже, только с таймаутом
𝚁 𝚌𝚘𝚗𝚜𝚝 & 𝚘𝚙𝚎𝚛𝚊𝚝𝚘𝚛()() 𝚌𝚘𝚗𝚜𝚝; // ждет значения и возвращает его
𝚟𝚘𝚒𝚍 𝚜𝚎𝚝_𝚟𝚊𝚕𝚞𝚎( 𝚁 𝚌𝚘𝚗𝚜𝚝 & 𝚛 ); // устанавливает значение 𝚛 и сообщает в 𝚛𝚎𝚊𝚍𝚢()
𝚝𝚎𝚖𝚙𝚕𝚊𝚝𝚎<𝚌𝚕𝚊𝚜𝚜 𝙴> 𝚟𝚘𝚒𝚍 𝚜𝚎𝚝_𝚎𝚡𝚌𝚎𝚙𝚝𝚒𝚘𝚗() 𝚝𝚑𝚛𝚘𝚠(); // как если бы сохранил 𝙴() и сообщает в 𝚛𝚎𝚊𝚍𝚢()
𝚝𝚎𝚖𝚙𝚕𝚊𝚝𝚎<𝚌𝚕𝚊𝚜𝚜 𝙴> 𝚟𝚘𝚒𝚍 𝚜𝚎𝚝_𝚎𝚡𝚌𝚎𝚙𝚝𝚒𝚘𝚗( 𝙴 𝚌𝚘𝚗𝚜𝚝 & 𝚎 ) 𝚝𝚑𝚛𝚘𝚠(); // сохраняет исключение 𝚎 и сообщает в 𝚛𝚎𝚊𝚍𝚢()
};
Давайте рассматривать future как контейнер единственного элемента — передаваемого значения.
Это могло бы пригодиться, например, когда поток-производитель и поток-потребитель получают копию одного и того же пустого future. Когда производитель заканчивает тяжеловесные вычисления, он помещает результат в future с помощью set_value. Потребитель же может получить результат вызвав operator().
Если же результата нет, то почему бы не поместить исключение в future. А выкинет его уже при вызове operator().
Неблокирующий метод ready() можно использовать для проверки наличия значения или исключения.
Еще есть два вызова, wait и timed_wait, которые позволяют потребителю блокироваться до тех пор, пока future не дойдет до состояния готовности.
Функция operator() будет блокироваться до готовности, после чего вернет сохраненное значение r после вызова set_value(r) или выбросит сохраненное исключение E(), или e после вызова set_exception<E>() или set_exception(e).
set_value(r) сохраняет r в future, последующий вызов operator() вернет r.
С set_exception<E>() и set_exception(e) все понятно, последующий вызов operator() выбросит исключение.
Идея понятна, осталось только заглянуть в текущую реализацию. Так ли сильно мутировал future?