【C++】マルチスレッド利用の詳しい説明

I.はじめに

1. マルチスレッドの意味

マルチスレッドとは、ソフトウェアまたはハードウェア上で複数のスレッドを同時に実行できるようにするテクノロジを指します。マルチコア CPU をサポートするコンピューターは、実際に複数のプログラム フラグメントを同時に実行できるため、プログラムの処理パフォーマンスが向上します。プログラムでは、このように独立して動作するプログラムの断片を「スレッド」と呼び、これを使ってプログラミングする概念を「マルチスレッド」と呼びます。

2. プロセスとスレッドの違い

プロセスはプログラムの実行中のインスタンスを指し、スレッドはプロセス内の独立した実行フローを指します。プロセスには複数のスレッドを含めることができ、複数のスレッドを同時に実行できます。

  • プログラムにはプロセスが 1 つしかありませんが、少なくとも 1 つのスレッドを持つことができます。
  • 異なるプロセスは異なるアドレス空間を持ち、相互に関連しませんが、異なるスレッドは同じプロセスのアドレス空間を共有します。

2. スレッドを作成する

1.糸

C++ は、主にスレッド ライブラリを使用したマルチスレッド プログラミングをサポートしています<thread>

例 1:スレッド使用std::threadクラスの作成

#include <iostream>
#include <thread>    //必须包含<thread>头文件

void threadFunctionA()
{
    
    
	std::cout << "Run New thread: 1" << std::endl;
}
void threadFunctionB(int n)
{
    
    
	std::cout << "Run New thread: "<< n << std::endl;
}

int main()
{
    
    
	std::cout << "Run Main Thread" << std::endl;

	std::thread newThread1(threadFunctionA);
	std::thread newThread2(threadFunctionB,2);

	newThread1.join();
	newThread2.join();

	return 0;
}
//result
Run Main Thread
Run New thread: 1
Run New thread: 2

上記の例では、newThread1newThread2 という2 つのスレッドを作成し、関数threadFunctionA()と をthreadFunctionB()スレッドの実行関数として使用し、join()関数を使用してスレッドの実行が完了するのを待ちました。

例 2:実行関数に参照パラメータがある

#include <iostream>
#include <thread>    //必须包含<thread>头文件

void threadFunc(int &arg1, int arg2)
{
	arg1 = arg2;
	std::cout << "arg1 = " << arg1 << std::endl;
}

int main()
{
    std::cout << "Run Main Thread!" << std::endl;
	int a = 1;
	int b = 5;
	std::thread newTh(threadFunc, a, b);  //此处会报错
	newTh.join();
	return 0;
}

注:上記のコードをコンパイルすると、コンパイラーは次のエラーを報告します。

错误	C2672	“std::invoke”: 未找到匹配的重载函数
错误	C2893	未能使函数模板“unknown-type std::invoke(_Callable &&,_Types &&...) noexcept(<expr>)”专用化

これは、スレッドがパラメーターを渡すときに、それらを rvalues として渡すためです。左辺値を渡したい場合は、std::refと を使用できます。std::cref

  • std::ref参照によって渡された値を右辺値としてラップできます。
  • std::crefconst参照によって渡された値を右辺値としてラップできます。

したがって、例 2 のコードは次のように変更できます。

#include <iostream>
#include <thread>    //必须包含<thread>头文件

void threadFunc(int &arg1, int arg2)
{
    
    
	arg1 = arg2;
	std::cout << "New Thread arg1 = " << arg1 << std::endl;
}

int main()
{
    
    
	std::cout << "Run Main Thread!" << std::endl;
	int a = 1, b = 5;
	std::thread newTh(threadFunc, std::ref(a), b);  //使用ref
	newTh.join();
	return 0;
}
//result
Run Main Thread!
arg1 = 5

2. join() と detach()

C++ では、スレッドが作成されると、それは通常、結合可能スレッドと呼ばれ、関数または関数(joinable)を呼び出すことによってスレッドの実行を管理できます。join()detach()

方法 説明する
1 参加する() スレッドが完了するまで待ちます。スレッドが実行を完了していない場合、現在のスレッド (通常はメイン スレッド) はブロックされます。スレッドが実行を完了するまで、メイン スレッドは実行を続行しません
2 デタッチ() 現在のスレッドを作成されたスレッドから分離して、別々に実行できるようにします。分離されたスレッドの実行が完了すると、システムは自動的にリソースを再利用します。スレッドが切り離されると、join()スレッドを結合できなくなるため、関数は使用できなくなります。
3 参加可能() スレッドがjoin()関数を実行してtrue/falseを返すことができるかどうかを判断します。

