c++多線程並發學習筆記(0)


多進程並發:將應用程序分為多個獨立的進程,它們在同一時刻運行。如圖所示,獨立的進程可以通過進程間常規的通信渠道傳遞訊息(信號、套接字、。文件、管道等等)。

優點:1.操作系統在進程間提供附附加的保護操作和更高級別的通信機制,意味着可以編寫更安全的並發代碼。

           2. 可以使用遠程連接的方式,在不同的機器上運行獨立的進程,雖然增加了通信成本,但在設計精良的系統數上,這可能是一個提高並行可用性和性能的低成本方法。

缺點:1. 這種進程間的通信通常不是設置復雜,就是速度慢,這是因為操作系統會在進程間提供了一定的保護措施,以免一個進程去修改另一個進程的數據

      2. 運行多個進程所需的固定開銷:需要時間啟動進程,操作系統需要內部資源來管理進程

多線程並發:在單個進程中運行多個線程。線程就是輕量級的進程:每個線程獨立運行,且線程可以在不同的指令序列中運行。但是進程中的所有線程都共享地址空間,並且所有線程所訪問的大部分數據---全局變量仍然是全局的,指針 對象的引用或者數據可以在線程之間傳遞。

優點:地址空間共此昂,以及缺少線程間數據的保護,使得操作系統的記錄工作量減小,所以使用多線程的開銷遠遠小於使用多進程

缺點:共享內存的靈活性的代價:如果數據要被多個線程訪問,那么程序員必須寶成每個線程訪問的數據是一致的,這意味着需要對線程間的通信做大量的工作

 並發與並行:

並行更加注重性能。在討論使用當前可用硬件來提高批量數據處理的速度時,我們會討論程序的並行性;當關注的重點在於任務分離或任務響應時,就會討論到程序的並發性

 

std::thread 學習

需要包含的頭文件 <thread>

初始化線程(啟動線程)

一個簡單的例子:

#include <iostream>
#include <thread>

using namespace std;

void sayHello()
{
    cout << "hello" <<endl;
}

int main()
{
    thread t(sayHello);
    t.join();
}

初始化線程(啟動線程)就是構造一個std::thread 的實例: std::thread(func)。 func 不簡單的指函數,它是一個函數調用符類型,如下例子:

#include <iostream>
#include <thread>

using namespace std;

class Test
{
public:
    void operator()()
    {
        cout << "hello" <<endl;
    }
};

int main()
{
    Test test;
    thread t(test);
    t.join();
}

也可以使用類的成員變量來初始化std::thread:  std::thread(&Class::func, (Class)object)

#include <iostream>
#include <thread>

using namespace std;

class Test
{
public:
    void sayHello()
    {
        cout << "hello" <<endl;
    }
};

int main()
{
    Test test;
    thread t(&Test::sayHello, &test);
    t.join();
}

注意:把函數對象傳入到線程構造函數中時,需要避免以下情況: 如果你傳遞了一個臨時變量,而不是一個命名的變量,c++的編譯器會將其解釋為函數聲明,而不是類型對象的定義。例如:

std::thread myThread(func());

這里相當於聲明了一個名為myThread的函數,這個函數帶一個參數(函數指針指向一個沒有參數並且返回func對象的函數),返回一個std::thread對象的函數,而不是啟動了一個線程。

要解決這個問題,解決方法:

  • 使用多組括號
std::thread myThread((func()));
  • 使用大括號
std::thread myThread({func()});
  • 使用lambda表達式
std::thread myThread([](){
    do_something();
});

啟動線程后,需要明確是要等待線程結束(加入式)還是讓其自主運行(分離式),如果在對象銷毀之前還沒做出決定,程序就會終止(std::thread的析構函數會調用std::terminate())。即使有異常情況也要保證線程能夠正確的加入(join)或者分離(detached)。

如果不等待線程,就要保證線程結束之前,可訪問的數據的有效性。例如主線程往子線程中傳了一個變量A的引用,子線程detach,則表示主線程可能在子線程之前結束,這樣變量A便會被銷毀,這時子線程再使用A的引用就會產生異常。處理這種情況的常規方法:使線程的功能齊全,將數據復制到線程中,而非復制到共享數據中。如果使用一個可調用的對象作為線程函數,這個對象就會復制到線程中,而后原始對象就會立即銷毀,但對於對象中包含的指針和引用還需謹慎。最好不要使用一個訪問局部變量的函數去創建線程。此外,可以通過join()函數來確保線程在函數完成前結束。

 

等待線程完成

使用std::thread 方法中的join()來實現等待線程完成

