天天看點

【Linux】線程互斥

在之前的部落格中,我講到線程的相關概念和線程的控制,在本節中我們聊一下線程互斥。

五個概念

  • 臨界資源

    多線程執行流共享的資源叫做臨界資源。大量執行流同時通路時可能會導緻資料二義性的問題。

  • 臨界區

    每個線程内部通路臨界資源區的代碼叫做臨界。我們可以通過控制代碼的讀寫規則保證臨界區的安全性。

  • 互斥

    任何時刻有且僅有一個執行流進入臨界區的情況稱互斥。我們通常在通路臨界資源時會對它進行加鎖保護,保證資料的安全性。

  • 同步

    在保證臨界資源安全的情況下讓多執行流按一定順序通路它稱同步。同步非同時,保證同步是為了協調多執行流的步調,避免饑餓問題。

  • 原子性

    不會被任何排程機制打斷的操作,該操作隻有兩種狀态,要麼完成,要麼未開始。一般隻需要一條彙編代碼就可以完成解析。

多線程并發的問題

大部分情況下,線程使用的資料局部變量,變量的位址空間線上程棧空間内,這種情況下變量隻屬于單個線程,其他線程無法通路。但是也有時候,我們的變量需要在程序間共享,這樣的變量稱為共享資料可用于完成線程間的互動。

然而多線程并發的操作共享變量會帶來一些問題:

先看下面代碼

#include <iostream>
#include <pthread.h>
#include <unistd.h>

using namespace std;
int goal=0;
void* handle(void* arg)
{
	int count=10000000;
	while(count--)
		goal++;
	return (void*)1;
}
int main()
{
	pthread_t t1,t2;
	pthread_create(&t1,NULL,handle,NULL);
	pthread_create(&t2,NULL,handle,NULL);
	sleep(1);
	cout<<goal<<endl;
	pthread_join(t1,NULL);
	pthread_join(t2,NULL);
	return 0;
}
           

不看不知道,一看吓一跳!!本來我以為我寫了兩個線程分别對goal加10000000次,最後的結果應該是20000000,然而!!

【Linux】線程互斥

通過運作結果,我發現運作了三次的結果都不相同,而且也沒有正确的結果。那麼問題出在哪裡呢?

原來是因為兩個線程是并發運作的,不同的線程在執行時有不同的寄存器,他們可能同時取走了某一時刻的goal,對它分别進行++後結果都加了一,但最後寫回記憶體時由原來的加2變成隻加1,導緻結果越錯越離譜。這裡的根本原因是++這個操作并不是原子性的。看下圖你應該就能明白了

【Linux】線程互斥

來看我們在vs2013下一段簡單的反彙編代碼:從圖中你可以清楚的看到對于goal的++操作分解成了三步,是以這就是導緻上面問題的罪魁禍首

【Linux】線程互斥

如果++是一個原子性的操作的話,那麼就不會出現上述的問題,是以現在要想解決上面的問題要麼将上述代碼實作原子性操作,要麼實作互斥的機制,Linux中确實提供了原子的++操作(atomic)但是這僅僅解決了目前問題,是以我們這裡重點介紹線程的互斥鎖實作互斥。

互斥量mutex

為了解決上面的問題,我們需要做到3點:
  1. 代碼必須要有互斥行為:當代碼進入臨界區執行時,不允許其他線程進入該臨界區
  2. 如果多個線程同時要執行臨界區的代碼,并且沒有線程在執行,那麼隻允許一個線程進入該臨界區
  3. 如果線程不在臨界區中執行,那麼該線程不能阻止其他線程進入臨界區

要做到以上三點,本質上需要一把鎖,Linux下提供了一把鎖叫做互斥量:

【Linux】線程互斥

接下來我介紹一下互斥量的接口。

互斥量的接口

初始化互斥量:有兩種方法

  1. 靜态配置設定

    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

  2. 動态配置設定

    int pthread_mutex_init(pthread_mutex_t restrict mutex, const pthread_mutexattr_trestrict attr);

    //參數1為要初始化的互斥鎖,參數2一般設定為NULL表示使用預設屬性

銷毀互斥量:

int pthread_mutex_destroy(pthread_mutex_t *mutex);

注意:使用PTHREAD_MUTEX_INITIALIZER初始化的互斥量不需要銷毀

不要銷毀一個被加鎖的互斥量

已經銷毀的互斥量不能再被線程加鎖

互斥量加鎖和解鎖:

int pthread_mutex_lock(pthread_mutex_t *mutex);

int pthread_mutex_unlock(pthread_mutex_t *mutex);

注意:調用pthread_lock時,可能會遇到以下情況:

互斥量處于未鎖狀态,該函數會将互斥量鎖定,同時傳回成功

發起函數調用時,其他線程已經鎖定互斥量,或者存在其他線程同時申請互斥量,但沒有競争到互斥量,那麼pthread_ lock調用會陷入阻塞(執行流被挂起),等待互斥量解鎖

下面我們使用上面的函數,修改之前的代碼為線程安全的:

【Linux】線程互斥

這時如果目前沒有線程在臨界區中,那麼線程一通路臨界資源的時候會先進行加鎖處理,當線程二要通路臨界資源時發現資源被鎖,是以OS将線程二設定為非r狀态并将其加入到阻塞隊列中等待線程一釋放鎖資源。這樣就可以保證線程在對臨界區通路時任一時刻隻有一個線程,也就是說我們現在可以認為我們的程式++是原子的。

運作結果如下:

【Linux】線程互斥
互斥量實作的原理

互斥鎖的實作原理實際上非常的簡單,我之前說到過,當彙編是一句指令時就可以認為是原子的,是以為了實作鎖的互斥操作,大多數體系結構對于互斥鎖的實作使用了swap或者exchange指令,該指令的作用是把寄存器和單中繼資料交換,因為就算是多線程,總線周期也有先後時間,是以利用此指令總能保證同一個時間隻有一個線程進入臨界區。

現在我們來看看如何使用swap或者exchange指令實作互斥鎖原理,來看下面的一段僞代碼:可以看到鎖資源最開始拿到的值為1,然後使用swap或exchange指令将寄存器中的值和鎖擁有的值交換,如果寄存器中現在值為1,那麼表示資源申請成功,如果寄存器中值為0,說明鎖資源中的1已經被其他線程交換走了,是以目前程序也就需要進入等待隊列。

【Linux】線程互斥

注意: 加鎖操作要求原子性,解鎖操作無要求。

線程安全

線程安全:多個線程并發處理同一段代碼時,不會出現不同的結果。常見的對全局變量或者靜态變量進行操作并且沒有鎖的保護下,會出現不同的結果。

重入:同一函數被不同的執行流調用,目前執行流未執行完畢就有其他執行流再次進入,稱這種現象為重入。一個函數在重入情況下,運作結果不會出現任何問題,稱為可重入函數,否則稱為不可重入函數。

常見線程不安全的情況:

  1. 不保護共享變量的函數
  2. 函數的狀态随着被調用,函數發生變化的函數
  3. 傳回指向靜态變量指針的函數
  4. 調用線程不安全函數的函數

常見線程安全的情況:

  1. 每個線程對于全局變量或者靜态變量隻有讀取的權限,而沒有寫入的權限
  2. 類或者接口對于線程來說都是原子操作
  3. 多個線程之間的切換不會導緻該接口的執行結果存在二義性

可重入與線程安全

  • 可重入函數一定是線程安全的,線程安全的函數不一定可重入
  • 不可重入函數不能由多個線程使用過,否則有線程安全問題
  • 若一個函數中有全局變量,則這個函數既不是線程安全也不是可重入的