例 3:

#include <iostream>
#include <thread>
#include <windows.h>

void foo()
{
    
    
	std::cout << "Run New thread!\n";
	Sleep(2000);   		//需要头文件<windows.h>
}

int main()
{
    
    
	std::thread t(foo);

	if (t.joinable())
	{
    
    
		t.join();  		// 等待线程t执行完毕

		// t.detach();  // 分离线程t与主线程
	}

	std::cout << "Run Main thread!\n";
	return 0;
}

上の例では、参加可能なスレッドが作成されtスレッドの実行が完了するt.join()までメイン スレッドはブロックされます。別々のスレッドtを使用する場合、それらは同時に実行され、メインスレッドはブロックされません。t.detach()t

知らせ:

  • スレッドは、関数が呼び出されたときではなく、スレッドjoin()オブジェクトが定義されたときに実行を開始します。関数の呼び出しはjoin()ブロックされるだけで、スレッドが終了してリソースが再利用されるまで待機します。
  • 切り離されたスレッド (detach()実行されたスレッド) は、それを呼び出したスレッドが終了するか、スレッド自体が終了すると、リソースを自動的に解放します。
  • スレッドは、関数の実行終了後に自動的に解放されます。他の方法を使用してスレッドを強制的に終了することはお勧めできません。リソースが解放されないため、メモリ リークが発生する可能性があります。
  • 実行中のスレッドがない場合join()、またはdetach()プログラムの最後に例外がスローされます。

3. このスレッド

C++ では、this_threadクラスは現在のスレッドに関するいくつかの関数を提供します。詳細は次のとおりです。

使用 説明する
1 std::this_thread::sleep_for() 現在のスレッドは指定された時間スリープします
2 std::this_thread::sleep_until() 現在のスレッドは指定された時点までスリープします
3 std::this_thread::yield() 現在のスレッドは CPU を放棄し、他のスレッドが実行できるようにします
4 std::this_thread::get_id() 現在のスレッドのIDを取得します

さらに、 2 つのスレッドが等しいかどうかを比較するために、オーバーロードされた演算子の合計this_threadが含まれています==!=

例 4:

#include <iostream>
#include <thread>
#include <chrono>

void my_thread()
{
    
    
	std::cout << "Thread " << std::this_thread::get_id() << " start!" << std::endl;

	for (int i = 1; i <= 5; i++)
	{
    
    
		std::cout << "Thread " << std::this_thread::get_id() << " running: " << i << std::endl;
		std::this_thread::yield();	// 让出当前线程的时间片
		std::this_thread::sleep_for(std::chrono::milliseconds(200));  // 线程休眠200毫秒
	}

	std::cout << "Thread " << std::this_thread::get_id() << " end!" << std::endl;
}

int main()
{
    
    
	std::cout << "Main thread id: " << std::this_thread::get_id() << std::endl;
	
    std::thread t1(my_thread);
	std::thread t2(my_thread);
	
	t1.join();
	t2.join();
	return 0;
}
//result 程序输出的结果可能如下:
Main thread id: 43108
Thread 39272 start!
Thread 33480 start!
Thread 33480 running: 1
Thread 39272 running: 1
Thread 33480 running: 2
Thread 39272 running: 2
Thread 33480 running: 3
Thread 39272 running: 3
Thread 33480 running: 4
Thread 39272 running: 4
Thread 39272 running: 5
Thread 33480 running: 5
Thread 39272 ends
Thread 33480 ends

3.std::mutex

マルチスレッド プログラミングでは、次の問題に注意する必要があります。

  • データ競合やその他の問題を防ぐために、スレッド間の共有データへのアクセスを同期する必要があります。ミューテックス条件変数などのメカニズムを同期に使用できます。
  • デッドロックの問題が発生する場合があります。この問題では、複数のスレッドが互いのロックの解除を待機し、プログラムの実行を続行できなくなります。
  • 競合状態が発生する可能性があり、複数のスレッドの実行順序が不確実になります。

1.lock()とunlock()

std::mutexC++11 の最も基本的なミューテックスです。スレッドがミューテックスをロックすると、このスレッドがミューテックスのロックを解除するまで、他のスレッドはミューテックスを操作できなくなります。

方法 説明する
1 ロック() ミューテックスをロックしますミューテックスが別のスレッドによってすでにロックされている場合は、ロックが解除されるまでブロックされます。ミューテックスが同じスレッドによってすでにロックされている場合は、デッドロックが発生します
2 ロック解除() ミューテックスのロックを解除し、その所有権を解放します。呼び出しをロックできないためにスレッドがlock()ブロックされている場合、この関数を呼び出すと、ミューテックスの主導権がランダムにいずれかのスレッドに与えられます。ミューテックスがこのスレッドによってロックされていない場合は、未定義の例外がスローされます。
3 try_lock() ミューテックスをロックしてみますミューテックスがロックされていない場合はロックしてtrueを返し、ミューテックスがロックされている場合はfalseを返します。

例:ミューテックスの使用

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

std::mutex mtx;
int num = 0;

void thread_function(int &n)
{
    
    
	for (int i = 0; i < 100; ++i)
	{
    
    
		mtx.lock();
		n++;
		mtx.unlock();
	}
}

int main()
{
    
    
	std::thread myThread[500];
	for (std::thread &a : myThread)
	{
    
    
		a = std::thread(thread_function, std::ref(num));
		a.join();
	}

	std::cout << "num = " << num << std::endl;
	std::cout << "Main thread exits!" << std::endl;
	return 0;
}
//result
num = 50000
Main thread exits!

注:ミューテックスを使用する場合は、次の問題に注意する必要があります。

  • ロックとロック解除の順序は同じである必要があります。
  • ロックを取得しないと共有データを操作できません。
  • std::mutexは共有リソースへのアクセスを制御するために使用されるため、プログラムのパフォーマンスに影響を与える可能性があります。プログラムのパフォーマンスを最適化する必要がある場合は、ロックフリー プログラミングやその他のテクノロジの使用を検討できます。

2.ロックガード

std::lock_guardC++標準ライブラリのテンプレートクラスであり、リソースの自動ロック・ロック解除を実現するために使用されます。これはRAII (リソースの取得は初期化である) の設計コンセプトに基づいており、スコープが終了するとロック リソースが自動的に解放され、手動によるロック管理の複雑さと発生する可能性のあるエラーを回避できます。

std::lock_guard主な特徴は以下のとおりです。

  • 自動ロック:オブジェクトが作成されるとstd::lock_guard、指定されたミューテックスがただちにロックされます。これにより、スコープに入った後にミューテックスが確実にロックされ、リソースへの同時アクセスの競合状態が回避されます。
  • 自動ロック解除:std::lock_guardオブジェクトがスコープ内で終了すると、ミューテックスは自動的に解放されます。スコープが通常のプロセスで終了するか、例外がスローされるか、returnステートメントを使用して早期に戻るかに関係なく、std::lock_guardミューテックスが正しくロック解除されることが保証され、リソース リークやデッドロックのリスクが回避されます。
  • ローカル ロックに適しています:std::lock_guardスタック上のオブジェクトを通じて実装されるため、ローカル スコープ内のミューテックスをロックするのに適しています。オブジェクトのスコープを超えるとstd::lock_guard、ミューテックスは自動的にロックが解除され、制御が解放されます。

使用するための一般的な手順はstd::lock_guard次のとおりです。

  1. オブジェクトを作成しstd::lock_guard、ロックするミューテックスをパラメータとして渡します。
  2. ロック保護が必要なコード ブロックを実行します。
  3. std::lock_guardオブジェクトのスコープが終了すると、ミューテックスのロックを解除するためにデストラクターが自動的に呼び出されます。

例:

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

std::mutex mtx;  // 互斥量

void thread_function()
{
    
    
    std::lock_guard<std::mutex> lock(mtx);  // 加锁互斥量
    std::cout << "Thread running" << std::endl;
    // 执行需要加锁保护的代码
}  // lock_guard对象的析构函数自动解锁互斥量

int main()
{
    
    
    std::thread t1(thread_function);
    t1.join();
    std::cout << "Main thread exits!" << std::endl;
    return 0;
}

上記の例では、std::lock_guardオブジェクトはミューテックスlockをロックし、出力ステートメントの実行を保護します。thread_function終了するとthread_functionlock_guardオブジェクトのデストラクターが自動的にミューテックスのロックを解除します。これにより、ミューテックスが適切なタイミングでロックおよびロック解除されるようになり、複数のスレッド間の競合の問題が回避されます。

全体として、std::lock_guardミューテックスのロックとロック解除を管理するためのシンプルかつ安全な方法を提供し、マルチスレッド プログラミングをより便利で信頼性の高いものにします。


3. unique_lock

std::unique_lockこれは、C++ 標準ライブラリのテンプレート クラスであり、より柔軟なミューテックスのロックおよびロック解除操作を実装するために使用されます。std::lock_guardより多くの機能と柔軟性を提供します。