調用join()的行為,還清理了線程相關的存儲部分,這樣std::thread對象將不再與已經完成的線程有任何關聯。這意味着,只能對一個線程使用一次join();一旦已經使用過join(),std::thread對象就不能再次加入了,當對其使用joinable()時,將返回false。

 

后台運行線程

使用std::thread 方法中的detach()來實現等待線程完成

使用detach()會讓線程在后台運行,這就意味着主線程不能與之產生直接交互。也就是說,不會等待這個線程結束;如果線程分離,那么就不可能有std::thread對象能引用它,分離線程的確在后台運行,所以分離線程不能被加入。不過C++運行庫保證,當線程退出時,相關資源的能夠正確回收,后台線程的歸屬和控制C++運行庫都會處理。std::thread對象使用t.joinable()返回的是true,才可以使用t.detach()

 

向線程函數傳遞參數

在初始化的時候可以進行參數傳遞 std::thread myThread(func, arg0, arg1,...),另外在使用類的成員函數來初始化線程時的參數傳遞std::thread(&Class::func, (Class)object, arg0, arg1,...)

在這里需要注意兩個問題:

1. 在將指向動態變量的指針作為參數傳給線程的時候,想要依賴隱式轉換將字面值轉換為函數期待的對象(1),但是std::thread的構造函數會復制提供的變量,就只復制了沒有轉換成期望類型的字符串字面值。解決方法是:(2)在傳入之前先顯示的進行轉換

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

2. 期望傳入一個引用,但整個對象被復制了。雖然期望傳入一個引用類型的參數(1),但std::thread的構造函數並不知曉;構造函數無視函數期待的參數類型,並盲目的拷貝已提供的變量。

解決方法是使用std::ref()來將參數轉換為引用的形式(2)

void update_data_for_widget(widget_id w,widget_data& data);
void oops_again(widget_id w)
{
  widget_data data;
  //std::thread t(update_data_for_widget,w,data); // 1
  std::thread t(update_data_for_widget,w,std::ref(data)); // 2
  display_status();
  t.join();
  process_widget_data(data);
}

 

轉移線程所有權 

首先要明確的是對於std::thread,不能將一個對象賦值給另一個對象,即賦值構造函數是被刪除的。

thread(thread&) = delete;

但是我們有時需要轉移線程的所有權,這時候就需要使用std::move()來實現

void some_function();
void some_other_function();
std::thread t1(some_function);            // 1
std::thread t2=std::move(t1);            // 2 
t1=std::thread(some_other_function);    // 3 隱式移動操作
std::thread t3;                            // 4
t3=std::move(t2);                        // 5
t1=std::move(t3);                        // 6 賦值操作將使程序崩潰

最后一個移動操作⑥,將some_function線程的所有權轉移給t1。不過,t1已經有了一個關聯的線程(執行some_other_function的線程),所以這里系統直接調用std::terminate()終止程序繼續運行。這樣做(不拋出異常,std::terminate()noexcept函數)是為了保證與std::thread的析構函數的行為一致。需要在線程對象被析構前,顯式的等待線程完成,或者分離它;進行賦值時也需要滿足這些條件(說明:不能通過賦一個新值給std::thread對象的方式來"丟棄"一個線程)。

std::thread 支持移動操作,意味着它可以當做函數的返回值和參數

//作為函數返回值
std::thread g()
{
  void some_other_function(int);
  std::thread t(some_other_function,42);
  return t;
}

//作為函數的參數
void f(std::thread t);
void g()
{
  void some_function();
  f(std::thread(some_function));
  std::thread t(some_function);
  f(std::move(t));
}

 

運行時決定線程數量

std::thread::hardware_concurrency() 這個函數會返回能並發在一個程序中的線程數量。例如,多核系統中,返回值可以是CPU核芯的數量。返回值也僅僅是一個提示,當系統信息無法獲取時,函數也會返回0。

 

標識線程

線程標識類型為std::thread::id,獲取方法有兩種:

1. 調用std::thread對象的成員函數get_id()來直接獲取,如果std::thread對象沒有與任何執行線程相關聯,get_id()將返回std::thread::type默認構造值,這個值表示“無線程”

2. 當前線程中調用std::this_thread::get_id()(這個函數定義在<thread>頭文件中)也可以獲得線程標識。

如果兩個對象的std::thread::id相等,那它們就是同一個線程,或者都“無線程”。如果不等,那么就代表了兩個不同線程,或者一個有線程,另一沒有線程。

 

std::thread::id 有豐富的比較方法,因此它可以當做容器的鍵值,做排序等等比較。標准庫也提供std::hash<std::thread::id>容器,所以std::thread::id也可以作為無序容器的鍵值。

 

參考資料:

https://chenxiaowei.gitbook.io/c-concurrency-in-action-second-edition-2019/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM