目錄
1 前言
實際開發過程中,使用鎖會帶來一定性能的損失,但如果使用鎖也能滿足性能要求,對於鎖的使用就無妨。使用鎖可能帶來如下性能損失:
- 加鎖和解鎖操作,本身有一定的開銷;
- 臨界區的代碼不能併發執行;
- 進入臨界區的次數過於頻繁,線程之間對臨界區的爭奪太過激烈,若線程競爭互斥量失敗,就會陷入阻塞,讓出 CPU,導致執行上下文切換的次數要遠遠多於不使用互斥量的版本。
替代鎖的方式有很多,比如無鎖隊列。
2 注意事項
2.1 明確鎖的範圍
舉例:
if(hashtable.is_empty())
{
pthread_mutex_lock(&mutex);
htable_insert(hashtable, &elem);
pthread_mutex_unlock(&mutex);
}
可以發現上述代碼,雖然對hashtable的插入做了鎖的保護,但是判斷 hash_table 是否爲空也需要使用鎖保護,所以正確的寫法應該是:
pthread_mutex_lock(&mutex);
if(hashtable.is_empty())
{
htable_insert(hashtable, &elem);
}
pthread_mutex_unlock(&mutex);
2.2 減少鎖的粒度
通過減少被鎖的代碼範圍,減少被鎖的時間粒度,從而提高執行效率。
示例1:
void TaskPool::addTask(Task* task)
{
std::lock_guard<std::mutex> guard(m_mutexList);
std::shared_ptr<Task> spTask;
spTask.reset(task);
m_taskList.push_back(spTask);
m_cv.notify_one();
}
上述代碼中 guard 鎖保護 m_taskList,仔細分析下這段代碼發現,只需要鎖住m_taskList的插入處理動作就行,其他的處理並不需要,可修改如下:
void TaskPool::addTask(Task* task)
{
std::shared_ptr<Task> spTask;
spTask.reset(task);
{
std::lock_guard<std::mutex> guard(m_mutexList);
m_taskList.push_back(spTask);
}
m_cv.notify_one();
}
示例2:
void EventLoop::doPendingFunctors()
{
std::unique_lock<std::mutex> lock(mutex_);
for (size_t i = 0; i < pendingFunctors_.size(); ++i)
{
pendingFunctors_[i]();
}
}
上述代碼中 pendingFunctors_ 是被鎖保護的對象,需要執行完所有的對象纔會釋放鎖,這嚴重的降低了執行效率。可修改代碼如下:
void EventLoop::doPendingFunctors()
{
std::vector<Functor> functors;
{
std::unique_lock<std::mutex> lock(mutex_);
functors.swap(pendingFunctors_);
}
for (size_t i = 0; i < functors.size(); ++i)
{
functors[i]();
}
}
修改之後的代碼使用了一個局部變量 functors,然後把 pendingFunctors_ 中的內容倒換到 functors 中,這樣就可以釋放鎖了,允許其他線程操作 pendingFunctors_ ,現在只要繼續操作本地對象 functors 就可以了,提高了效率。
3 避免死鎖的建議
- 加了鎖,一定記得釋放鎖。但可能會因邏輯疏忽忘記釋放鎖,所以強烈建議使用RAII技術封裝鎖。
- 多線程請求鎖的方向要一致,以避免死鎖
關於“活鎖”的理解:當多個線程使用 trylock 系列的函數時,由於多個線程相互謙讓,導致即使在某段時間內鎖資源是可用的,也可能導致需要鎖的線程拿不到鎖。所以儘量避免不要過多的線程使用 trylock 請求鎖,以免出現“活鎖”現象,這是對資源的一種浪費。