ホームページ  >  記事  >  バックエンド開発  >  C++11 マルチスレッド プログラミングの基礎の概要

C++11 マルチスレッド プログラミングの基礎の概要

无忌哥哥
无忌哥哥オリジナル
2018-07-19 09:52:332267ブラウズ

1. C 11 で新しいスレッドを作成する

すべての C アプリケーションには、main 関数であるデフォルトのメインスレッドがあります。C 11 では、次のオブジェクトによってそれを作成できます。 class std::thread を使用して他のスレッドを作成します。ヘッダー ファイル を含めるだけで、各 std::thread オブジェクトをスレッドに関連付けることができます。 std::thread オブジェクトを使用して、この新しいスレッドの開始時に実行されるコールバックをアタッチできます。これらのコールバックは、関数ポインター、関数オブジェクト、または Lambda 関数にすることができます。
スレッド オブジェクトは std::thread thObj(e713b189830c2ff991f906dd1cce83fe) によって作成できます。新しいスレッドは新しいオブジェクトの作成直後に開始され、渡されたコールバックは開始されたスレッドと並行して実行されます。さらに、どのスレッドも、そのスレッドのオブジェクトで join() 関数を呼び出すことによって、別のスレッドが終了するのを待つことができます。
関数ポインターを使用してスレッドを作成する:

//main.cpp
#include <iostream>
#include <thread>
void thread_function() {    
    for (int i = 0; i < 5; i++)        
    std::cout << "thread function excuting" << std::endl;
}int main() {    
    std::thread threadObj(thread_function);    
    for (int i = 0; i < 5; i++)        
    std::cout << "Display from MainThread" << std::endl;
   threadObj.join();    
   std::cout << "Exit of Main function" << std::endl;    return 0;
}

関数オブジェクトを使用してスレッドを作成する:

#include <iostream>
#include <thread>
class DisplayThread {
    public:void operator ()() {        
        for (int i = 0; i < 100; i++)            
        std::cout << "Display Thread Excecuting" << std::endl;
    }
};
int main() {    
    std::thread threadObj((DisplayThread()));    
    for (int i = 0; i < 100; i++)        
    std::cout << "Display From Main Thread " << std::endl;    
    std::cout << "Waiting For Thread to complete" << std::endl;
    threadObj.join();    
    std::cout << "Exiting from Main Thread" << std::endl;    
    return 0;
}

CmakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(Thread_test)set(CMAKE_CXX_STANDARD 11)
find_package(Threads REQUIRED)
add_executable(Thread_test main.cpp)
target_link_libraries(Thread_test ${CMAKE_THREAD_LIBS_INIT})

各 std::thread オブジェクトには関連付けられた ID があります、std::thread::get_id() —- 対応するスレッド オブジェクトの ID がメンバー関数で指定されます;
std::this_thread::get_id()—- 現在のスレッドの ID が指定される場合std::thread オブジェクト 関連付けられたスレッドがない場合、get_id() はデフォルトで構築された std::thread::id オブジェクトを返します。「スレッドではありません」。std::thread::id は ID を表すこともできます。

2.スレッドの参加と切り離し

スレッドが開始されると、別のスレッドは std::thread オブジェクトの join() 関数を呼び出すことで、このスレッドの実行が完了するのを待つことができます:

std::thread threadObj(funcPtr); 
threadObj.join();

たとえば、メイン スレッドは 10 個のスレッドを開始します。開始後、メイン関数はそれらのスレッドの実行が完了するまで待機します。すべてのスレッドに参加した後、メイン関数は実行を続けます:

#include <iostream>
#include <thread>
#include <algorithm>
class WorkerThread
{
    public:void operator()(){        
        std::cout<<"Worker Thread "<<std::this_thread::get_id()<<"is Excecuting"<<std::endl;
    }
};
    int main(){    
        std::vector<std::thread> threadList;    
        for(int i = 0; i < 10; i++){
        threadList.push_back(std::thread(WorkerThread()));
    }    
    // Now wait for all the worker thread to finish i.e.
    // Call join() function on each of the std::thread object
    std::cout<<"Wait for all the worker thread to finish"<<std::endl;    
    std::for_each(threadList.begin(), threadList.end(), std::mem_fn(&std::thread::join));    
    std::cout<<"Exiting from Main Thread"<<std::endl;    
    return 0;
}

detach canスレッドをスレッド オブジェクトから分離すると、スレッドはバックグラウンド スレッドとして実行され、現在のスレッドはブロックされませんが、切り離した後はスレッドに接続できなくなります。変数を使用すると問題が発生する可能性があります。スレッドはバックグラウンドで実行するために detach を呼び出し、一時変数が破棄されている可能性があります。その後、スレッドは破棄された変数にアクセスするため、std::detach() 関数を呼び出す必要があります。 std::thread オブジェクト内:

std::thread threadObj(funcPtr)
threadObj.detach();

detach() を呼び出した後、std::thread オブジェクトは実際の実行スレッドに関連付けられなくなります。スレッド ハンドル.

3. スレッドにパラメータを渡す

スレッドに関連付けられたオブジェクトまたは関数にパラメータを渡し、引数を std::thread コンストラクタに渡すだけで、デフォルトではすべての引数が新しいスレッドの内部ストレージにコピーされます。
スレッドにパラメータを渡す:

#include <iostream>
#include <string>
#include <thread>
void threadCallback(int x, std::string str) {  
    std::cout << "Passed Number = " << x << std::endl;  
    std::cout << "Passed String = " << str << std::endl;
}int main() {  
    int x = 10;  
    std::string str = "Sample String";  
    std::thread threadObj(threadCallback, x, str);
  threadObj.join();  
  return 0;
}

スレッドに参照を渡す:

#include <iostream>
#include <thread>
void threadCallback(int const& x) {  
    int& y = const_cast<int&>(x);
  y++;  
  std::cout << "Inside Thread x = " << x << std::endl;
}int main() {  
    int x = 9;  
    std::cout << "In Main Thread : Before Thread Start x = " << x << std::endl;  
    std::thread threadObj(threadCallback, x);
  threadObj.join();  
  std::cout << "In Main Thread : After Thread Joins x = " << x << std::endl;  
  return 0;
}

出力結果は次のとおりです:
メイン スレッド: Before Thread Start x = 9
内部スレッド x = 10
メインスレッド内: スレッド参加後 x = 9

プロセスは終了コード 0 で終了しました
threadCallback がパラメータを参照として受け入れても、x の値は変更されませんメインでは、スレッド参照の外側では見えません。これは、スレッド関数 threadCallback の x が、新しいスレッドのスタックにコピーされた一時値を参照しているためです。この値は、std::ref:

#include <iostream>
#include <thread>
void threadCallback(int const& x) {  
    int& y = const_cast<int&>(x);
  y++;  
  std::cout << "Inside Thread x = " << x << std::endl;
}int main() {  
    int x = 9;  std::cout << "In Main Thread : Before Thread Start x = " << x << std::endl;  
    std::thread threadObj(threadCallback, std::ref(x));
  threadObj.join();  
  std::cout << "In Main Thread : After Thread Joins x = " << x << std::endl;  
  return 0;
}

を使用して変更できます。出力結果は次のとおりです:
In Mainスレッド: スレッド開始前 x = 9
スレッド内 x = 10
メインスレッド内: スレッド結合後 x = 10

プロセスは終了コード 0 で終了しました
メンバーへのポインターを指定してくださいクラスの関数を Thread 関数として渡し、メンバー関数へのポインタをコールバック関数として渡し、オブジェクトへのポインタを 2 番目のパラメータとして指定します:

#include <iostream>
#include <thread>
class DummyClass { 
public:
  DummyClass() { }
  DummyClass(const DummyClass& obj) { }  
  void sampleMemberfunction(int x) {    
      std::cout << "Inside sampleMemberfunction " << x << std::endl;
  }
};
    int main() {
      DummyClass dummyObj;  
      int x = 10;  
      std::thread threadObj(&DummyClass::sampleMemberfunction, &dummyObj, x);
      threadObj.join();  
      return 0;
}

4. スレッド間でのデータの共有と競合条件

マルチスレッドでは、プログラム間でのデータの共有は簡単ですが、プログラム内でのこのデータ共有により問題が発生する可能性があり、その 1 つが競合状態です。 2 つ以上のスレッドが一連の操作を並行して実行し、同じメモリ位置にアクセスすると、そのうちの 1 つ以上のスレッドがメモリ位置内のデータを変更し、予期しない結果が生じる可能性があります。これは競合状態です。競合状態は常に発生するわけではなく、2 つ以上のスレッドが操作を実行する相対的な順序によって予期しない結果が生じる場合にのみ発生するため、競合状態を特定して再現するのは困難なことがよくあります。
たとえば、5 つのスレッドを作成し、これらのスレッドはクラス Wallet のオブジェクトを共有し、addMoney() メンバー関数を使用して並行して 100 元を追加します。したがって、最初にウォレット内の資金が 0 である場合、すべてのスレッドの競合実行が完了すると、ウォレット内の資金は 500 になるはずです。ただし、すべてのスレッドが同時に共有データを変更するため、場合によっては、財布の中のお金は500よりはるかに少ないかもしれません。
テストは次のとおりです:

#include <iostream>
#include <thread>
#include <algorithm>
class Wallet {    
    int mMoney;
    public: Wallet() : mMoney(0) { }    
    int getMoney() { return mMoney; }    
    void addMoney(int money) {        
        for (int i = 0; i < money; i++) {
            mMoney++;
        }
    }
};int testMultithreadWallet() {
    Wallet walletObject;    
    std::vector<std::thread> threads;    
    for (int i = 0; i < 5; i++) {
        threads.push_back(std::thread(&Wallet::addMoney, &walletObject, 100));
    }    for (int i = 0; i < 5; i++) {
        threads.at(i).join();
    }    
    return walletObject.getMoney();
}int main() {    
        int val = 0;    
        for (int k = 0; k < 100; k++) {        
        if ((val=testMultithreadWallet()) != 500) {            
            std::cout << "Error at count = " << k << " Money in Wallet = " << val << std::endl;
        }
    }    
    return 0;
}

各スレッドは同じメンバー変数「mMoney」を並行して増加させます。これは行のように見えますが、この「nMoney」は実際には 3 つのマシン コマンドに変換されます。 # #・「mMoney」変数を Register にロードします
・ register の値を増やします
・「mMoney」変数を register の値で更新します
この場合、増分は無視されます。インクリメント mMoney 変数ではなく、別のレジスタが追加され、「mMoney」変数の値が上書きされます。

5.使用mutex处理竞争条件

为了处理多线程环境中的竞争条件,我们需要mutex互斥锁,在修改或读取共享数据前,需要对数据加锁,修改完成后,对数据进行解锁。在c++11的线程库中,mutexes在eed5423f7254a06cc4264105c2e6fe37头文件中,表示互斥体的类是std::mutex。
就上面的问题进行处理,Wallet类提供了在Wallet中增加money的方法,并且在不同的线程中使用相同的Wallet对象,所以我们需要对Wallet的addMoney()方法加锁。在增加Wallet中的money前加锁,并且在离开该函数前解锁,看代码:Wallet类内部维护money,并提供函数addMoney(),这个成员函数首先获取一个锁,然后给wallet对象的money增加指定的数额,最后释放锁。

#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
class Wallet {    
        int mMoney;    
        std::mutex mutex;public:
    Wallet() : mMoney(0) { }    
    int getMoney() { return mMoney;}    
    void addMoney(int money) {
        mutex.lock();        
        for (int i = 0; i < money; i++) {
            mMoney++;
        }
        mutex.unlock();
    }
};int testMultithreadWallet() {
    Wallet walletObject;    
    std::vector<std::thread> threads;    
    for (int i = 0; i < 5; ++i) {
        threads.push_back(std::thread(&Wallet::addMoney, &walletObject, 1000));
    }    for (int i = 0; i < threads.size(); i++) {
        threads.at(i).join();
    }    
    return walletObject.getMoney();
}int main() {    
        int val = 0;    
        for (int k = 0; k < 1000; k++) {        
        if ((val = testMultithreadWallet()) != 5000) {            
        std::cout << "Error at count= " << k << " money in wallet" << val << std::endl;
        }
    }    
    return 0;
}

这种情况保证了钱包里的钱不会出现少于5000的情况,因为addMoney()中的互斥锁确保了只有在一个线程修改完成money后,另一个线程才能对其进行修改,但是,如果我们忘记在函数结束后对锁进行释放会怎么样?这种情况下,一个线程将退出而不释放锁,其他线程将保持等待,为了避免这种情况,我们应当使用std::lock_guard,这是一个template class,它为mutex实现RALL,它将mutex包裹在其对象内,并将附加的mutex锁定在其构造函数中,当其析构函数被调用时,它将释放互斥体。

