概念
線程同步的方法有多種惶看,互斥量、信號(hào)量六孵、條件變量纬黎、讀寫鎖等〗僦希互斥量在允許或阻塞對(duì)臨界區(qū)的訪問上是很有效的本今,線程是在對(duì)已加鎖的互斥量加鎖時(shí)發(fā)生阻塞;條件變量則允許線程由于一些未達(dá)到的條件而阻塞主巍,此處的“條件”可以由用戶來定義冠息,在訪問該條件時(shí)需要加鎖(互斥量),如果條件沒達(dá)到孕索,線程將阻塞在該條件上逛艰。
條件變量特別適用于多個(gè)線程等待某個(gè)條件的發(fā)生。如果不使用條件變量搞旭,那么每個(gè)線程就需要不斷嘗試獲得互斥鎖并檢查條件是否發(fā)生散怖,這樣大大浪費(fèi)了系統(tǒng)的資源菇绵。
條件變量與互斥量通常一起使用,原因是線程在因條件未滿足而阻塞并等待前镇眷,需要訪問“條件”咬最,而“條件”是允許其它線程修改的,因此欠动,訪問“條件”時(shí)需要加鎖永乌,訪問結(jié)束后釋放鎖。所以具伍,這也就可以解釋翅雏,條件變量的等待操作pthread_cond_wait()需要一個(gè)互斥量參數(shù),在pthread_cond_wait()內(nèi)部將調(diào)用線程放到等待隊(duì)列上后沿猜,要解鎖互斥量枚荣,以讓其它線程可以訪問“條件”;否則啼肩,該線程將一直占用互斥量橄妆,其它線程將不能訪問“條件”。不過祈坠,pthread_cond_wait()內(nèi)的解鎖互斥量只是臨時(shí)的害碾,在其它線程修改“條件”使其滿足要求并喚醒當(dāng)前阻塞線程時(shí),pthread_cond_wait()內(nèi)部又會(huì)鎖住互斥量赦拘,這樣做的目的是:
- 使互斥量的狀態(tài)在進(jìn)入慌随、退出pthread_cond_wait()函數(shù)時(shí)保持一致;
- 本身“判斷‘條件’是否滿足要求躺同,否則阻塞并等待‘條件’”這部分程序?qū)儆谂R界區(qū)阁猜,在訪問臨界區(qū)前對(duì)互斥量加鎖,退出臨界區(qū)后對(duì)互斥量解鎖是應(yīng)有的操作蹋艺,也就是說剃袍,真正地釋放互斥量鎖的操作在退出臨界區(qū)后;所以捎谨,在pthread_cond_wait()函數(shù)結(jié)束時(shí)民效,應(yīng)保持互斥量的加鎖狀態(tài);
通常涛救,“判斷‘條件’是否滿足要求畏邢,否則阻塞并等待‘條件’”這部分臨界區(qū)代碼中的“判斷‘條件’”采用while循環(huán)來做,因?yàn)閜thread_cond_wait()在阻塞并等待的過程中检吆,“條件”滿足要求后舒萎,其它線程會(huì)調(diào)用pthread_cond_signal()或pthread_cond_broadcast()喚醒阻塞線程,在pthread_cond_wait()將其從阻塞隊(duì)列放到就緒隊(duì)列及pthread_cond_wait()重新獲取互斥鎖之間蹭沛,其它線程有可能又改變了“條件”逆甜,使得此時(shí)的“條件”已不再滿足要求虱肄;因此,若不采用while()循環(huán)判斷“條件”是否成立交煞,在阻塞線程被喚醒之后咏窿,它以為“條件”滿足要求,實(shí)際上已經(jīng)被其它線程修改了素征。
當(dāng)其它線程修改了“條件”使之滿足要求后集嵌,會(huì)調(diào)用pthread_cond_signal()或pthread_cond_broadcast()發(fā)送信號(hào),發(fā)送信號(hào)的步驟順序有兩種:
- 順序一
- 調(diào)用pthread_mutex_lock()對(duì)互斥量加鎖御毅;
- 改變條件使之滿足要求根欧;
- 向阻塞并等待條件的線程發(fā)送信號(hào)(比如調(diào)用pthread_cond_broadcast());
- 調(diào)用pthread_mutex_unlock()對(duì)互斥量解鎖端蛆;
- 順序二
- 調(diào)用pthread_mutex_lock()對(duì)互斥量加鎖凤粗;
- 改變條件使之滿足要求;
- 調(diào)用pthread_mutex_unlock()對(duì)互斥量解鎖今豆;
- 向阻塞并等待條件的線程發(fā)送信號(hào)(比如調(diào)用pthread_cond_broadcast())嫌拣;
這兩種步驟順序都可以,但都存在一些不足呆躲。在順序一中异逐,發(fā)送條件成立信號(hào)的步驟在對(duì)互斥量解鎖之前,也就是發(fā)送線程仍是占有鎖的插掂,當(dāng)阻塞線程收到信號(hào)后結(jié)束休眠灰瞻,但pthread_cond_wait()在退出之前會(huì)對(duì)互斥量重新加鎖,可發(fā)送信號(hào)的線程尚未釋放鎖辅甥,所以剛結(jié)束休眠的阻塞線程酝润,對(duì)互斥量加鎖又導(dǎo)致阻塞了;在順序二中璃弄,發(fā)送條件成立信號(hào)的步驟在對(duì)互斥量解鎖之后要销,此時(shí)發(fā)送信號(hào)時(shí),發(fā)送線程已經(jīng)解鎖互斥量谢揪,但在剛解鎖互斥量之后蕉陋,有可能其它線程在發(fā)送線程發(fā)送信號(hào)之前捐凭,成功對(duì)互斥量加鎖拨扶,拿到了“條件”的訪問權(quán),因此茁肠,可以修改“條件”患民,這樣一來,使得“條件”不再滿足阻塞線程的要求垦梆,但發(fā)送線程不知道匹颤,仍會(huì)調(diào)用pthread_cond_broadcast()發(fā)送信號(hào)仅孩,阻塞線程收到信號(hào)后被喚醒,可此時(shí)的“條件”是不滿足要求的印蓖,這一點(diǎn)可以通過while循環(huán)判斷“條件”是否成立來修正辽慕,即便阻塞線程被喚醒,但它仍會(huì)判斷“條件”是否成立赦肃,不成立則繼續(xù)阻塞等待溅蛉。
實(shí)例 - 讀寫鎖的實(shí)現(xiàn)
目標(biāo)
通過線程通信原語實(shí)現(xiàn)讀寫鎖,讀寫鎖的要求:
- 同一時(shí)間他宛,允許多個(gè)讀者讀取數(shù)據(jù)船侧;
- 同一時(shí)間,只有一個(gè)寫者寫數(shù)據(jù)厅各;
- 寫優(yōu)先級(jí)比讀優(yōu)先級(jí)高镜撩。
分析
最簡(jiǎn)單的讀寫鎖實(shí)現(xiàn)
最簡(jiǎn)單的讀寫鎖實(shí)現(xiàn)如下,只用一個(gè)互斥量表示讀寫鎖队塘,任何讀操作或?qū)懖僮鞫急仨毾葘?duì)互斥量加鎖袁梗,在讀、寫完畢之后人灼,再釋放鎖围段。這樣實(shí)現(xiàn)的讀寫鎖雖然能保證讀和寫是互斥的,但有如下弊端:
- 讀操作之間也是互相排斥的投放,由于多個(gè)讀操作并不會(huì)改變共享區(qū)的內(nèi)容奈泪,所以這樣加鎖再釋放鎖的讀寫鎖實(shí)現(xiàn),大大降低了讀取的效率灸芳;
- 無法保證寫操作的優(yōu)先級(jí)高于讀操作涝桅,下面的這種實(shí)現(xiàn)方式將讀、寫放到了同一層級(jí)上烙样,訪問共享區(qū)的操作權(quán)取決于誰先對(duì)互斥量成功加鎖冯遂。
pthread_mutex_t rw_lock;
int rw_lock_init(pthread_mutex_t* prw_lock)
{
pthread_mutex_init(prw_lock, NULL);
return 0;
}
int rw_lock_lock(pthread_mutex_t* prw_lock)
{
pthread_mutex_lock(prw_lock);
return 0;
}
int rw_lock_unlock(pthread_mutex_t* prw_lock)
{
pthread_mutex_unlock(prw_lock);
return 0;
}
優(yōu)化方法
互斥鎖的目的,是為了避免出現(xiàn)如下競(jìng)爭(zhēng)條件:一個(gè)線程在未完成讀操作之前谒获,另一個(gè)線程寫操作改變了數(shù)據(jù)蛤肌,或者多個(gè)線程同時(shí)進(jìn)行寫操作;而在多個(gè)讀操作之間是沒有必要互斥的批狱。解決方式是引入對(duì)“讀者數(shù)量”的計(jì)數(shù)裸准。
讀寫鎖優(yōu)化一 - 讀操作之間共享
第一次優(yōu)化之后的讀寫鎖實(shí)現(xiàn)如下,引入了對(duì)“讀者數(shù)量”的計(jì)數(shù)赔硫,由于“讀者數(shù)量”是動(dòng)態(tài)變化的炒俱,由讀者間共享,屬共享變量,所以加入reader_mutex互斥量對(duì)其保護(hù)权悟,以保證讀者對(duì)其訪問的互斥砸王;寫者之間、讀者與寫者之間通過write_mutex達(dá)到互斥峦阁。當(dāng)讀者調(diào)用讀鎖加鎖操作時(shí)谦铃,首先判斷調(diào)用者是否是第一個(gè)讀者,若是榔昔,則對(duì)寫鎖加鎖荷辕;若不是第一個(gè)讀者,即前面仍有多個(gè)讀者件豌,則在釋放reader_mutex后直接訪問共享數(shù)據(jù)疮方。這樣做的結(jié)果是,在第一個(gè)讀者讀取共享數(shù)據(jù)前茧彤,對(duì)寫鎖加鎖骡显,導(dǎo)致讀寫之間互斥,之后的讀者再讀取共享數(shù)據(jù)時(shí)曾掂,便不用再因等待寫鎖而阻塞(“最簡(jiǎn)單的讀寫鎖”方法)惫谤,即讀者之間是不互斥的,可以有多個(gè)讀者訪問共享數(shù)據(jù)珠洗。
但這樣的讀寫鎖實(shí)現(xiàn)溜歪,仍有一個(gè)弊端:無法保證寫操作的優(yōu)先級(jí)高于讀操作,當(dāng)有大量讀者訪問共享數(shù)據(jù)時(shí)许蓖,寫者想對(duì)共享數(shù)據(jù)寫入蝴猪,必須等到所有讀者退出之后,才得以操作膊爪,出現(xiàn)“寫者饑餓”的情況自阱。
typedef struct rw_lock_t
{
pthread_mutex_t reader_mutex;
pthread_mutex_t write_mutex;
int reader_counts;
} rw_lock;
int rw_lock_init(rw_lock* prw_lock)
{
pthread_mutex_init(&prw_lock->reader_mutex, NULL);
pthread_mutex_init(&prw_lock->write_mutex, NULL);
reader_counts = 0;
}
int rw_lock_read_lock(rw_lock* prw_lock)
{
pthread_mutex_lock(&prw_lock->reader_mutex);
if(0 == reader_counts++)
{
pthread_mutex_lock(&prw_lock->write_mutex);
}
pthread_mutex_unlock(&prw_lock->reader_mutex);
return 0;
}
int rw_lock_read_unlock(rw_lock* prw_lock)
{
pthread_mutex_lock(&prw_lock->reader_mutex);
if(0 == --reader_counts)
{
pthread_mutex_unlock(&prw_lock->write_mutex);
}
pthread_mutex_unlock(&prw_lock->reader_mutex);
return 0;
}
int rw_lock_write_lock(rw_lock* prw_lock)
{
pthread_mutex_lock(&prw_lock->write_lock);
return 0;
}
int rw_lock_write_unlock(rw_lock* prw_lock)
{
pthread_mutex_unlock(&prw_lock->write_lock);
return 0;
}
讀寫鎖優(yōu)化二 - 寫操作優(yōu)先級(jí)高于讀操作
深入分析
讀寫鎖要滿足的條件
- 寫者的優(yōu)先級(jí)大于讀者
這條規(guī)則主要體現(xiàn)讀者進(jìn)入共享區(qū)和寫者離開共享區(qū)的判斷流程。
(1) 當(dāng)讀者進(jìn)入共享區(qū)時(shí)米酬,應(yīng)首先檢查是否有寫者阻塞沛豌,再檢查是否有寫者在寫,再檢查是否有讀者已在共享區(qū)赃额;只有沒有寫者阻塞或?qū)懻咴趯憰r(shí)加派,讀者才能進(jìn)入共享區(qū);(2)當(dāng)寫者離開共享區(qū)時(shí)跳芳,應(yīng)首先檢查是否有寫者在阻塞芍锦,再檢查是否有讀者阻塞;只有沒有寫者阻塞時(shí)筛严,才能繼續(xù)對(duì)讀者是否阻塞做判斷醉旦; - 讀、寫之間互斥
這條規(guī)則的含義是只要一方在共享區(qū)內(nèi)桨啃,另一方就必須阻塞车胡,不得進(jìn)入,只有等到在共享區(qū)內(nèi)的一方退出時(shí)照瘾,另一方才有機(jī)會(huì)進(jìn)入匈棘。
(1) 當(dāng)讀者離開共享區(qū)時(shí),應(yīng)首先檢查共享區(qū)內(nèi)的讀者是否已全部退出析命,若是主卫,再檢查是否有寫者在阻塞,若有鹃愤,則喚醒寫者(寫者阻塞有兩種原因?qū)е拢?. 有讀者正在共享區(qū)內(nèi)讀取數(shù)據(jù)簇搅,寫者需等到當(dāng)前讀者都退出后,再進(jìn)入共享區(qū)软吐;2. 當(dāng)前共享區(qū)內(nèi)正有寫者在寫瘩将,此時(shí)想進(jìn)入共享區(qū)的寫者需阻塞);(2) 當(dāng)寫者進(jìn)入共享區(qū)時(shí)凹耙,首先檢查當(dāng)前共享區(qū)內(nèi)是否有讀者存在姿现,若有,則寫者應(yīng)阻塞肖抱; - 寫备典、寫之間互斥
這條規(guī)則的含義是只要有寫者在共享區(qū)內(nèi)執(zhí)行寫操作,其它的寫者就必須在共享區(qū)外等待意述,直到當(dāng)前寫者退出后提佣,其它寫者才有機(jī)會(huì)。
(1)當(dāng)寫者進(jìn)入共享區(qū)荤崇,且檢查了當(dāng)前共享區(qū)內(nèi)沒有讀者存在镐依,應(yīng)再檢查共享區(qū)內(nèi)是否有寫者在寫,若有天试,寫者應(yīng)阻塞槐壳;若沒有,寫者可直接進(jìn)入共享區(qū)喜每; - 讀务唐、讀之間共享
這條規(guī)則的含義是即便當(dāng)前共享區(qū)內(nèi)有讀者在讀,其它讀者也是可以進(jìn)入到共享區(qū)內(nèi)的带兜,而不必阻塞枫笛。
(1)當(dāng)讀者要進(jìn)入數(shù)據(jù)共享區(qū)時(shí),如果沒有寫者阻塞和寫者在進(jìn)行寫操作刚照,則可以直接進(jìn)入共享區(qū)刑巧;
讀寫鎖的加鎖、解鎖流程
- 讀加鎖流程
讀者在進(jìn)入數(shù)據(jù)共享區(qū)時(shí),應(yīng)首先檢查是否有寫者阻塞或共享區(qū)內(nèi)寫者是否在寫啊楚,兩者只要有其一吠冤,讀者阻塞數(shù)量應(yīng)累加,且當(dāng)前讀者應(yīng)阻塞恭理;若兩者皆沒有拯辙,讀者將共享區(qū)內(nèi)的讀者數(shù)目累加,然后直接進(jìn)入共享區(qū)颜价; - 讀解鎖流程
讀者在離開數(shù)據(jù)共享區(qū)時(shí)涯保,共享區(qū)內(nèi)的讀者數(shù)目減1,并判斷值是否為0周伦,若是夕春,接著判斷是否有寫者在阻塞,若有专挪,則直接喚醒某個(gè)寫者撇他;若沒有寫者阻塞或共享區(qū)內(nèi)的讀者數(shù)目不為0,則直接退出共享區(qū)(注意:當(dāng)沒有寫者在寫或沒有寫者阻塞時(shí)狈蚤,讀者是不會(huì)阻塞的)困肩; - 寫加鎖流程
寫者在進(jìn)入數(shù)據(jù)共享區(qū)時(shí),首先判斷當(dāng)前共享區(qū)內(nèi)是否有讀者存在脆侮,若有锌畸,寫者阻塞數(shù)目累加,然后寫者阻塞靖避;若沒有潭枣,再接著判斷共享區(qū)內(nèi)是否有寫者在寫,若有幻捏,寫者阻塞數(shù)目累加盆犁,然后寫者阻塞;若沒有篡九,寫者將寫標(biāo)志置位谐岁,并進(jìn)入共享區(qū); - 寫解鎖流程
寫者在離開數(shù)據(jù)共享區(qū)時(shí)榛臼,首先將寫標(biāo)志清零伊佃,接著判斷是否有寫者在阻塞,若有沛善,則喚醒某個(gè)寫者航揉;若沒有,再判斷是否有讀者在阻塞金刁,若有帅涂,則喚醒所有讀者议薪;若沒有,則直接離開共享區(qū)媳友;
讀寫鎖的實(shí)現(xiàn)
經(jīng)過上述分析斯议,在實(shí)現(xiàn)讀寫鎖時(shí),需要記錄“存在于共享區(qū)內(nèi)在讀的讀者數(shù)量”庆锦、“讀者阻塞的數(shù)量”、“寫者阻塞的數(shù)量”轧葛、“寫者是否在寫共享區(qū)的標(biāo)志”搂抒,以及和讀者、寫者阻塞相關(guān)的兩個(gè)條件變量尿扯,進(jìn)入共享區(qū)的互斥鎖求晶。
typedef struct rw_lock_ {
unsigned int on-readers; //readers exists in sharing region
unsigned int read_waiters; //waiting readers
unsigned int write_waiters; //waiting writers
unsigned int write_flag;
pthread_mutex_t mutex_lock;
pthread_cond_t rwait_cond;
pthread_cond_t wwait_cond;
} rw_lock_t;
int rw_lock_init(rw_lock_t* prw_lock)
{
prw_lock->on-readers = 0;
prw_lock->read_waiters = 0;
prw_lock->write_waiters = 0;
prw_lock->write_flag = 0;
pthread_mutex_init(&prw_lock->mutex_lock, NULL);
pthread_cond_init(&prw_lock->rwait_cond, NULL);
pthread_cond_init(&prw_lock->wwait_cond, NULL);
return 0;
}
int rw_lock_destroy(rw_lock_t* prw_lock)
{
prw_lock->on-readers = 0;
prw_lock->read_waiters = 0;
prw_lock->write_waiters = 0;
prw_lock->write_flag = 0;
pthread_mutex_destroy(&prw_lock->mutex_lock);
pthread_cond_destroy(&prw_lock->rwait_cond);
pthread_cond_destroy(&prw_lock->wwait_cond);
return 0;
}
int rw_lock_read_lock(rw_lock_t* prw_lock)
{
pthread_mutex_lock(&prw_lock->mutex_lock);
while(prw_lock->write_waiters > 0 || prw_lock->write_flag == 1)
{
prw_lock->read_waiters++;
pthread_cond_wait(&prw_lock->rwait_cond, &prw_lock->mutex_lock);
prw_lock->read_waiters--;
}
prw_lock->on-readers++;
pthread_mutex_unlock(&prw_lock->mutex_lock);
return 0;
}
int rw_lock_read_unlock(rw_lock_t* prw_lock)
{
pthread_mutex_lock(&prw_lock->mutex_lock);
prw_lock->on-readers--;
if(prw_lock->on-readers == 0)
{
if(prw_lock->write_waiters > 0)
{
pthread_cond_signal(&prw_lock->rwait_cond);
}
}
pthread_mutex_unlock(&prw_lock->mutex_lock);
return 0;
}
int rw_lock_write_lock(rw_lock_t* prw_lock)
{
pthread_mutex_lock(&prw_lock->mutex_lock);
while(prw_lock->on-readers > 0 || prw_lock->write_flag == 1)
{
prw_lock->write_waiters++;
pthread_cond_wait(&prw_lock->wwait_cond);
prw_lock->write_waiters--;
}
prw_lock->write_flag = 1;
pthread_mutex_unlock(&prw_lock->mutex_lock);
return 0;
}
int rw_lock_write_unlock(rw_lock_t* prw_lock)
{
pthread_mutex_lock(&prw_lock->mutex_lock);
prw_lock->write_flag = 0;
if(prw_lock->write_waiters > 0)
{
pthread_cond_signal(&prw_lock->wwait_cond);
}
else if(prw_lock->read_waiters > 0)
{
pthread_cond_broadcast(&prw_lock->rwait_cond);
}
pthread_mutex_unlock(&prw_lock->mutex_lock);
return 0;
}
另有非阻塞讀鎖和非阻塞寫鎖,實(shí)現(xiàn)思路分別為:
- 非阻塞讀鎖:當(dāng)讀者進(jìn)入共享區(qū)時(shí)衷笋,如果檢查到有寫者阻塞或共享區(qū)的寫標(biāo)志置為1芳杏,則直接返回,而不是阻塞等待辟宗;若兩者均未檢測(cè)到爵赵,則直接進(jìn)入共享區(qū);
- 非阻塞寫鎖:當(dāng)寫者進(jìn)入共享區(qū)時(shí)泊脐,如果檢查到共享區(qū)內(nèi)有讀者在讀或有寫者在寫空幻,則直接返回,而不是阻塞等待容客;若兩者均未檢測(cè)到秕铛,則直接進(jìn)入共享區(qū);
int rw_lock_read_trylock(rw_lock_t* prw_lock)
{
pthread_mutex_lock(&prw_lock->mutex_lock);
if(prw_lock->write_waiters > 0 || prw_lock->write_flag == 1)
{
return -1;
}
prw_lock->on-readers++;
pthread_mutex_unlock(&prw_lock->mutex_lock);
return 0;
}
int rw_lock_write_trylock(rw_lock_t* prw_lock)
{
pthread_mutex_lock(&prw_lock->mutex_lock);
if(prw_lock->on-readers > 0 || prw_lock->write_flag == 1)
{
return -1;
}
prw_lock->write_flag = 1;
pthread_mutex_unlock(&prw_lock->mutex_lock);
return 0;
}
參考文章
1. ChinaUnix | 可不可以不取名 | 線程同步:條件變量的使用細(xì)節(jié)分析
2. Cnblogs | 寒莓 | 動(dòng)手實(shí)現(xiàn)讀寫鎖
3. Cnblogs | myd620 | 一步一步實(shí)現(xiàn)讀寫鎖
4. Cnblogs | Vamei | Linux多線程與同步
5. CSDN | LUCKYOJ | 實(shí)現(xiàn)線程讀寫鎖的四種方法