歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux編程 >> Linux編程

C++並發編程 thread

std::thread

  C++11在標准庫中為多線程提供組件, 使用線程需要包含頭文件 thread, 其命名空間為 std.

啟動新線程

每個進程至少有一個線程: 執行main()函數的線程, 其余線程有其各自的入口函數(線程函數)。
當線程執行完線程函數後, 線程也會退出. 如果不傳入線程函數(類似這種形式std::thread t;), 線程不會運行. 線程函數不能重載, 否則不能編譯.
在為一個線程創建了一個 std::thread 對象後, 如果線程已啟動(不傳入線程序函數時, 線程不會啟動), 必須要明確是加入(join)還是分離線程(detach).

        // 啟動一個線程:
        void MyThread(const std::string& str)
        {
            PRINT_LINE_INFO();
            std::cout << str << std::endl;
        }
        //std::thread t(MyThread, "Hello C...");
        std::thread t([] {
            MyThread("Hello C...");
            MyThread("Hello C2...");
        });               
        // 對於類方法, 需要使用 std::bind.
        std::thread t(std::bind(&ThreadExample::MyThread, this, "msg"));
        ThreadGuard tg(t);

如果 std::thread 對象銷毀之前還沒有調用 join 或 detach, 程序就會終止( std::thread 的析構函數會調用 std::terminate() ). 因此, 即便是有異常存在, 也需要確保線程能夠正確的加入(joined)或分離(detached).
調用 join 或 detach 之前需要調用 joinable() 判斷一下線程是否運行. 如果 joinable() 返回 false, 則不需要.
join()是簡單粗暴的等待線程完成, 此時創建 std::thread 對象的線程(以下稱主線程)將被阻塞. 如果在線程啟動後到主線程在調用 join() 前的代碼中發生了異常, 此時將會導致主線程永遠沒有機會執行.
針對此問題, 需要使用 RAII 機制來解決, 如創建一個 ThreadGuard 對象, 在析構函數中保證總是可以調用到 join.

        #ifndef _THREAD_GUARD_
        #define _THREAD_GUARD_

        #include <thread>

        class ThreadGuard
        {
        public:
            ThreadGuard(std::thread& t_) : t(t_){}

            ~ThreadGuard()
            {
                if (t.joinable())
                {
                    t.join();
                }
            }

            ThreadGuard(const ThreadGuard &) = delete;
            ThreadGuard& operator=(const ThreadGuard &) = delete;

        private:
            std::thread& t;
        };

        #endif // _THREAD_GUARD_

如果是分離線程, 必須保證可訪問數據的有效性, 否則會產生未定義的行為, 如同單線程中一個對象被銷毀後再訪問一樣.
處理這種情況的常規方法: 使線程函數的功能齊全, 將數據復制到線程中. 如果使用一個可調用的對象作為線程函數,這個對象就會復制到線程中,而後原始對象就可以銷毀. 下面是錯誤的使用方法示例:

        class Func
        {
            int& i;
        public:
            Func(int& i_) : i(i_) {}
            void operator() ()
            {
                for (unsigned j = 0; j < 10; ++j)
                {
                    // 潛在訪問隱患:懸空引用 i 
                    std::cout << i << " ";
                }
                std::cout << std::endl;
            }
        };
        { // 某個作用域內
            int* p = new int(100);
            Func f(*p);
            std::thread t(f);
            t.detach(); // 不等待線程結束
            delete p;
        } // 新線程可能還在運行

線程函數

  線程函數可以有不同的參數, 向線程傳遞參數,只要在構造 std::thread 對象時,按照線程函數參數列表一一對應傳入即可。線程函數有幾點需要注意的地方:

(1) 默認的參數會被拷貝到獨立的線程中,即使是引用的形式, 如果需要需要傳遞引用, 需要使用 std::ref 顯示說明(並且線程函數參數也需要聲明為引用).

        void ThreadParamRef(std::string& str)
        {
            str += " --> add";
        }

        void ThreadParam(std::string str)
        {
            str += " --> add";
        }    
    
        std::string str("Hello C++ Thread...");
        //std::thread t(ThreadParamRef, str);
        std::thread t(ThreadParamRef, std::ref(str)); // 只有這種形式才能在線程執行完畢後輸出 Hello C++ Thread... --> add 
        //std::thread t(ThreadParam, std::ref(str));
        t.join();
        std::cout << str << std::endl;

(2)  線程參數傳遞時需要注意不能傳入局部變量, 考慮下面的代碼,buffer②是一個指針變量,指向本地變量,然後本地變量通過buffer傳遞到新線程中②。
  函數有很大的可能,會在字面值轉化成 std::string 對象之前崩潰,從而導致線程的一些未定義行為。
  解決方案就是在傳遞到 std::thread 構造函數之前就將字面值轉化為 std::string 對象。

        void f(int i,std::string const& s);
        void oops(int some_param)
        {
            char buffer[1024]; // 1
            sprintf(buffer, "%i",some_param);
            std::thread t(f,3,buffer); // 2
            t.detach();
        }          
        // 正確的方法
        void f(int i,std::string const& s);
        void not_oops(int some_param)
        {
            char buffer[1024];
            sprintf(buffer,"%i",some_param);
            std::thread t(f,3,std::string(buffer)); // 使用std::string,避免懸垂指針
            t.detach();
        }

  (3) 線程函數參數傳遞時, 可以移動, 但不能拷貝. "移動"是指: 原始對象中的數據轉移給另一對象,而轉移的這些數據在原始對象中不再保存.

        void ThreadParamUniquePtr(std::unique_ptr<int> up)
        {
            std::cout << (up.get() ? *up : -1) << std::endl;
        }
        std::thread t(ThreadParamUniquePtr, std::move(up));
        //std::thread t(ThreadParamUniquePtr, up);  // 不能編譯
        //std::thread t(ThreadParamUniquePtr, std::ref(up)); // 要求線程函數參數也為引用才能編譯
        t.join();
        std::cout << (up.get() ? *up : -1) << std::endl; // 將輸出-1

線程所有權轉移

  線程是資源獨占型, 但可以將所有權轉移給別的對象. 如果一個 std::thread 對象與一個運行的線程關聯, 此時接受一個新的線程所有權時, 其以前關聯的線程將直接調用 std::terminate() 終止程序繼續運行.

std::thread t1(f);
std::thread t2(f);
// t1 = std::thread(f); // t1 所有權還沒有轉移, 不能通過賦一個新值來放棄線程
// t1 = std::move(t2); // t1 所有權還沒有轉移, 不能通過賦一個新值來放棄線程
t1.detach(); 或 t1.join();
t1 = std::move(t2);
std::thread t3 = std::move(t1);
t1 = std::move(t2);

線程對象也可以在函數中進行轉移.

std::thread f1()
{
  return std::thread(f);
}
std::thread f2()
{
  std::thread t(f);
  return t;
}
void f3(std::thread t);
void f4()
{
  f3(std::thread(f));
  std::thread t(f);
  f3(std::move(t));
}

由於 std::thread 是可轉移的, 如果容器對移動操作支持, 則可以將 std::thread 對象放入其中.

        class Func
        {
            int i;
        public:
            Func(int i_) : i(i_) {}
            void operator() ()
            {
                for (unsigned j = 0; j < 10; ++j)
                {
                    std::cout << i << " ";
                }
                std::cout << std::endl;
            }
        };
        std::vector<std::thread> threads;
        for (int i = 1; i < 10; i++)
        {
            Func f(i);
            //std::thread t(f);
            //v.push_back(t);     // 不能采用這種方式
            //v.push_back(std::move(t));  // 需要使用移動操作才可以
            threads.push_back(std::thread(f));
        }
        std::for_each(threads.begin(), threads.end(), std::mem_fn(&std::thread::join)); // 對每個線程調用join()

常用函數

  std::thread::hardware_concurrency() 返回 CPU 核心線程數. 如果無法查詢系統信息時, 返回0. (static 函數)
  get_id() 返回 std::thread 對象關聯的線程的 id. 如果所有權已轉移, 或線程函數已返回, 返回0.
  std::this_thread::get_id() 取得當前線程的 id. (static 函數) 

一個更好的ThreadGuard

#ifndef _THREAD_GUARD_
#define _THREAD_GUARD_

template <class _Thread>
class ThreadGuard
{
public:
    explicit ThreadGuard(_Thread& t_) : t(t_) {}

    ~ThreadGuard()
    {
        if (t.joinable())
        {
            t.join();
        }
    }

    ThreadGuard(const ThreadGuard &) = delete;
    ThreadGuard& operator=(const ThreadGuard &) = delete;

private:
    _Thread& t;
};

#endif // _THREAD_GUARD_

Copyright © Linux教程網 All Rights Reserved