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