c++ 多线程 类成员函数_多线程(C++/Python)
多線程(C++/Python)
本文包括一下內容:
通過C++11的標準庫進行多線程編程,包括線程的創建/退出,線程管理,線程之間的通信和資源管理,以及最常見的互斥鎖,另外對python下多線程的實現進行討論。
[TOC]
- 前言
- 線程管理初步
- 1. 線程函數
- 2. 線程啟動
- 3. 線程結束/退出
- 4. 線程傳參
- 5. 互斥鎖
- Python中的多線程
- 1. 線程中的參數訪問ThreadLocal
- 2. Python中的鎖機制Threading.Lock()
前言
多線程模型共享同一進程資源,通過多線程可以極大的提高代碼的效率,完成單一線程無法完成的任務。
幾個需要記住的點: C++中的線程是一個類,因此可以像操作類一樣進行操作; C++中的線程也是一類資源;
sample
#include <iostream> #include <thread>void Thread_1() { std::cout << "This is Thread_1." << std::endl;return; }int main() {std::thread t{greeting}; // 列表初始化t.join(); return 0; }以上是多線程下的HelloWorld!,從上我們可以看出C++多線程編程的基本步驟:
創建線程函數 -> 實例一個線程 -> 運行
兩個注意點
1. 編譯 我們使用了C++11的特性以及線程庫pthread,因此在編譯的時候這兩個都要說明:
g++ --std=c++11 -pthread main.cpp2.線程初始化 從 C++ 11 開始,推薦使用列表初始化{}的方式,構造類類型的變量。
線程管理初步
包括線程函數,啟動線程,結束線程,線程傳參
1. 線程函數
任何事情都有個開始,線程函數就是新線程的開始入口。 線程函數必須是callable和無返回值的。
普通函數 例如上面例子中的簡單形式void func(void *params);
可調用類型的實例
class ThreadTask {private:size_t count_ = 0;public:explicit ThreadTask (size_t count) : count_(count) {}void operator()() const { // 定義callabledo_something(this->count_);} };ThreadTask task{42}; // 初始化可調用類型的實例 std::thread wk_thread{task}; // 創建并初始化和運行新線程 // 列表初始化注意: 雖然callable的實例看起來和函數用法一樣,但是其本質上仍然是一個類的對象,因此在傳入線程進行初始化時,其會被拷貝到線程空間,因此callable的類在這里必須做好完善的拷貝控制(參拷貝構造函數)
2. 線程啟動
線程隨著thread類型實例的創建而創建,因此線程就變成了如同實例一樣的資源,由C++提供統一的接口進行管理。
創建線程的三種不同的方式:
(1)最簡單最常見的方式
void thread_1(); // 創建線程函數std::thread new_thread{thread_1}; // 通過列表初始化的方式,實例化一個線程當函數的名字被拿來使用的時候,其實使用的是一個指針(隱式的),當然我們也可以進行顯式的使用&thread_1,二者表示的是一樣的。
(2)通過可調用類型callable的實例創建 參見上方線程函數:可調用類型的實例。
注意,強烈建議使用c++11的列表初始化方法,尤其是使用臨時構造的實例創建線程的時候:
std::thread new_thread1(CallableClass()); // 錯誤方式 std::thread new_thread2{CallableClass{}}; // 正確(3)以lambda-表達式創建線程 lambda表達式是c++中的可調用對象之一,在C++11中被引入到標準庫中,使用時不需要包含任何頭文件。
3. 線程結束
任何事情都有個結束。
當線程啟動之后,我們必須在 std::thread 實例銷毀之前,顯式地說明我們希望如何處理實例對應線程的結束狀態,尤其是線程內部調用了系統資源,比如打開串口和文件等等。未加說明,則會調用std::terminate()函數,終止整個程序。
join()和detach()
join和detach的區別如果選擇接合子線程t.join(),則主線程會阻塞住,直到該子線程退出為止。
如果選擇分離子線程t.detach(),則主線程喪失對子線程的控制權,其控制權轉交給 C++ 運行時庫。這就引出了兩個需要注意的地方:
異常退出/結束的處理
以上所說的是正常結束退出的情況,但是在某些情況下線程會異常退出,導致整個程序終止。
線程也是種一種資源,因此我們可以考慮RAII的思想,構建一個ThreadGuard類來處理這種異常安全的問題。
RAII: "資源獲取即初始化",是C++語言的一種管理資源、避免泄漏的慣用法。其利用C++中的構造的對象最終會被銷毀的原則,即棧對象在離開作用域后自動析構的語言特點,將受限資源的生命周期綁定到該對象上,當對象析構時以達到自動釋放資源的目的。通過使用一個對象,在其構造時獲取對應的資源,在對象生命期內控制對資源的訪問,使之始終保持有效,最后在對象析構的時候,釋放構造時獲取的資源,因為析構函數一定會執行。這里說的資源都是指的受限資源,比如堆上分配的內存、文件句柄、線程、數據庫連接、網絡連接等。
直接通過例子來說明:
struct ThreadGuard{private:std::thread& _t;public:explicit ThreadGuard(std::thread& t):_t(t){};~ThreadGuard(){if (this->_t.joinable()){ // 如果線程沒有結束,那么就等待線程結束this-_t.join();}}ThreadGuard(const ThreadGuard&) = delete; // 禁止不必要的特殊成員函數ThreadGuard& operator=(const ThreadGuard&) = delete; };void func();void do(){std::thread thread_1;ThreadGuard guard{thread_1}; // 傳入ThreadGuardthread_1 = std::thread{func}; // 正常的線程創建和啟動// .....return; }以上是一個典型的利用RAII保護資源的例子,無論do()進程如何退出,guard都會最終幫助thread_1確保退出。
4. 線程傳參
共享數據的管理 和 線程間的通信 是多線程編程的兩大核心參數為引用類型時的處理
注: 線程傳遞參數默認都是值傳遞, 即使參數的類型是引用,也會被轉化 如果在線程中使用引用來更新對象時,就需要注意了。默認的是將對象拷貝到線程空間,其引用的是拷貝的線程空間的對象,而不是初始希望改變的對象. 解決方案:使用std::ref()thread t(func, std::ref(data))在創建和啟動線程傳入線程函數時,其需要采用引用方式的參數用std::ref()進行修飾,如此,在t線程中對data的修改會反饋到當前線程中。
建議傳參方式
線程傳參時,除了默認采用值傳遞,還會自動進行格式轉換操作,這種操作有時是會出問題的,比如const char*強制轉為char時。 因此,線程間進行傳參建議采用結構體的方式,將參數統一包裹進來。
struct ThreadGuard{private:std::thread& _t;public:explicet ThreadGuard(std::thread& t):_t(t){};~ThreadGuard(){if (this->_t.joinable()){this->_t.joinable();}}ThreadGuard(const std::thread&) = delete;ThreadGuard& operator=(const std::Thread&) = delete; };struct Param{ // 定義參數的結構體uint_8 thread_control;std::string name;ros::Publisher mode_publisher; };void thread_1(void *param){Param *_param = (Param *)param;std::string name = _param->name;// ... }void do(){ std::thread thread_1; ThreadGuard guard{thread_1}; param = new Param(); // 構建paramthread_1 = std::thread{thread_1, param};return; }以上為一個通過結構體進行傳參,并使用RAII守護線程的完整例子。
以類中非靜態成員函數為線程函數
前期在寫USB2CAN驅動時,需要在同一個類中構建多個非靜態成員函數并作為線程函數,特此記錄。
class Task{public:void thread_1(int a);void do(); }Task task; // 1 std::thead{&Task::func, &task, 20};該方法的使用注意事項:
5. 互斥鎖
線程之間的鎖有:互斥鎖、條件鎖、自旋鎖、讀寫鎖、遞歸鎖。一般而言,鎖的功能越強大,性能就會越低。 其中互斥鎖使用的頻率最高,本處也僅對互斥鎖進行討論。
std::mutex
std::mutex是C++11 中最基本的互斥量,std::mutex對象提供了獨占所有權的特性——即不支持遞歸地對std::mutex對象上鎖(而 std::recursive_lock 則可以遞歸地對互斥量對象上鎖。)
std::mutex的成員函數
1. lock(): 調用線程將鎖住該互斥量。線程調用該函數會發生下面 3 種情況:
(1). 如果該互斥量當前沒有被鎖住,則調用線程將該互斥量鎖住,直到調用 unlock之前,該線程一直擁有該鎖。
(2). 如果當前互斥量被其他線程鎖住,則當前的調用線程被阻塞住。
(3). 如果當前互斥量被當前調用線程鎖住,則會產生死鎖(deadlock)。
2. unlock(): 解鎖,釋放對互斥量的所有權。
3. try_lock(): 嘗試鎖住互斥量,如果互斥量被其他線程占有,則當前線程也不會被阻塞。線程調用該函數也會出現下面 3 種情況,
(1). 如果當前互斥量沒有被其他線程占有,則該線程鎖住互斥量,直到該線程調用 unlock 釋放互斥量。
(2). 如果當前互斥量被其他線程鎖住,則當前調用線程返回 false,而并不會被阻塞掉。
(3). 如果當前互斥量被當前調用線程鎖住,則會產生死鎖(deadlock)。
sample
#include <thread> #include <mutex>volatile int counter(0); std::mutex mutex;void new_thread(){for(int i=0;i<100;i++){try(mutex.try_lock()){++counter;mutex.unlock();}} }int main(int argc, char** argv){std::thread[10] threads[10];for(int i=0;i<10;i++){threads[i] = std::thread{new_thread};}for(auto& th:threads) th.join();return 0; }std::lock_guard std::unique_lock
在這個什么都講究智能的時代,互斥所也不能跟不上潮流。std::lock_guard std::unique_lock與Mutex RAII相關,其智能性體現在如下兩個方面: 1. 方便對互斥量上鎖,不必手動解鎖 2. RAII機制確保在崩潰或異常退出的情況下仍然能夠正常釋放鎖
sample
二者在使用上是相似的,即在需要上鎖的地方運行
#include <mutex> // std::mutex std::lock_guard std::unique_lockstd::mutex mutex;// lock_guard std::lock_guard<std::mutex> lck(mutex);// unique_lock std::unique_lock<std::mutex> lck(mutex);Python中的多線程
由于Python解釋器的特性,Python對于cpu密集型的任務其加速效果并不明顯。但是對于這一門“爬蟲語言”,在大量的IO時用多線程還是很有必要的。
Python的標準庫提供了兩個模塊:_thread和threading,_thread是低級模塊,threading是高級模塊,對_thread進行了封裝。絕大多數情況下,我們只需要使用threading這個高級模塊。
與C++很相似,Python創建多線程也是創建一個線程實例,傳入線程函數,不一樣的地方在于Python需要手動調用start()以開始線程的執行,即創建和執行是分開的。
import threadingdef thread_new():print("This is thread {}".format(threading.current_thread().name))t = threading.Thread(target=thread_new, args=(), name="HelloThread") t.start() t.join() // join1. 線程中的參數訪問ThreadLocal
問題:如果有好幾個線程都調用某個函數來進行數據處理,那么就得把數據每次都作為參數傳入進去,每個函數都一層一層調用/傳參,如下:
def process_data_1(data):process_data_2(data)passdef process_data_2(data):passdef task_1(data):process_data_1(data)process_data_2(data)def task_2(data):process_data_2(data)process_data_2(data)可以看出以上參數的傳遞是非常復雜的。由于線程中的局部變量是只有當前線程能夠訪問的,因此這類參數的傳遞可以考慮使用線程中的“全局變量”來解決。 ThreadLocal就是解決這個問題的。
import threadinglocal_school = threading.local() # 創建在所有線程外的全局ThreadLocal對象def process_data_1():data = local_school.student# processdef process_data_2():data = local_school.student# processdef task_1(data):local_school.student = dataprocess_data_1()process_data_2()# 以下正常啟動線程local_school = threading.local()相當于定義在全局中的一個dict,每個線程都可以訪問得到,并修改/獲取里面的數據,并且不同的線程進行的操作互不影響。 注意: 1. threading.local()必須定義在所有線程之外 2. 線程中必須先修改ThreadLock中的數據然后才能訪問到
2. Python中的鎖機制Theading.Lock()
Python對線程鎖的實現也定義在Threading模塊中,實現起來非常簡單
data = 0 lock = Threading.Lock() def run_change():for i in range(100):lock.acquire() # 獲取鎖try:data += 1finally:lock.release() # 釋放鎖總結
以上是生活随笔為你收集整理的c++ 多线程 类成员函数_多线程(C++/Python)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 正圆锥体空间方程_你也可以理解“麦克斯韦
- 下一篇: java actor模型实例,详解The