c++ std::asyncで非同期実行

std::asyncで関数の非同期実行ができる。

std::asyncの戻り値であるfutureを使って、

  • 関数の実行終了を待つ (wait)
  • 関数の実行終了をタイムアウト時間指定で待つ (wait_for)
  • 関数の実行結果を受け取る (get)

ことができる。

また、std::asyncの実行ポリシーによって

  • 別スレッドで実行する
  • すぐには実行せず、後で実行する

ことができる。

#include <iostream>
#include <future>
#include <chrono>

using namespace std::literals;
int main(){
    {
        std::cout << "start " << std::chrono::system_clock::now() << '\n';
        auto f = std::async(std::launch::async, [](){
            std::cout << "thread start " << std::chrono::system_clock::now() << '\n';
            std::this_thread::sleep_for(2s);
        });
    
        // 終了を待つ
        f.wait();
        std::cout << "end " << std::chrono::system_clock::now() << "\n\n";
    }
    {
        std::cout << "start " << std::chrono::system_clock::now() << '\n';
        auto f = std::async(std::launch::async, [](){
            std::cout << "thread start " << std::chrono::system_clock::now() << '\n';
            std::this_thread::sleep_for(2s);
        });
    
        // 終了を待つ (ただし、1秒でタイムアウト)
        auto result = f.wait_for(1s);
        std::cout << ((result == std::future_status::timeout) ? "timeout" : "") << '\n';
        std::cout << "end " << std::chrono::system_clock::now() << "\n\n";
    }
    {
        std::cout << "start " << std::chrono::system_clock::now() << '\n';
        auto f = std::async(std::launch::async, []() -> int{
            std::cout << "thread start " << std::chrono::system_clock::now() << '\n';
            std::this_thread::sleep_for(2s);
            return 123;
        });
    
        // 終了を待って結果を取得 
        auto result = f.get();
        std::cout << result << '\n';
        std::cout << "end " << std::chrono::system_clock::now() << "\n\n";
    }
    {
        std::cout << "start " << std::chrono::system_clock::now() << '\n';
        // 遅延実行
        auto f = std::async(std::launch::deferred, []() -> int{
            std::cout << "thread start " << std::chrono::system_clock::now() << '\n';
            std::this_thread::sleep_for(2s);
            return 123;
        });
    
        std::this_thread::sleep_for(1s);
        // 終了を待って結果を取得 
        auto result = f.get();
        std::cout << result << '\n';
        std::cout << "end " << std::chrono::system_clock::now() << "\n\n";
    }
/* 
start 2022-09-12 03:37:02.2842713
thread start 2022-09-12 03:37:02.2899610
end 2022-09-12 03:37:04.2908115

start 2022-09-12 03:37:04.2909026
thread start 2022-09-12 03:37:04.2910574
timeout
end 2022-09-12 03:37:05.2954522

start 2022-09-12 03:37:06.3097559
thread start 2022-09-12 03:37:06.3099622
123
end 2022-09-12 03:37:08.3127969

start 2022-09-12 03:37:08.3128716
thread start 2022-09-12 03:37:09.3226742
123
end 2022-09-12 03:37:11.3310814
*/
}

std::format

std::format基本

printf()と同じ感覚で、書式と変数を指定する。置換フィールド({と}で囲まれた部分)に置換フィールド書式を指定するが、デフォルトの書式で良ければ省略できる。

#include <format>
#include <iostream>

int main(){
    auto i{5};
    auto str{"abc"};
    std::cout << std::format("i={}, str={}\n'", i, str);    // =>  "i=5, str=abc"
}

置換フィールドに書式を指定する例

#include <format>
#include <iostream>

int main(){
    auto i{15};
    // 6桁の16進表現,先行0埋め
    std::cout << std::format("i={:#06x}\n", i); // => "i=0x000f"
}

独自のフォーマットを定義する

#include <format>
#include <iostream>

struct coord {
    float x_{0.0};
    float y_{0.0};
    float z_{0.0};
};

// coord用のカスタムフォーマット定義
template <>
struct std::formatter<coord> {
    constexpr auto parse(std::format_parse_context& ctx) {
        return ctx.begin();
    }

    auto format(const coord& obj, std::format_context& ctx) {
        return std::format_to(ctx.out(), "x={},y={},z={}", obj.x_, obj.y_, obj.z_);
    }
};

int main(){
    coord c{10.0, 20.5, 30.0};

    std::cout << std::format("{}", c); // => "x=10,y=20.5,z=30"
}

c++ modules

MSVCでのサポート

https://en.cppreference.com/w/cpp/compiler_support によるとMSVCでは19.28でサポートされている。

Visual Studioの設定

https://docs.microsoft.com/ja-jp/cpp/build/reference/experimental-module?view=msvc-170 に従い、 /std:c++20 experimental:module を指定する。

モジュールのエクスポート

拡張子はixx

export module mylib;

namespace mylib {
    export int myfunc() {
        return 999;
    }

    export class myclass {
    public:
        int myfunc() {
            return 888;
        }
    };
};

呼び出し側

import mylib;
...
std::cout << mylib::myfunc() << "\n";

mylib::myclass c;
std::cout << c.myfunc() << "\n";

c++ 小ネタ

if文内で初期化できる

if(auto i = 5 * -10 * 3; i < 0){
    std::cout << i << " < 0" << '\n';
}

(switch文も同様)

initializer_listを関数に渡す

void disp_all(std::initializer_list<int> il){
    for(auto i: il)
        std::cout << i << " ";
}

disp_all({2,4,6,8});

推測可能な場合、テンプレートの引数は省略できる

 std::vector v;               // 型がわからないのでコンパイルエラー
 std::vector vi{1,2,3};       // 推測可能
 std::vector vs{"1","2","3"}; // 推測可能

std::source_locationを使えば、FILELINEを使っていた処理を少しスマートに書ける。

void log(const std::string & msg, 
    std::source_location s = std::source_location::current()) {
    std::cout << msg 
        << " " << s.file_name() 
        << " " << s.function_name() 
        << " " << s.line() 
        << std::endl;
}

int main(){
    ...
    log("hello");
    ...
}

pairやtupleの要素をautoで受ける

auto coord = std::make_pair(10.2, 30.4);
auto[x,y] = coord;

auto t = std::make_tuple("abc", 1024, "xyz");
auto[type, length, value] = t;
std::map<int, std::string> m{{1,"abc"},{2,"def"}};
for(auto [key, value] : m)
    std::cout << std::format("key={} value={}\n", key, value)

for文内で要素を初期化

 for(std::array a{2,4,6,8}; auto i : a)
        std::cout << i << " ";

std::iota()で連番を設定

    std::array<int,10> a;
    std::iota(a.begin(), a.end(),10); // 10から始まる連番を設定
    for(auto i : a)
        std::cout << i << '\n'; 

桁数の大きい数値は'(アポストロフィー)で区切れる