std::unique_lock主な特徴は以下のとおりです。

  • 自動ロックとロック解除:オブジェクトの作成時に指定されたミューテックスを即座にロックして、ミューテックスが確実にロックされるようにするのと同様std::lock_guardです。std::unique_lockオブジェクトの存続期間が終了すると、ミューテックスは自動的にロック解除されます。この自動ロックおよびロック解除メカニズムにより、手動ロック管理の複雑さと起こり得るエラーが回避されます。

  • 柔軟なロックとロック解除をサポート:std::lock_guard自動ロックとロック解除と比較して、std::unique_lockより柔軟な方法を提供します。必要に応じてミューテックスを手動でロックおよびロック解除できるため、コードのさまざまなブロックでミューテックスに対する複数のロックおよびロック解除操作が可能になります。

  • 遅延ロックと条件変数のサポート:std::unique_lock遅延ロックもサポートされており、すぐにロックせずにオブジェクトを作成し、必要に応じて後でロック操作を実行できます。さらに、条件変数 ( std::condition_variable) とともに使用して、より複雑なスレッド同期および待機メカニズムを実装することもできます。

使用するための一般的な手順はstd::unique_lock次のとおりです。

  1. オブジェクトを作成しstd::unique_lock、ロックするミューテックスをパラメータとして渡します。
  2. ロック保護が必要なコード ブロックを実行します。
  3. lock必要に応じて、関数を呼び出してミューテックスを手動でロックするか、unlock関数を呼び出してミューテックスを手動でロック解除します。

例:

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

std::mutex mtx;  // 互斥量

void thread_function()
{
    
    
    std::unique_lock<std::mutex> lock(mtx);  // 加锁互斥量
    std::cout << "Thread running" << std::endl;
    // 执行需要加锁保护的代码
    lock.unlock();  // 手动解锁互斥量
    // 执行不需要加锁保护的代码
    lock.lock();  // 再次加锁互斥量
    // 执行需要加锁保护的代码
}  
// unique_lock对象的析构函数自动解锁互斥量

int main()
{
    
    
    std::thread t1(thread_function);
    t1.join();
    std::cout << "Main thread exits!" << std::endl;
    return 0;
}

上記の例では、std::unique_lockオブジェクトは、lockミューテックスの作成時に自動的にミューテックスをロックし、破棄時にミューテックスのロックを自動的に解除します。lock呼び出しや関数により施錠・解錠のタイミングを手動で制御することができ、unlockより柔軟な運用を実現します。

全体として、std::unique_lockより柔軟で機能豊富なミューテックスのロックおよびロック解除メカニズムを提供し、マルチスレッド プログラミングをより便利かつ安全にします。これは、複雑な同期要件、遅延ロック、および条件変数との組み合わせを処理する場合に非常に役立ちます。


四、条件変数

std::condition_variableこれは C++ 標準ライブラリのクラスで、マルチスレッド プログラミングで条件変数とスレッド間のスレッド同期を実装するために使用されます。これは待機および通知メカニズムを提供し、スレッドが特定の条件が確立されるまで待機して起動したり、特定の条件が満たされたときに待機中の他のスレッドに通知したりできます。スレッドを待機および通知するための次の関数を提供します。

方法 説明する
1 待って notify_one()他のスレッドまたは関数によって起動されるまで、現在のスレッドを待機状態にしますnotify_all()この関数はパラメータとしてミューテックス ロックを必要とし、呼び出されるとミューテックス ロックは自動的に解放され、ウェイクアップ後に再取得されます。
2 を待つ wait_for()notify_one(): 現在のスレッドを待機状態にし、他のスレッドまたはnotify_all()関数によって起動されるか、待機がタイムアウトになるまで、最大一定時間待機します。この関数にはパラメータとしてミューテックスロックと期間が必要ですが、復帰する際にはタイムアウトを待つ場合std::cv_status::timeoutと覚醒して復帰する場合の2つの状況がありますstd::cv_status::no_timeout
3 wait_until wait_until()notify_one(): 現在のスレッドは、他のスレッドまたはnotify_all()関数によって起動されるか、待機時間が指定された絶対時点に達するまで待機状態になります。この関数はパラメータとしてミューテックスロックと絶対時刻を必要とし、復帰時には時刻が来たら復帰する場合std::cv_status::timeoutと覚醒時に復帰する場合の2つの状況がありますstd::cv_status::no_timeout
4 通知ワン notify_one(): 待機中のスレッドを起動します。複数のスレッドが待機している場合は、起動するスレッドの 1 つを選択します。
5 通知すべて notify_all(): 待機中のすべてのスレッドをウェイクアップし、待機状態から戻します。

