コンテンツにスキップ

最終更新:2026-10-12

C++ マルチスレッド入門

本文中のすべてのコードは《C++那些事》リポジトリにあります。

https://github.com/Light-City/CPlusPlusThings

1.スレッドとプロセスを理解する

スレッドはスケジューリングの基本単位であり、プロセスはリソース割り当ての基本単位です。1つのプログラムをプロセスと捉えることができ、プロセスは複数のスレッドを含みます。

例えば:ブラウザが1つのプロセスであり、タブを1つ開くごとに1つのスレッドです。

両者の簡単な違い:

  • アドレス空間とその他のリソース:プロセス間は相互に独立しており、同一プロセス内の各スレッド間で共有されます。あるプロセス内のスレッドは、他のプロセスからは見えません。
  • 通信:プロセス間通信にはIPCを使い、スレッド間ではプロセスのデータセグメント(グローバル変数など)を直接読み書きして通信できます。ただし、データの整合性を保証するために、プロセスの同期・排他の手段を併用する必要があります。
  • スケジューリングと切り替え:スレッドのコンテキストスイッチは、プロセスのコンテキストスイッチよりはるかに高速です。
  • **マルチスレッドOS**では、プロセスは実行可能なエンティティではありません。

IPC通信とスレッド間通信については、後日新しい記事を書く予定です。

2.スレッドを作成する5つの方法

  • 関数ポインタ
  • Lambda関数
  • Functor(関数オブジェクト)
  • 非静的メンバ関数
  • 静的メンバ関数

2.1 関数ポインタ

// 1.函数指针
void fun(int x) {
    while (x-- > 0) {
        cout << x << endl;
    }
}
// 调用
std::thread t1(fun, 10);
t1.join();

2.2 Lambda関数

// 注意:如果我们创建多线程 并不会保证哪一个先开始
int main() {
    // 2.Lambda函数
    auto fun = [](int x) {
        while (x-- > 0) {
            cout << x << endl;
        }
    };
//    std::1.thread t1(fun, 10);
    // 也可以写成下面:
    std::thread t1_1([](int x) {
        while (x-- > 0) {
            cout << x << endl;
        }
    }, 11);
//    std::1.thread t2(fun, 10);
//    t1.join();
    t1_1.join();
//    t2.join();
    return 0;
}

2.3 関数オブジェクト(Functor)

// 3.functor (Funciton Object)
class Base {
public:
    void operator()(int x) {
        while (x-- > 0) {
            cout << x << endl;
        }
    }
};
// 调用
thread t(Base(), 10);
t.join();

2.4 非静的メンバ関数

// 4.Non-static member function
class Base {
public:
    void fun(int x) {
        while (x-- > 0) {
            cout << x << endl;
        }
    }
};
// 调用
thread t(&Base::fun,&b, 10);
t.join();

2.5 静的メンバ関数

// 4.Non-static member function
class Base {
public:
    static void fun(int x) {
        while (x-- > 0) {
            cout << x << endl;
        }
    }
};
// 调用
thread t(&Base::fun, 10);
t.join();

3.joinとdetach

3.1 join

  • 一度スレッドが開始されたら、スレッドの完了を待つには、そのオブジェクトに対してjoin()を呼び出す必要があります
  • 二重joinはプログラムの終了を招きます
  • joinの前に、joinable()を使ってjoin可能かどうかを確認すべきです
void run(int count) {
    while (count-- > 0) {
        cout << count << endl;
    }
    std::this_thread::sleep_for(chrono::seconds(3));
}

int main() {
    thread t1(run, 10);
    cout << "main()" << endl;
    t1.join();
    if (t1.joinable()) {
        t1.join();
    }
    cout << "main() after" << endl;
    return 0;
}

