【Linux C編程】第十五章 線程同步
一、整體大綱
二、線程同步
1. 同步概念
所謂同步,即同時起步,協調一致。不同的對象,對“同步”的理解方式略有不同。如,設備同步,是指在兩個設備之間規定一個共同的時間參考;數據庫同步,是指讓兩個或多個數據庫內容保持一
致,或者按需要部分保持一致;文件同步,是指讓兩個或多個文件夾里的文件保持一致等等。
而編程中、通信中所說的同步與生活中大家印象中的同步概念略有差異。“同”字應是指協同、協助、互相配合。主旨在協同步調,按預定的先后次序運行。
2. 線程同步
(1)線程同步概念
同步即協同步調,按預定的先后次序運行。
線程同步,指一個線程發出某一功能調用時,在沒有得到結果之前,該調用不返回。同時其它線程為保證數據一致性,不能調用該功能。
舉例1: 銀行存款 5000。柜臺,折:取3000;提款機,卡:取 3000。剩余:2000
舉例2: 內存中100字節,線程T1欲填入全1, 線程T2欲填入全0。但如果T1執行了50個字節失去cpu,T2執行,會將T1寫過的內容覆蓋。當T1再次獲得cpu繼續 從失去cpu的位置向后寫入1,當執
行結束,內存中的100字節,既不是全1,也不是全0。
產生的現象叫做“與時間有關的錯誤”(time related)。為了避免這種數據混亂,線程需要同步。
“同步”的目的,是為了避免數據混亂,解決與時間有關的錯誤。實際上,不僅線程間需要同步,進程間、信號間等等都需要同步機制。
因此,所有“多個控制流,共同操作一個共享資源”的情況,都需要同步。
(2)數據混亂原因
1)資源共享(獨享資源則不會)
2)調度隨機(意味著數據訪問會出現競爭)
3)線程間缺乏必要的同步機制。
以上3點中,前兩點不能改變,欲提高效率,傳遞數據,資源必須共享。只要共享資源,就一定會出現競爭。只要存在競爭關系,數據就很容易出現混亂。
所以只能從第三點著手解決。使多個線程在訪問共享資源的時候,出現互斥。
3. 實現線程同步
(1)互斥量mutex
Linux中提供一把互斥鎖mutex(也稱之為互斥量)。
每個線程在對資源操作前都嘗試先加鎖,成功加鎖才能操作,操作結束解鎖。
資源還是共享的,線程間也還是競爭的,
但通過“鎖”就將資源的訪問變成互斥操作,而后與時間有關的錯誤也不會再產生了。
但應注意:同一時刻,只能有一個線程持有該鎖。
當A線程對某個全局變量加鎖訪問,B在訪問前嘗試加鎖,拿不到鎖,B阻塞。C線程不去加鎖,而直接訪問該全局變量,依然能夠訪問,但會出現數據混亂。
所以,互斥鎖實質上是操作系統提供的一把“建議鎖”(又稱“協同鎖”),建議程序中有多線程訪問共享資源的時候使用該機制。但,并沒有強制限定。
因此,即使有了mutex,如果有線程不按規則來訪問數據,依然會造成數據混亂。
1)主要函數
pthread_mutex_init函數 pthread_mutex_destroy函數 pthread_mutex_lock函數 pthread_mutex_trylock函數 pthread_mutex_unlock函數 以上5個函數的返回值都是:成功返回0, 失敗返回錯誤號。 pthread_mutex_t 類型,其本質是一個結構體。為簡化理解,應用時可忽略其實現細節,簡單當成整數看待。 pthread_mutex_t mutex; 變量mutex只有兩種取值1、0。
pthread_mutex_init函數
初始化一個互斥鎖(互斥量)?--->?初值可看作1
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
參1:傳出參數,調用時應傳 &mutex
restrict關鍵字:只用于限制指針,告訴編譯器,所有修改該指針指向內存中內容的操作,只能通過本指針完成。不能通過除本指針以外的其他變量或指針修改。
參2:互斥量屬性。是一個傳入參數,通常傳NULL,選用默認屬性(線程間共享)。 參APUE.12.4同步屬性
靜態初始化:如果互斥鎖 mutex 是靜態分配的(定義在全局,或加了static關鍵字修飾),可以直接使用宏進行初始化。e.g. ?pthead_mutex_t muetx = PTHREAD_MUTEX_INITIALIZER;
動態初始化:局部變量應采用動態初始化。e.g. ?pthread_mutex_init(&mutex, NULL)
pthread_mutex_destroy函數
銷毀一個互斥鎖
int pthread_mutex_destroy(pthread_mutex_t *mutex);
pthread_mutex_lock函數
加鎖。可理解為將 mutex--(或-1)
int pthread_mutex_lock(pthread_mutex_t *mutex);
pthread_mutex_unlock函數
解鎖??衫斫鉃閷utex ++(或+1)
int pthread_mutex_unlock(pthread_mutex_t *mutex);
pthread_mutex_trylock函數
嘗試加鎖
int pthread_mutex_trylock(pthread_mutex_t *mutex);
2)加鎖與解鎖
lock與unlock:
lock嘗試加鎖,如果加鎖不成功,線程阻塞,阻塞到持有該互斥量的其他線程解鎖為止。
unlock主動解鎖函數,同時將阻塞在該鎖上的所有線程全部喚醒,至于哪個線程先被喚醒,取決于優先級、調度。默認:先阻塞、先喚醒。
例如:T1 T2 T3 T4 使用一把mutex鎖。T1加鎖成功,其他線程均阻塞,直至T1解鎖。T1解鎖后,T2 T3 T4均被喚醒,并自動再次嘗試加鎖。
可假想mutex鎖 init成功初值為1。 lock 功能是將mutex--。 unlock將mutex++
lock與trylock:
lock加鎖失敗會阻塞,等待鎖釋放。
trylock加鎖失敗直接返回錯誤號(如:EBUSY),不阻塞。
trylock示例
1 #include
3)加鎖步驟測試
看如下程序:該程序是非常典型的,由于共享、競爭而沒有加任何同步機制,導致產生于時間有關的錯誤,造成數據混亂:
多線程數據打印混亂
1 #include
練習:修改該程序,使用mutex互斥鎖進行同步。
定義全局互斥量,初始化init(&m, NULL)互斥量,添加對應的destry
兩個線程while中,兩次printf前后,分別加lock和unlock
將unlock挪至第二個sleep后,發現交替現象很難出現。
線程在操作完共享資源后本應該立即解鎖,但修改后,線程抱著鎖睡眠。睡醒解鎖后又立即加鎖,這兩個庫函數本身不會阻塞。
所以在這兩行代碼之間失去cpu的概率很小。因此,另外一個線程很難得到加鎖的機會。
bug修復版(加互斥鎖)
1 #include
結論:
在訪問共享資源前加鎖,訪問結束后立即解鎖。鎖的“粒度”應越小越好。
4)死鎖
線程試圖對同一個互斥量A加鎖兩次。
線程1擁有A鎖,請求獲得B鎖;線程2擁有B鎖,請求獲得A鎖
練習:編寫程序,實現上述兩種死鎖現象。
(2)讀寫鎖
1)概念
與互斥量類似,但讀寫鎖允許更高的并行性。其特性為:寫獨占,讀共享。
2)讀寫鎖狀態
一把讀寫鎖具備三種狀態:
a. 讀模式下加鎖狀態 (讀鎖)
b. 寫模式下加鎖狀態 (寫鎖)
c. 不加鎖狀態
3)讀寫鎖特性
讀寫鎖是“寫模式加鎖”時, 解鎖前,所有對該鎖加鎖的線程都會被阻塞。
讀寫鎖是“讀模式加鎖”時, 如果線程以讀模式對其加鎖會成功;如果線程以寫模式加鎖會阻塞。
讀寫鎖是“讀模式加鎖”時, 既有試圖以寫模式加鎖的線程,也有試圖以讀模式加鎖的線程。那么讀寫鎖會阻塞隨后的讀模式鎖請求。優先滿足寫模式鎖。讀鎖、寫鎖并行阻塞,寫鎖優先級高。
讀寫鎖也叫共享-獨占鎖。當讀寫鎖以讀模式鎖住時,它是以共享模式鎖住的;當它以寫模式鎖住時,它是以獨占模式鎖住的。寫獨占、讀共享。
讀寫鎖非常適合于對數據結構讀的次數遠大于寫的情況。
3)主要函數
pthread_rwlock_init函數 pthread_rwlock_destroy函數 pthread_rwlock_rdlock函數 pthread_rwlock_wrlock函數 pthread_rwlock_tryrdlock函數 pthread_rwlock_trywrlock函數 pthread_rwlock_unlock函數 以上7 個函數的返回值都是:成功返回0, 失敗直接返回錯誤號。 pthread_rwlock_t類型 用于定義一個讀寫鎖變量。 pthread_rwlock_t rwlock;
pthread_rwlock_init函數
初始化一把讀寫鎖
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
參2:attr表讀寫鎖屬性,通常使用默認屬性,傳NULL即可。
pthread_rwlock_destroy函數
銷毀一把讀寫鎖
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
pthread_rwlock_rdlock函數
以讀方式請求讀寫鎖。(常簡稱為:請求讀鎖)
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
pthread_rwlock_wrlock函數
以寫方式請求讀寫鎖。(常簡稱為:請求寫鎖)
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
pthread_rwlock_unlock函數
解鎖
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
pthread_rwlock_tryrdlock函數
非阻塞以讀方式請求讀寫鎖(非阻塞請求讀鎖)
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
pthread_rwlock_trywrlock函數
非阻塞以寫方式請求讀寫鎖(非阻塞請求寫鎖)
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
4)讀寫鎖示例
看如下示例,同時有多個線程對同一全局數據讀、寫操作。
讀寫鎖示例
1 #include
讀寫鎖場景練習:
a. 線程A加寫鎖成功,線程B請求讀鎖
線程B阻塞
b. 線程A持有讀鎖,線程B請求寫鎖
線程B阻塞
c. 線程A持有讀鎖,線程B請求讀鎖
B加鎖成功
d. 線程A持有讀鎖,然后線程B請求寫鎖,然后線程C請求讀鎖
BC阻塞
A釋放后,B加鎖
B釋放后,C加鎖
e. 線程A持有寫鎖,然后線程B請求讀鎖,然后線程C請求寫鎖
BC阻塞
A釋放,C加鎖
C釋放,B加鎖
(3)條件變量
1)概念
條件變量本身不是鎖!但它也可以造成線程阻塞。通常與互斥鎖配合使用。給多線程提供一個會合的場所。
2)主要應用函數
pthread_cond_init函數 pthread_cond_destroy函數 pthread_cond_wait函數 pthread_cond_timedwait函數 pthread_cond_signal函數 pthread_cond_broadcast函數 以上6 個函數的返回值都是:成功返回0, 失敗直接返回錯誤號。 pthread_cond_t類型 用于定義條件變量 pthread_cond_t cond;
pthread_cond_init函數
初始化一個條件變量
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
參2:attr表條件變量屬性,通常為默認值,傳NULL即可
也可以使用靜態初始化的方法,初始化條件變量:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_cond_destroy函數
銷毀一個條件變量
int pthread_cond_destroy(pthread_cond_t *cond);
pthread_cond_wait函數
阻塞等待一個條件變量
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
函數作用:
a. 阻塞等待條件變量cond(參1)滿足
b. 釋放已掌握的互斥鎖(解鎖互斥量)相當于pthread_mutex_unlock(&mutex);
a.b.兩步為一個原子操作。
c. 當被喚醒,pthread_cond_wait函數返回時,解除阻塞并重新申請獲取互斥鎖pthread_mutex_lock(&mutex);
pthread_cond_timedwait函數
限時等待一個條件變量
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
參3: 參看man?sem_timedwait函數,查看struct?timespec結構體。
struct timespec { time_t tv_sec; /* seconds */ 秒 long tv_nsec; /* nanosecondes*/ 納秒 }
形參abstime:絕對時間。
如:time(NULL)返回的就是絕對時間。而alarm(1)是相對時間,相對當前時間定時1秒鐘。
struct timespec t = {1, 0};
pthread_cond_timedwait (&cond, &mutex, &t);?只能定時到 1970年1月1日 00:00:01秒(早已經過去)
正確用法:
time_t cur = time(NULL); 獲取當前時間。 struct timespec t; 定義timespec 結構體變量t t.tv_sec = cur+1; 定時1秒 pthread_cond_timedwait (&cond, &mutex, &t); 傳參 參APUE.11.6線程同步條件變量小節 在講解setitimer函數時我們還提到另外一種時間類型: struct timeval { time_t tv_sec; /* seconds */ 秒 suseconds_t tv_usec; /* microseconds */ 微秒 };
pthread_cond_signal函數
喚醒至少一個阻塞在條件變量上的線程
int pthread_cond_signal(pthread_cond_t *cond);
pthread_cond_broadcast函數
喚醒全部阻塞在條件變量上的線程
int pthread_cond_broadcast(pthread_cond_t *cond);
3)生產者消費者條件變量模型
線程同步典型的案例即為生產者消費者模型,而借助條件變量來實現這一模型,是比較常見的一種方法。假定有兩個線程,一個模擬生產者行為,一個模擬消費者行為。兩個線程同時操作一個共
享資源(一般稱之為匯聚),生產向其中添加產品,消費者從中消費掉產品。
看如下示例,使用條件變量模擬生產者、消費者問題:
條件變量實現生產者消費者模型
1 #include
4)條件變量的優點
相較于mutex而言,條件變量可以減少競爭。
如直接使用mutex,除了生產者、消費者之間要競爭互斥量以外,消費者之間也需要競爭互斥量,但如果匯聚(鏈表)中沒有數據,消費者之間競爭互斥鎖是無意義的。有了條件變量機制以后,只有
生產者完成生產,才會引起消費者之間的競爭。提高了程序效率。
(4)信號量
1)概念
進化版的互斥鎖(1 --> N)
由于互斥鎖的粒度比較大,如果我們希望在多個線程間對某一對象的部分數據進行共享,使用互斥鎖是沒有辦法實現的,只能將整個數據對象鎖住。這樣雖然達到了多線程操作共享數據時保證數
據正確性的目的,卻無形中導致線程的并發性下降。線程從并行執行,變成了串行執行。與直接使用單進程無異。
信號量,是相對折中的一種處理方式,既能保證同步,數據不混亂,又能提高線程并發。
2)主要應用函數
sem_init函數 sem_destroy函數 sem_wait函數 sem_trywait函數 sem_timedwait函數 sem_post函數 以上6 個函數的返回值都是:成功返回0, 失敗返回-1,同時設置errno。(注意,它們沒有pthread前綴) sem_t類型,本質仍是結構體。但應用期間可簡單看作為整數,忽略實現細節(類似于使用文件描述符)。 sem_t sem; 規定信號量sem不能 < 0。頭文件
信號量基本操作:
sem_wait:
a. 信號量大于0,則信號量-- (類比pthread_mutex_lock)
b. 信號量等于0,造成線程阻塞
對應 ->
sem_post: 將信號量++,同時喚醒阻塞在信號量上的線程 (類比pthread_mutex_unlock)
但由于sem_t的實現對用戶隱藏,所以所謂的++、--操作只能通過函數來實現,而不能直接++、--符號。
信號量的初值,決定了占用信號量的線程的個數。
sem_init函數
初始化一個信號量
int sem_init(sem_t *sem, int pshared, unsigned int value);
參1:sem信號量
參2:pshared取0用于線程間;取非0(一般為1)用于進程間
參3:value指定信號量初值
sem_destroy函數
銷毀一個信號量
int sem_destroy(sem_t *sem);
sem_wait函數
給信號量加鎖 --
int sem_wait(sem_t *sem);
sem_post函數
給信號量解鎖 ++
int sem_post(sem_t *sem);
sem_trywait函數
嘗試對信號量加鎖 -- (與sem_wait的區別類比lock和trylock)
int sem_trywait(sem_t *sem);
sem_timedwait函數
限時嘗試對信號量加鎖 --
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
參2:abs_timeout采用的是絕對時間。
定時1秒: time_t cur = time(NULL); 獲取當前時間。 struct timespec t; 定義timespec 結構體變量t t.tv_sec = cur+1; 定時1秒 t.tv_nsec = t.tv_sec +100; sem_timedwait(&sem, &t); 傳參
3)生產者消費者信號量模型
使用信號量完成線程間同步,模擬生產者,消費者問題。
信號量實現生產者消費者模型
1 #include
(5)進程間同步
互斥量mutex
進程間也可以使用互斥鎖,來達到同步的目的。但應在pthread_mutex_init初始化之前,修改其屬性為進程間共享。mutex的屬性修改函數主要有以下幾個。
主要應用函數:
pthread_mutexattr_t mattr 類型: 用于定義mutex鎖的【屬性】 pthread_mutexattr_init函數: 初始化一個mutex屬性對象 int pthread_mutexattr_init(pthread_mutexattr_t *attr); pthread_mutexattr_destroy函數: 銷毀mutex屬性對象 (而非銷毀鎖) int pthread_mutexattr_destroy(pthread_mutexattr_t *attr); pthread_mutexattr_setpshared函數: 修改mutex屬性。 int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared); 參2:pshared取值: 線程鎖:PTHREAD_PROCESS_PRIVATE (mutex的默認屬性即為線程鎖,進程間私有) 進程鎖:PTHREAD_PROCESS_SHARED
進程間mutex示例:
進程間使用mutex來實現通信
1 #include
(6)文件鎖
借助 fcntl函數來實現鎖機制。 操作文件的進程沒有獲得鎖時,可以打開,但無法執行read、write操作。
fcntl函數: 獲取、設置文件訪問控制屬性。
int fcntl(int fd, int cmd, ... /* arg */ );
參2:
F_SETLK (struct flock *) 設置文件鎖(trylock) F_SETLKW (struct flock *) 設置文件鎖(lock)W --> wait F_GETLK (struct flock *) 獲取文件鎖
參3:
struct flock { ... short l_type; 鎖的類型:F_RDLCK 、F_WRLCK 、F_UNLCK short l_whence; 偏移位置:SEEK_SET、SEEK_CUR、SEEK_END off_t l_start; 起始偏移:1000 off_t l_len; 長度:0表示整個文件加鎖 pid_t l_pid; 持有該鎖的進程ID:(F_GETLK only) ... };
進程間文件鎖示例:
進程間文件鎖
1 #include
依然遵循“讀共享、寫獨占”特性。但!如若進程不加鎖直接操作文件,依然可訪問成功,但數據勢必會出現混亂。
【思考】:多線程中,可以使用文件鎖嗎?
多線程間共享文件描述符,而給文件加鎖,是通過修改文件描述符所指向的文件結構體中的成員變量來實現的。因此,多線程中無法使用文件鎖。
Linux 任務調度
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。