std::condition_variable主な特徴は以下のとおりです。

  • 待機および通知メカニズム:std::condition_variableスレッドが待機状態に入り、特定の条件が満たされるまで起動されないようにします。スレッドはwait関数を呼び出して待機状態に入り、パラメータとしてミューテックスを指定して、スレッドの待機中にミューテックスが確実にロックされるようにすることができます。他のスレッドが条件を満たしてnotify_oneornotify_all関数を呼び出すと、待機中のスレッドが起動されて実行を継続します。

  • ミューテックスと併用:スレッド間の相互排他性を確保するには、ミューテックス (または)std::condition_variableとともに使用する必要があります。待機する前に、スレッドは競合状態を避けるためにまずミューテックスをロックする必要があります。条件が満たされた場合、待機中の他のスレッドに通知する前に、ミューテックスを再度ロックする必要があります。std::mutexstd::unique_lock<std::mutex>

  • タイムアウト待機のサポート:std::condition_variable待機関数wait_forとタイムアウト パラメーターを提供しwait_until、一定時間待機した後にスレッドを自動的に起動できるようにします。これは、タイムアウト状況や時間指定された待機を処理する場合に役立ちます。

使用するための一般的な手順はstd::condition_variable次のとおりです。

  1. オブジェクトを作成しますstd::condition_variable
  2. ミューテックス オブジェクト (std::mutexまたはstd::unique_lock<std::mutex>) を作成します。
  3. 待機スレッドでは、std::unique_lockロックミューテックスを使用してwait関数を呼び出し、待機状態に移行します。
  4. 起動中のスレッドでは、std::unique_lockロック ミューテックスを使用し、notify_oneまたはnotify_all関数を呼び出して待機中のスレッドに通知します。
  5. 待機中のスレッドが起動された後、対応する操作の実行を続けます。

例:

#include <iostream>
#include <thread>
#include <condition_variable>

std::mutex mtx;  // 互斥量
std::condition_variable cv;  // 条件变量
bool isReady = false;  // 条件

void thread_function()
{
    
    
    std::unique_lock<std::mutex> lock(mtx);
    while (!isReady) 
    {
    
    
        cv.wait(lock);  // 等待条件满足
    }
    std::cout << "Thread is notified" << std::endl;
}

int main()
{
    
    
    std::thread t(thread_function);

    // 模拟一段耗时操作
    std::this_thread::sleep_for(std::chrono::seconds(2));

    {
    
    
        std::lock_guard<std::mutex> lock(mtx);
        isReady = true;  // 设置条件为true
    }
    cv.notify_one();  // 通知等待的线程

    t.join();

    return 0;
}

cv.wait(lock)上記の例では、条件が満たされるのを待って待機状態のスレッドが作成されます。メインスレッドはしばらくしてから条件を設定しtruecv.notify_one()待機中のスレッドに通知します。待機中のスレッドは起動後にメッセージを出力します。


5. std::atomic

std::mutexマルチスレッドのリソース競合の問題は非常にうまく解決できますが、サイクルごとにロックとロック解除を行う必要があり、確実に多くの時間を無駄にします。

C++ では、std::atomicアトミック操作を提供するために使用されるクラスです。アトミック(元の意味はatomic )。アトミック操作は、並列化できない最小の操作です。これは、複数のスレッドがある場合でも、アトミック オブジェクトを同期的に操作する必要があることを意味し、ミューテックスのロックとロック解除にかかる時間の消費を排除します。

を使用するとstd::atomic、動作中にデータが他のスレッドによって変更されないようにできるため、データの競合が回避され、複数のスレッドから同時にアクセスされた場合でもプログラムを正しく実行できます。

例:

#include <iostream>
#include <thread>
#include <mutex>
#include <atomic>   //必须包含

std::atomic_int num = 0;

void thread_function(std::atomic_int &n)  //修改类型
{
    
    
	for (int i = 0; i < 100; ++i)
	{
    
    
		n++;
	}
}

int main()
{
    
    
	std::thread myThread[500];
	for (std::thread &a : myThread)
	{
    
    
		a = std::thread(thread_function, std::ref(num));
		a.join();
	}

	std::cout << "num = " << num << std::endl;
	std::cout << "Main thread exits!" << std::endl;
	return 0;
}
//result
num = 50000
Main thread exits!

説明:std::atomic_intはい、std::atomic<int>エイリアスです。


この記事が参考になったら「いいね!」をお願いします!

ここに画像の説明を挿入します

おすすめ

転載: blog.csdn.net/AAADiao/article/details/131385108