c++多線程

多線程編程過程中,最令人煩惱的就是數據競爭

問題,此時mutex(互斥量)就派上了用場。

數據競爭(data race):是指在非線程安全的情況下,多線程對同一個地址空間進行寫操作。一般來說,我們都會通過線程同步方法來保證數據的安全,比如採用互斥量或者讀寫鎖。

一個應用程序,無論大小,都可以分解為這樣的流程或其組合:讀數據(取)->運算->寫數據(存)。數據讀取,因為不涉及寫操作,所以不會引發數據競爭。當然,數據讀取一樣會存在線程的切換,數據訪問的先後,不可能多個線程同時訪問同一存儲空間,只是不需要保護而已。那麼,怎樣避免線程切換,減少數據讀取時的等待?我們可以為每個線程都配備一個資源管理實例,這樣每個線程訪問不同的儲存空間,其實也就是多進程的思想。

常用的使用方法

一個純粹的運算過程,即:不含數據的讀寫

,也是線程安全的。但是這樣的運算是無意義的,運算的目的就是為了拿到輸入並輸出結果。這裡就涉及了寫數據,我們需要藉助mutex,來保證寫數據的安全。這裡的數據指的是位於數據段和堆上的數據:各個線程共享的數據,通常全局變量/堆上分配的內存/靜態變量都屬於這類數據。所有涉及全局數據修改的操作,都應該被保護。下面是mutex用法的見到那示例:(本文示例均在VS2015下編譯運行)

<code>// 示例1:多個線程訪問同一全局變量
#include <thread>
#include <mutex>
#include <iostream>
int main()
{
std::mutex ctx;
int cnt = 0; // 如果移到lambda表達式內會如何?

auto func = [&](const std::string& tag)
{
// cnt = 0; // A
ctx.lock(); // B1
for (int i = 0; i < 5; i++)
{
cnt++;
std::cout< }
ctx.unlock(); // B2
};

std::thread t1(func, "thread 1: ");
std::thread t2(func, "thread 2: ");

t1.join();
t2.join();
}
////////////////////////////////////////////OUTPUT/////////////////////////////////////////////////
// 加鎖的結果 | 不加鎖的結果 | cnt放在A處作為局部變量的結果
// thread 1: 1 | thread 1: 2 | thread 1: 1
// thread 1: 2 | thread 2: 2 | thread 2: 1
// thread 1: 3 | thread 1: 4 | thread 1: 2
// thread 2: 4 | thread 2: 4 | thread 2: 2
// thread 2: 5 | thread 1: 6 | thread 1: 3
// thread 2: 6 | thread 2: 6 | thread 2: 3
///////////////////////////////////////////////////////////////////////////////////////////////////////////
/<iostream>/<mutex>/<thread>/<code>

上面的示例說明3個問題,一是:多個線程訪問同一對象,並修改該對象的值,此時構成數據競爭,結果往往與預期的不符;二是:當發生數據競爭時,可以通過加鎖得到期望的結果;三是:線程內的局部變量,位於當前線程的堆棧中,屬於線程私有,不構成數據競爭;

示例1中,B1處調用lock上鎖,B2處調用unlock解鎖。如果兩者之間發生異常,導致程序在解鎖之前異常返回,其餘線程將無法訪問,即:死鎖。為了保證任何情況下,只要上鎖一定會執行解鎖的操作,可以使用lock_guard (C++ 的RAII特性)。其實就是對lock/unlock函數的封裝,在封裝類的構造函數上鎖,在析構函數里調用unlock解鎖,這樣就可以保證:即使程序在鎖定期間發生了異常,也會安全的釋放鎖,不會發生死鎖。。示例如下:

<code>  auto func = [&](const std::string& tag)
{
std::lock_guard<:mutex> lock(ctx);
// ctx.lock(); // B1
for (int i = 0; i < 5; i++)
{
cnt++;
std::cout< }
// ctx.unlock(); // B2
};
/<code>

除了std::mutex之外,還有std::recursive_mutex/std::timed_mutex/std::recursive_timed_mutex,網上的資料很多且不常用,可以自行學習(me too)。同時,mutex文件中還提供了std::unique_lock,std::call_once, std::try_lock等操作,其實也挺常用的(見過幾次),具體用法沒深究,不敢妄言。

線程安全的精心設計

給大家看個“實例管理器”的例子,會比之前的例子複雜很多,但是也更接近實戰,它負責實例的申請/釋放/計數和運算:

<code>const static int MAX_INST_NUM = 20; // 實例限制
// 運算實例
class instance
{
public:
instance(int id) { inst_id_ = id; }
~instance() {}
public:
int push_data(const char* data)
{
// 這裡有必要保護嗎
std::lock_guard<:mutex> lock(inst_mutex_);
str_arr_.push_back(data);

return 0;
}
int compute()
{
for (auto str : str_arr_)
{
std::cout< }
return 0;
}
private:
std::mutex\t\t\t\t\t\t\t\t\t\t\t inst_mutex_;
std::vector<:string> str_arr_;
int\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tinst_id_;
}

// 實例管理器
class inst_mngr
{
public:
inst_mngr() { inst_cnt = 0; }
~inst_mngr() {}
public:
int alloc_inst(int inst_id)
{
// 全局變量-使用鎖保護
{
std::lock_guard<:mutex> guard(ctx);
if (inst_cnt >MAX_INST_NUM){
std::cout< }
}
instance* new_inst = new instance(inst_id);

// 鎖保護全局變量
{
std::lock_guard<:mutex> guard(ctx);
inst_cnt_++;
}

// 限於篇幅,不做異常檢查
inst_pool_.inster(std::make_pair(inst_id, new_inst));

return 0;
}


int release_inst(int inst_id)
{
if (inst_pool_.find(inst_id) == inst_pool_.end())
{
std::cout< return -1;
}
const instance* cur_inst = inst_pool_[inst_id];
if (nullptr != cur_inst)
{
delete cur_inst;
cur_inst = nullptr;
}

// 鎖保護全局變量
{
std::lock_guard<:mutex> guard(ctx);
inst_cnt_--;
}
return 0;
}

private:
std::mutex\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tmngr_mutex_;
int inst_cnt_; // 用於實例計數
std::unordered_map inst_pool_; // 實例池
}
/<code>

上面的例子,我想說明3個問題,也是我在工作中一度比較疑惑的點:

1.類內的互斥量是每個對象獨有,所以在一個函數中上了鎖,即使函數運行期間,其他線程獲取了該對象,也不能調用任何函數(正在運行的函數或其他成員函數),因為沒有解鎖;

2.其實線程的安全與否,部分取決於調用者,如上面的例子,如果一個線程只管理一個而特定的實例,此時實例所擁有的函數時沒必要加鎖的,因為線程內部還是串行執行的;

3.數據競爭可以分為:主線程與子線程的競爭,子線程之間的競爭。所以我們在加鎖時要同時考慮這兩方面,子線程之間的競爭相對時比較簡單的;子線程與主線程之間的競爭,要求我們要充分考慮調用者的使用場景,這一點需要一定的經驗;

關於線程安全的一點思考

加鎖解鎖是比較耗時的。學習了多線程後,你有沒有過這樣瘋狂的想法:“我可以控制每個函數,每調條語句,完美的控制程序的並行”。並行的粒度越細越好嗎? 顯然不是,線程的切換/鎖的使用都是很耗時的,從收益及後期維護來看,都是不值得的。抓住熱點,採用合理的方案優化,解決問題,早點回家睡覺:)。

關於std::thread類的介紹,請參考:


分享到:


相關文章: