初探C++多線程編程

準備工作

每個線程都需要開闢一個堆,線程切換也會有時間開銷,因此不是越多越好
每個進程都至少有有一個主線程,伴隨進程的生命週期,比如調用main()函數的線程
這裏提一下main的入口和出口
入口
內核會先調用一個特殊的啓動例程,從內核取得命令行參數和環境變量值傳遞給main函數列表
C++編譯器使用連接編譯器,鏈接啓動例程和main()函數。
出口
_exit()和_Exit()立即進入內核,exit()則先執行一些清理處理(調用終止處理程序,關閉標準I/O流等),然後進入內核。使用atexit()函數來登記(<32個)終止處理程序,這些函數會在main結束是被exit()函數自動調用,調用的順序與登記的順序相反,一個函數可以被登記多次。

int atexit(void (*func)(void));  // 若成功則返回0, 若出錯則返回非0值

執行exit(main)開啓主線程,當main()執行結束,內核調用exit()強行終止其他未執行完線程,所以,希望主線程結束後其他線程任然能夠繼續執行的話,需要主函數main()中調用pthread_exit()

按照POSIX標準定義,當主線程在子線程終止之前調用pthread_exit()時,子線程是不會退出的。
When you program with POSIX Threads API, there is one thing about pthread_exit() that you may ignore for mistake. In subroutines that complete normally, there is nothing special you have to do unless you want to pass a return code back using pthread_exit(). The completion won’t affect the other threads which were created by the main thread of this subroutine. However, in main(), when the code has been executed to the end, there could leave a choice for you. If you want to kill all the threads that main() created before, you can dispense with calling any functions. But if you want to keep the process and all the other threads except for the main thread alive after the exit of main(), then you can call pthread_exit() to realize it. And any files opened inside the main thread will remain open after its termination.
引用自 國境之南Fantasy

C++11之前並不支持多線程,不同平臺實現不一樣,需要不同的線程庫
windows: CreateThread()、_beginthred()、_beginthredexe()
linux:pthread_create()
一些臨界區,互斥變量管理函數,但都不能跨平臺
POSIX thread(pthread)可以跨平臺,但需要麻煩的配置

C++11自身增加了對多線程的支持
本文{ “IDE=VS Code” //超好用的! }

開始

一個線程從一個函數開始

#include<iostream>
#include<thread>
using namespace std;
void myprint()
{
    while(1)
      cout<<"S"<<endl;
}
int main(int argc, char** argv) 
{
    thread myth(myprint);  //1.創建子線程
                           //2.線程入口爲myprint()
                           //3.開始執行子線程
    myth.join();           //阻塞主線程,待子線程結束繼續主線程
    while(1)
      cout<<"E"<<endl;
    return 0;
}

在這裏插入圖片描述
不阻塞主線程時,主線程結束,強制結束子線程,可能會拋異常
在這裏插入圖片描述
主程序結束,內核殺掉子線程是很危險的事情,可能文件還沒有被關閉,遭到破壞
通過調用detach()實現“主線程先溜,子線程繼續”的情景,但會使主線程失去對子線程的控制

void myprint()
{
    while(1)
       cout<<"子線程"<<endl;
}
int main(int argc, char** argv) 
{
    thread myth(myprint); 
    myth.detach();           
    //失去與主線程的關聯,成爲“守護線程”,在後臺運行
    cout<<"主線程結束"<<endl;
    return 0;                
    //主線程結束後,子線程的cout無法通過結束的cmd顯示
    //但任然由編譯器內置的“運行時庫”(C run-time library)接管在後臺運行
    //因此建議使用join(),不要失去對線程的管理
}

通過其他 可調用對象 創建線程,如類中的調用運算符"()"的重載函數,或lambda表達式
在這裏插入圖片描述
避免指針或引用傳遞,否則會使內存管理變得異常複雜
避免隱式的類型轉換(如 int -> A.constructor(int)構造時的隱式轉換)
儘量通過函數參數列表中的引用傳遞臨時對象,編譯器還是會爲了thread拷貝一次
在這裏插入圖片描述
一定要用 const 傳遞?
可以使用ref()真正實現引用傳遞,從而躲過const和mutable
注意這裏還避免了編譯器爲thread拷貝構造的過程
在這裏插入圖片描述
附上std::ref()的源碼,reference_wrapper

//std::reference_wrapper 是包裝引用於可複製、可賦值對象的類模板。
//它常用作將容器存儲入無法正常保有引用的標準容器(類似 std::vector )的機制。
template<typename _Tp>
inline reference_wrapper<_Tp>
ref(_Tp& __t) noexcept { return reference_wrapper<_Tp>(__t); }

這裏將A的this指針傳遞給非靜態函數,從而使調用成立(還記得麼?成員函數隱藏了一個參數 this !)
在這裏插入圖片描述
傳遞智能指針,關於移動拷貝參見《C++拷貝控制(copy control)》
注意move後mp被回收,此時thread中使用被回收的懸空指針很危險

unique_ptr<int> mp(new int(1));
thread myth(fun,std::move(mp));
myth.join();  

初探一下thread類的構造函數

template<typename _Callable, typename... _Args> explicit
thread(_Callable&& __f, _Args&&... __args)
{
    #ifdef GTHR_ACTIVE_PROXY
	// Create a reference to pthread_create, not just the gthr weak symbol.
	auto __depend = reinterpret_cast<void(*)()>(&pthread_create);
	//reinterpret_cast操作符重新解釋指針類型
    #else  
	auto __depend = nullptr;
    #endif
        _M_start_thread(_S_make_state(
	      __make_invoker(std::forward<_Callable>(__f),
			     std::forward<_Args>(__args)...)),
	    __depend);
}
/*
*  @brief  Forward an rvalue.
*  @return The parameter cast to the specified type.
*
*  This function is used to implement "perfect forwarding".
*/
template<typename _Tp> 
constexpr _Tp&&  forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
{
    static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"
    " substituting _Tp is an lvalue reference type");
    return static_cast<_Tp&&>(__t);  //可以看到thread始終嘗試使用引用類型,甚至強轉
}
template<typename _Tp>
struct remove_reference
{ typedef _Tp   type; };

