Boost鎖~臨界區保護和臨界資源共享
前言:
除了thread,boost::thread另一個重要組成部分是mutex,以及工作在mutex上的boost::mutex::scoped_lock、condition和barrier,這些都是為實現執行緒同步提供的。
一、Boost鎖
mutex:
boost提供的mutex有6種:
boost::mutex
唯一鎖,同時只能由一個執行緒訪問,不分讀寫
boost::try_mutex
boost::timed_mutex
boost::recursive_mutex
讀寫鎖
boost::recursive_try_mutex
boost::recursive_timed_mutex
一、 下面僅對boost::mutex進行分析。
mutex類是一個CriticalSection(臨界區)封裝類,它在建構函式中新建一個臨界區並InitializeCriticalSection,然後用一個成員變數
void* m_mutex;
來儲存該臨界區結構。
除此之外,mutex還提供了do_lock、do_unlock等方法,這些方法分別呼叫EnterCriticalSection、 LeaveCriticalSection來修改成員變數m_mutex(CRITICAL_SECTION結構指標)的狀態,但這些方法都是private的,以防止我們直接對mutex進行鎖操作,所有的鎖操作都必須通過mutex的友元類detail
template <typename Mutex>
class lock_ops : private noncopyable
{
public:
static void lock(Mutex& m)
{
m.do_lock();
}
}
boost::thread的設計者為什麼會這麼設計呢?我想大概是:
1、boost::thread的設計者不希望被我們直接操作mutex,改變其狀態,所以mutex的所有方法都是private的(除了建構函式,解構函式)。
2、雖然我們可以通過lock_ops來修改mutex的狀態,如:
#include <boost/thread/thread.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/thread/detail/lock.hpp>
int main()
{
boost::mutex mt;
//mt.do_lock(); // Error! Can not access private member!
boost::detail::thread::lock_ops<boost::mutex>::lock(mt);
return 0;
}
但是,這是不推薦的,因為mutex、scoped_lock、condition、barrier是一套完整的類系,它們是相互協同工作的,像上面這麼操作沒有辦法與後面的幾個類協同工作。
scoped_lock:智慧鎖上面說過,不應該直接用lock_ops來操作mutex物件,那麼,應該用什麼呢?答案就是scoped_lock。與存在多種mutex一樣,存在多種與mutex對應的scoped_lock:
scoped_lock
scoped_try_lock
scoped_timed_lock
這裡我們只討論scoped_lock。
scoped_lock是定義在namespace boost::detail::thread下的,為了方便我們使用(也為了方便設計者),mutex使用了下面的typedef:
typedef detail::thread::scoped_lock<mutex> scoped_lock;
這樣我們就可以通過:
boost::mutex::scoped_lock
來使用scoped_lock類模板了。
由於scoped_lock的作用:僅在於對mutex加鎖/解鎖(即使mutex
EnterCriticalSection/LeaveCriticalSection),因此,它的介面也很簡單,除了建構函式外,僅有lock/unlock/locked(判斷是否已加鎖),及型別轉換操作符void*,一般我們不需要顯式呼叫這些方法,因為scoped_lock的建構函式是這樣定義的:
explicit scoped_lock(Mutex& mx, bool initially_locked=true)
: m_mutex(mx), m_locked(false)
{
if (initially_locked) lock();
}
注:m_mutex是一個mutex的引用。
因此,當我們不指定initially_locked引數構造一個scoped_lock物件時,scoped_lock會自動對所繫結的mutex加鎖,而解構函式會檢查是否加鎖,若已加鎖,則解鎖;當然,有些情況下,我們可能不需要構造時自動加鎖,這樣就需要自己呼叫lock方法。後面的condition、barrier也會呼叫scoped_lock的lock、unlock方法來實現部分方法。
正因為scoped_lock具有可在構造時加鎖,析構時解鎖的特性,我們經常會使用區域性變數來實現對mutex的獨佔訪問。
#include <boost/thread/thread.hpp>
#include <boost/thread/mutex.hpp>
#include <iostream>
boost::mutex io_mutex;
void count() // worker function
{
for (int i = 0; i < 10; ++i)
{
boost::mutex::scoped_lock lock(io_mutex);
std::cout << i << std::endl;
}
}
int main(int argc, char* argv[])
{
boost::thread thrd1(&count);
boost::thread thrd2(&count);
thrd1.join();
thrd2.join();
return 0;
}
在每次輸出資訊時,為了防止整個輸出過程被其它執行緒打亂,通過對io_mutex加鎖(進入臨界區),從而保證了輸出的正確性。
Java的synchronized可用於對方法加鎖,對程式碼段加鎖,對物件加鎖,對類加鎖(仍然是物件級的),這幾種加鎖方式都可以通過上面講的物件鎖來模擬;相反,在Java中實現全域性鎖好像有點麻煩,必須將請求封裝到類中,以轉換成上面的四種 synchronized形式之一。
condition
condition的介面如下:
class condition : private boost::noncopyable // Exposition only
{
public:
// construct/copy/destruct
condition();
~condition();
// notification
void notify_one();
void notify_all();
// waiting
template<typename ScopedLock> void wait(ScopedLock&);
template<typename ScopedLock, typename Pred> void wait(ScopedLock&, Pred);
template<typename ScopedLock>
bool timed_wait(ScopedLock&, const boost::xtime&);
template<typename ScopedLock, typename Pred>
bool timed_wait(ScopedLock&, Pred);
};
其中wait 用於等待某個condition的發生,而timed_wait則提供具有超時的wait功能,notify_one用於喚醒一個等待該condition發生的執行緒,notify_all則用於喚醒所有等待該condition發生的執行緒。
由於condition的語義相對較為複雜,它的實現也是整個boost::thread庫中最複雜的(對Windows版本而言,對支援pthread的版本而言,由於pthread已經提供了pthread_cond_t,使得condition實現起來也十分簡單),下面對wait和notify_one進行簡要分析。
condition內部包含了一個condition_impl物件,由該物件執行來處理實際的wait、notify_one...等操作
二、臨界資源保護(並行程式設計過程中)
從事軟體研發工作有近兩年的時間了,從自己的感覺來說,系統軟體,特別是核心軟體開發的難點在於併發程式設計,或者從更深層次的角度來講應該是並行程式設計(多核程式設計)。並行程式設計的難點在於臨界資源的保護。通常各並行的執行緒或者是程序之間都會存在共享的臨界資源,如果這些臨界資源處理不當,那麼小則程式執行出錯;大則系統崩潰。所以,我個人認為只要將臨界資源處理好,那麼並行程式設計就不是問題了。
下面結合自己這一段時間的程式設計“經驗”,對並行程式設計過程中所應該注意的問題和一些方法做初步小結。
1、 避免臨界資源,減少臨界資源的數量
並行程式設計中,臨界資源越多,程式設計將會越複雜,所以在程式設計之初,需要考慮臨界資源的數量,儘可能的減少臨界資源。另一方面,一個資源在不同的時間點會呈現出不同的特點。在某些情況下其可能為臨界資源;在某些情況其表現為非臨界資源。例如,操作需要建立一個裝置,該裝置需要新增到共享資源區中,為了避免臨界資源的產生,可以先將建立裝置的所有資訊都初始化完畢之後,最後將裝置新增到共享資源區,這樣在一定程度上避免了臨界資源,可以簡化建立裝置的過程,提高系統性能。所以,從這一點上我們可以得出一個結論,在處理臨界資源時,很多時候可以將資源從總的資源池中取出來,讓被訪問的臨界資源成為一個非臨界資源,在短時間內為一個上下文獨享,這樣可以簡化設計,提高效率。但是,並不是所有的應有都允許這樣的操作,然而,總的原則應該是不變的,那就是儘可能的減少臨界資源的數量,減少併發程式的依賴關係,這是從“根”上簡化併發程式的設計與實現。
2、 函式的設計需要考慮應用上下文環境
並行程式中的函式設計並不僅僅是函式功能的封裝,演算法的封裝,還需要考慮函式的應用上下文,也就是設計實現的函式將在什麼環境下被呼叫。這一點非常重要,如果處理不好,那麼很容易出現死鎖、系統崩潰等現象。
函式設計需要考慮應用上下文,這一點一個非常重要的原因在於並行程式,特別是核心程式中的上下文特點存在很大的區別。例如,Linux核心上下文分為普通的使用者程序上下文、軟中斷上下文、中斷上下文。在使用者程序上下文中,函式的限制條件不是很強烈,能夠睡眠的函式都可以執行,但是在中斷上下文限制條件很強烈,睡眠函式是不能執行的,所以,函式設計需要考慮應用上下文環境。
除了上述原因之外,另一個非常重要的原因在於加鎖的問題,而這個問題可能更容易在設計實現過程中被忽視。函式的執行上下文一定要考慮加鎖情況,例如一個函式在持有spinlock資源鎖的條件下呼叫一個可能引起睡眠的函式,那麼系統肯定崩潰;另外,一個函式在持有鎖A的時候再次呼叫可能訪問鎖A的函式,那麼系統肯定死鎖。總的來說,上述這些問題的根源都在於函式呼叫時沒有考慮執行上下文的加鎖情況,導致錯誤加鎖或者亂序加鎖。
因此,在並行程式設計過程中,設計一個函式需要考慮一下,這個函式是為哪個上下文寫的?呼叫這個函式存在哪些限制?這是在普通函式設計之上提出的更高要求。
3、 臨界資源的保護需要考慮讀寫性、競爭上下文
臨界資源的保護需要考慮對臨界資源訪問的讀寫性,如果訪問臨界資源的多個上下文存在讀操作,那麼訪問臨界資源的鎖可以被優化。通常可以採用讀鎖對臨界資源進行讀訪問。另外,在臨界資源訪問時一定要考慮競爭上下文,如果競爭上下文為中斷上下文,那麼需要考慮加鎖時間與可睡眠性,通常在Linux系統中採用Spinlock對其進行保護;如果競爭上下文為普通的程序上下文,那麼保護的方法將簡單的多。
臨界資源保護時,鎖的設計非常重要,通常在設計實現過程中會遇到大鎖、小鎖的抉擇。大鎖的設計實現簡單,競爭點可以分析的非常清晰,但是程式效率將會大打折扣;小鎖的設計實現複雜,競爭點的分析、考慮將會變得複雜,程式實現效率將會大大提高。我個人認為,在設計臨界資源保護時,首先需要分析清楚競爭上下文,根據競爭上下文對資源訪問的競爭點分析結果,設計合理的鎖資源。儘可能在不太影響效能的前提下(鎖不能成為系統的效能瓶頸),設計大鎖資源。在後繼的效能優化過程中,如果有必要再將鎖資源進行必要細化。
並行程式設計中,臨界資源的訪問是程式設計的一大難點,一個好的程式設計人員,一定需要將程式的功能模組切分好,程式的執行上下文及上下文之間的關係設計好,臨界資源及資源訪問的鎖設計好。只有這樣設計的程式才能具備一個完美的框架,只有擁有完美框架的程式才有可能成為一個非常出色的程式。
總的而言,在涉及臨界資源訪問時,設計開發人員需要問一下自己:訪問時這個資源是否為臨界資源?這個資源將在何種上下文中執行?資源的競爭點有哪些?對資源封裝的函式是否可以在該上下文中應用?不管怎麼樣,並行程式設計時,一定要清楚,共享的資源並不是在一個上下文中引用,需要對它進行合理保護。
這是我對臨界資源保護的一點小結,不是很全面,都是一些個人理解,寫下文字記錄一下,希望大家批評。