int i{1'234'567'890}; // int i{1234567890}; と等価

std::ranges:size()を使えば、生配列のサイズも取得できる

int array[100];
std::cout << std::ranges::size(array); // 100

畳み込み式で可変引数

#include <iostream>
#include <string>

using namespace std::literals;

template <typename ... T>
auto add(const T& ... param){
    return (param + ...);
}

int main() {
    auto s = "abc"s;
    std::cout << add(1,2,3) << '\n';            // => 6
    std::cout << add(1.1, 2.2, 3.3) << '\n';    // => 6.6
    std::cout << add(1.1, 2.2, 3) << '\n';      // -> 6.4
    std::cout << add("abc"s, "def"s, "xyz"s) << '\n'; // -> "abcdefxyz"
}

c++ jthread

自動join

std::threadでは、スレッドが終了する前にthreadオブジェクトが破棄されるとstd::terminate()が呼ばれる。そのため、join()を呼んで、スレッドの終了を待つ必要がある。 (スレッドの管理が必要ないのであれば、detach()でもよい)

TEST(thread, cpp20) {
    using namespace::std::literals;

    int x{0};
    std::thread t([&]() {
            std::cout << "thread start" << std::endl;
            while(x < 10){
                std::this_thread::sleep_for(0.5s);
                std::cout << "x:" << x++ << std::endl;
            }
            std::cout << "thread exit" << std::endl;
            });

    std::this_thread::sleep_for(2s);
    t.join(); // <<<<<<<< スレッドの終了を待つ
}

std::jthreadでは、jthreadオブジェクトが破棄される際にjoin()が自動的に呼ばれるので、明示的にjoin()を呼ぶ必要はない。

TEST(jthread, cpp20) {
    using namespace::std::literals;

    int x{0};
    std::jthread t([&]() {
            std::cout << "thread start" << std::endl;
            while(x < 10){
                std::this_thread::sleep_for(0.5s);
                std::cout << "x:" << x++ << std::endl;
            }
            std::cout << "thread exit" << std::endl;
            });

    std::this_thread::sleep_for(2s);
    // スレッドが終了するまで、自動的に待つ
}

スレッドのキャンセル

request_stop()の呼び出しで、スレッドをキャンセルできる。

TEST(jthread_cancel, cpp20) {
    using namespace::std::literals;

    int x = 0, y = 0;
    std::jthread t([&](std::stop_token token) {
            std::cout << "thread start" << std::endl;
            while(x < 10){
                // 停止要求があれば終了
                if(token.stop_requested()){
                    std::cout << "stop requested" << std::endl;
                    break;
                }
                std::this_thread::sleep_for(0.5s);
                std::cout << "x:" << x++ << std::endl;
            }
            std::cout << "thread exit" << std::endl;
            });

    // スレッドに対する停止要求で呼び出されるコールバック
    std::stop_callback sc { t.get_stop_token(), [&] {
        std::cout << "stop_callback" << std::endl;
        y++;
    }};
    ASSERT_EQ(x , 0);

    std::this_thread::sleep_for(2s);
    t.request_stop(); // スレッド停止要求

    ASSERT_EQ(y , 1);
}

std::latchによるスレッド同期

std::latchを使って、値が0になるまで待つ。

TEST(latch, cpp20) {
    using namespace::std::literals;

    const int num_threads = 5;
    std::latch l(num_threads);
    std::vector<std::jthread> threads;

    // 5つのスレッドを起動
    for(int i = 0 ; i < num_threads ; i++)
        threads.push_back(std::jthread([i, &l](){
            std::cout << "thread " << i << " start" << std::endl;
            std::this_thread::sleep_for(i * 1s);
            std::cout << "thread " << i << " exit" << std::endl;

            l.count_down(); // 1つ減らす
        }));

    l.wait(); // latchが0になるまで待つ
    std::cout << "all threads exit" << std::endl;
}

std::counter_semaphoreによる同時実行数の制限

std::counter_semaphoreを使って、特定の処理を同時に実行できるスレッドの数を制限できる。

TEST(semaphore, cpp20) {
    using namespace::std::literals;

    const int num_threads = 10;
    const int num_semaphre = 3;
    std::counting_semaphore semaphore(num_semaphre);
    std::vector<std::jthread> threads;
    std::atomic<int> counter;

    // 10のスレッドを起動
    for(int i = 0 ; i < num_threads ; i++)
        threads.push_back(std::jthread([i, num_semaphre, &semaphore, &counter](){
            semaphore.acquire(); // 空きを待つ
            std::cout << "counter=" << ++counter << std::endl;
            std::cout << "thread " << i << " acquired" << std::endl;
            ASSERT_LE(counter, num_semaphre); // 3以下のはず
            std::this_thread::sleep_for(i * 1s);
            counter--;
            std::cout << "thread " << i << " exit" << std::endl;
            semaphore.release(); // セマフォ開放
        }));
}