調用this_thread空間內的get_id()能夠返回當前所在線程的線程 thread::id

namespace this_thread
{
   /// get_id
   inline thread::id
   get_id() noexcept
   {
      #ifdef __GLIBC__
      // For the GNU C library pthread_self() is usable without linking to
      // libpthread.so but returns 0, so we cannot use it in single-threaded
      // programs, because this_thread::get_id() != thread::id{} must be true.
      // We know that pthread_t is an integral type in the GNU C library.
      if (!__gthread_active_p())
	     return thread::id(1);
      #endif
      return thread::id(__gthread_self());
    }
    //其他函數
}

總結一下:能用join()用join(),用detach()必須小心處理指針unique_ptr<>或shared_ptr<>和引用std::ref(和可能的&),以免子線程中使用被主線程釋放的內存地址!!!

數據共享

#include<thread>
#include<vector>
#include<iostream>
#include<deque>
#include<mutex>
using namespace std;
class mycl
{
public:
    int a=0;
    deque<int> iv;
    mutex mylock;
    mycl(int i):a(i)
    {cout<<"constructor!ID:"<<this_thread::get_id()<<endl;}
    mycl(const mycl &m):a(m.a)
    {cout<<"Copy constructor!ID:"<<this_thread::get_id()<<endl;}
    void iniv()
    {
        for(int i=0;i<1000;i++) 
        {       //使用作用域控制解鎖時機
                lock_guard<mutex> myguard(mylock);
                cout<<"iniv:"<<a++<<endl;
        }//析構情況下,lock_guard才解鎖
    }
    void outiv()
    {
        for(int i=0;i<1000;i++)
        {   //構造函數中mylock.lock();
            lock_guard<mutex> myguard(mylock);
            cout<<"outiv:"<<a--<<endl;
            //析構函數中mylock.unlock();  避免異常跳出後沒有解鎖的問題
        }
       
    }
    ~mycl(){cout<<"destructor!ID:"<<this_thread::get_id()<<endl;}
};
int main(void) 
{
    mycl A(100);   
    thread myth2(mycl::outiv,&A); //我當前的g++編譯器不需要ref也是傳遞的A對象的引用
    thread myth1(mycl::iniv,&A); 
    myth1.join();
    myth2.join();
    cout<<A.a<<endl;   //最終輸出還是100,中間有iniv+,也有outiv-
    while(1);
    return 0;
}

避免死鎖,可以使用lock()函數,同時鎖柱多個鎖頭,沒能全鎖成功則釋放剛鎖的鎖
adopt_lock()標記告知lock()要鎖的互斥量已經被鎖,如果lock之前沒鎖會報異常
配合lock使用,保證mutex最後一定被unlock()

class mycl
{
public:
    int a=0;
    deque<int> iv;
    mutex mylock1;
    mutex mylock2;
    mycl(int i):a(i)
    {cout<<"constructor!ID:"<<this_thread::get_id()<<endl;}
    mycl(const mycl &m):a(m.a)
    {cout<<"Copy constructor!ID:"<<this_thread::get_id()<<endl;}
    void iniv()
    {
        for(int i=0;i<1000;i++) 
        {
            lock(mylock1,mylock2);
            lock_guard<mutex> myguard1(mylock1,adopt_lock);
            lock_guard<mutex> myguard2(mylock2,adopt_lock);
            cout<<"iniv:"<<a++<<endl;
        }
    }
    void outiv()
    {
        for(int i=0;i<1000;i++)
        {
            lock(mylock1,mylock2);
            lock_guard<mutex> myguard1(mylock1,adopt_lock);
            lock_guard<mutex> myguard2(mylock2,adopt_lock);
            cout<<"outiv:"<<a--<<endl;
        }
       
    }
    ~mycl(){cout<<"destructor!ID:"<<this_thread::get_id()<<endl;}
};

unique_lock更靈活(支持跟多標記),但是會佔用更多內存,效率稍微低點
使用try_to_lock標記的mutex不能處於lock的狀態,否則會被卡死

class mycl
{
public:
    int a=0;
    deque<int> iv;
    mutex mylock1;
    mutex mylock2;
    mycl(int i):a(i)
    {cout<<"constructor!ID:"<<this_thread::get_id()<<endl;}
    mycl(const mycl &m):a(m.a)
    {cout<<"Copy constructor!ID:"<<this_thread::get_id()<<endl;}
    void iniv()
    {
        for(int i=0;i<1000;i++) 
        {
            unique_lock<mutex> unilock(mylock1,try_to_lock);
            if(unilock.owns_lock())
               cout<<"iniv:"<<a++<<endl;
            else
               cout<<"in沒有拿到鎖做其他事情"<<endl;
        }
    }
    void outiv()
    {
        for(int i=0;i<1000;i++)
        {
            unique_lock<mutex> unilock(mylock1,try_to_lock);
            if(unilock.owns_lock())
               cout<<"outiv:"<<a--<<endl;
            else
               cout<<"out沒有拿到鎖做其他事情"<<endl;
        }
       
    }
    ~mycl(){cout<<"destructor!ID:"<<this_thread::get_id()<<endl;}
};

try_lock()嘗試加鎖,成功返回true,否則返回false

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章