Синхронная асинхронность в C++

Наверняка все, кто изучал старый добрый стандарт C++11, знают о существовании в стандартной библиотеке вызова std::async, который позволяет выполнить некий код асинхронно (более точно – поведение указывается первым параметром вызова).

Согласно документации, вызов с параметром std::launch::async обещает выполнить пользовательский код в отдельном потоке. Посмотрим на приведённый ниже код.

    #include <future>
    #include <iostream>
    #include <thread>

     int main(int argc, char* argv[]) {
        int count = 10;

         std::async(std::launch::async, [&count] {
             for(int i=0; i<count; ++i) {
               std::cout << 1;
               std::this_thread::sleep_for(std::chrono::milliseconds(1));
           }
       });
       std::async(std::launch::async, [&count] {
           for(int i=0; i<count; ++i) {
               std::cout << 2;
               std::this_thread::sleep_for(std::chrono::milliseconds(1));
           }
       });

       return 0;
   }

В строках 8-13 запускаем асинхронное выполнение простой lambda-функции, которая должна вывести на экран цифру «1» каждую миллисекунду десять раз. В строках 14-19 запускаем выполнение аналогичной функции, но на этот раз она будет выводить на экран цифру «2». Что можно ожидать на экране по окончанию выполнения программы?

Кто сказал, что «результат не определён»?

Идея такой гипотезы заключается в том, что оба потока будут выполняться параллельно, поэтому вывод на экран перемешается. Мы можем увидеть на экране, например, такую последовательность:

12212121211212211221

Звучит логично, но эта гипотеза неверна. На самом деле на экран гарантированно будет выведена последовательность:

11111111112222222222

Почему? Что произошло?

А произошла принудительная синхронизация двух потоков. Выполнение второго потока (с выводом цифры «2») гарантированно начнётся только после того, как первый поток закончит своё выполнение.

Кто догадается, почему?

На самом деле не всё так просто. Но достаточно задуматься, про что мы забыли в этом примере? А забыли мы про то, что в качестве результата вызов std::async возвращает std::future. Если бы мы написали наш пример следующим образом, то результат на экране стал бы действительно неопределённым:

  #include <future>
  #include <iostream>
  #include <thread>

  int main(int argc, char* argv[]) {
      int count = 10;

      auto future1 = std::async(std::launch::async, [&count] {
          for(int i=0; i<count; ++i) {
            std::cout << 1;
            std::this_thread::sleep_for(std::chrono::milliseconds(1));
        }
    });
    auto future2 = std::async(std::launch::async, [&count] {
        for(int i=0; i<count; ++i) {
            std::cout << 2;
            std::this_thread::sleep_for(std::chrono::milliseconds(1));
        }
    });

    return 0;
}

Вот теперь на экране действительно может быть любая последовательность из перемешанных двадцати цифр 1 и 2. Почему результат так кардинально изменился, стоило нам только лишь сохранить std::future, которое вернул вызов std::async?

Как говорится, всё законно, всё по стандарту

Стандарт гарантирует, что окончание выполнение потока, запущенного вызовом std::async, синхронизировано с вызовом получения результата std::future::get или с освобождением общего состояния (shared state) – области памяти, ответственной за передачу результата между std::async и std::future.

В первом примере автоматическое удаление временного объекта std::future, который был возвращён из первого вызова std::async, приводит к освобождению общего состояния и автоматической синхронизации двух потоков. Просто не сохранив результат вызова std::async, мы получили ожидание – второй поток не начнёт выполнение до окончания выполнения первого потока.

Есть вопрос? Напишите в комментариях!