class Wallet {  
    int mMoney;  
    std::mutex mutex; public:
  Wallet() : mMoney(0) { }  int getMoney() { return mMoney;}  
  void addMoney(int money) {    
  std::lock_guard<std::mutex> lockGuard(mutex);    
  for (int i = 0; i < mMoney; ++i) {      
  //如果在此处发生异常,lockGuadr的析构函数将会因为堆栈展开而被调用
      mMoney++;      
      //一旦函数退出,那么lockGuard对象的析构函数将被调用,在析构函数中mutex会被释放
    }

  }
};

6.条件变量

  条件变量是一种用于在2个线程之间进行信令的事件,一个线程可以等待它得到信号,其他的线程可以给它发信号。在c++11中,条件变量需要头文件87d06fb90893e2a96c75139ed482c1e2,同时,条件变量还需要一个mutex锁。
  条件变量是如何运行的:
  ·线程1调用等待条件变量,内部获取mutex互斥锁并检查是否满足条件;
  ·如果没有,则释放锁,并等待条件变量得到发出的信号(线程被阻塞),条件变量的wait()函数以原子方式提供这两个操作;
  ·另一个线程,如线程2,当满足条件时,向条件变量发信号;
  ·一旦线程1正等待其恢复的条件变量发出信号,线程1便获取互斥锁,并检查与条件变量相关关联的条件是否满足,或者是否是一个上级调用,如果多个线程正在等待,那么notify_one将只解锁一个线程;
  ·如果是一个上级调用,那么它再次调用wait()函数。
  条件变量的主要成员函数:
Wait()
它使得当前线程阻塞,直到条件变量得到信号或发生虚假唤醒;
它原子性地释放附加的mutex,阻塞当前线程,并将其添加到等待当前条件变量对象的线程列表中,当某线程在同样的条件变量上调用notify_one() 或者 notify_all(),线程将被解除阻塞;
这种行为也可能是虚假的,因此,解除阻塞后,需要再次检查条件;
一个回调函数会传给该函数,调用它来检查其是否是虚假调用,还是确实满足了真实条件;
当线程解除阻塞后,wait()函数获取mutex锁,并检查条件是否满足,如果条件不满足,则再次原子性地释放附加的mutex,阻塞当前线程,并将其添加到等待当前条件变量对象的线程列表中。
notify_one()
如果所有线程都在等待相同的条件变量对象,那么notify_one会取消阻塞其中一个等待线程。
notify_all()
如果所有线程都在等待相同的条件变量对象,那么notify_all会取消阻塞所有的等待线程。

#include <iostream>
#include <thread>
#include <functional>
#include <mutex>
#include <condition_variable>
using namespace std::placeholders;
class Application {    
    std::mutex m_mutex;    
    std::condition_variable m_condVar;    
    bool m_bDataLoaded;public:
  Application() {
        m_bDataLoaded = false;
    }    
    void loadData() {        
            //使该线程sleep 1秒
        std::this_thread::sleep_for(std::chrono::milliseconds(1000));        
        std::cout << "Loading Data from XML" << std::endl;        //锁定数据
        std::lock_guard<std::mutex> guard(m_mutex);        //flag设为true,表明数据已加载
        m_bDataLoaded = true;        //通知条件变量
        m_condVar.notify_one();
    }    bool isDataLoaded() {        
             return m_bDataLoaded;
    }    void mainTask() {        
            std::cout << "Do some handshaking" << std::endl;        //获取锁
        std::unique_lock<std::mutex> mlock(m_mutex);        //开始等待条件变量得到信号
        //wait()将在内部释放锁,并使线程阻塞
        //一旦条件变量发出信号,则恢复线程并再次获取锁
        //然后检测条件是否满足,如果条件满足,则继续,否则再次进入wait
        m_condVar.wait(mlock, std::bind(&Application::isDataLoaded, this));        
        std::cout << "Do Processing On loaded Data" << std::endl;
    }
};int main() {
    Application app;    
    std::thread thread_1(&Application::mainTask, &app);    
    std::thread thread_2(&Application::loadData, &app);
    thread_2.join();
    thread_1.join();    return 0;
}

以上がC++11 マルチスレッド プログラミングの基礎の概要の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。