3.2 detach

  • これは新しく作成したスレッドを親スレッドから分離するために使います
  • スレッドを分離する前に、必ずjoinableかどうかを確認してください。確認しないと2回分離される可能性があり、二重detach()はプログラムの終了を招きます
  • 分離したスレッドがあり、main関数がreturnしようとしている場合、その分離されたスレッドの実行は中断されます
void run(int count) {
    while (count-- > 0) {
        cout << count << endl;
    }
    std::this_thread::sleep_for(chrono::seconds(3));
}

int main() {
    thread t1(run, 10);
    cout << "main()" << endl;
    t1.detach();
    if(t1.joinable())
        t1.detach();
    cout << "main() after" << endl;
    return 0;

4.クリティカルセクションとミューテックス

4.1 クリティカルセクション(Critical Sections)とは?

クリティカルセクションとは、プログラムを正しく動作させるために、一度に1つのスレッドだけが実行できるコードの区間のことです。2つのスレッド(またはプロセス)が同時にクリティカルセクション内のコードを実行すると、プログラムは正しい動作をしなくなる可能性があります。

4.2 ただ変数をインクリメントするだけでもクリティカルセクションなのか?

場合によります。

変数のインクリメント(i ++)には、3つのステップがあります:

  • メモリの内容をCPUレジスタへコピーする。 load
  • CPU内でその値をインクリメントする。 increment
  • 新しい値をメモリに格納する。 store

そのメモリ位置(例えば下の変数i)へのアクセスが1つのスレッドからしか行われない場合、競合は発生せず、iに関連するクリティカルセクションも存在しません。しかしsum変数はグローバル変数であり、2つのスレッドからアクセスされる可能性があります。2つのスレッドが同時に変数をインクリメントしようとするかもしれません。

#include <iostream>
#include <mutex>
#include <thread>

using namespace std;

int sum = 0; //shared

mutex m;

void *countgold() {
    int i; //local to each thread
    for (i = 0; i < 10000000; i++) {
        sum += 1;
    }
    return NULL;
}

int main() {
    thread t1(countgold);
    thread t2(countgold);

    //Wait for both threads to finish
    t1.join();
    t2.join();

    cout << "sum = " << sum << endl;
    return 0;
}

上記コードの典型的な出力は、sumの合計が20000000となります。競合条件が存在するため、プログラムを実行するたびに異なる合計が出力されます。このコードは、2つのスレッドが同時に合計を読み書きすることを防いでいません。例えば、2つのスレッドがどちらもsumの現在の値(123としましょう)を、それぞれのスレッドが動作しているCPUへコピーします。2つのスレッドがそれぞれ自分のコピーを1つインクリメントします。2つのスレッドがその値(124)を書き戻します。もしスレッドが異なるタイミングで合計にアクセスしていれば、カウントは125になっていたでしょう。

4.3 グローバル変数へのアクセスを一度に1つのスレッドだけに保証するには?

あるスレッドが現在クリティカルセクションにいるとき、別のスレッドには最初のスレッドが完了するまで待ってほしいものです。そのために、ミューテックス(相互排他の略称)を使用できます。

ミューテックスのわかりやすい例え話:

bg2013042408

他人の侵入を防ぐシンプルな方法は、入り口に鍵をかけることです。先に来た人が鍵をかけ、後から来た人は鍵がかかっているのを見て入り口で並び、鍵が開くのを待ってから入ります。これが"ミューテックス"(Mutual exclusion、略称Mutex)で、複数のスレッドが同時に同じメモリ領域を読み書きするのを防ぎます。

m.lock();
sum += 1;
m.unlock();

上記のコードなら、正しくsum = 20000000と出力されます。

参考文献

http://www.ruanyifeng.com/blog/2013/04/processes_and_threads.html?utm_source=com.ideashower.readitlater.pro&utm_medium=social&utm_oi=35626384621568

https://www.youtube.com/watch?v=eZ8yKZo-PGw&list=PLk6CEY9XxSIAeK-EAh3hB4fgNvYkYmghp&index=4

関連トピック

コメント