目錄
前言
1.互斥
1.先來見一種現象(數據不一致問題)
2.如何解決上述問題
3.理解為什么數據會不一致&&認識加鎖的接口
4.理解鎖
5.鎖的封裝
前言
? 在前面對線程的概念和控制的學習過程中,我們知道了線程是共享地址空間的,也就是會共享大部分資源,那么這個時候就會產生新的問題——并發訪問,最直觀的感受就是每次運行得出的結果值大概率不一致,這種執行結果不一致的現象是非常致命,因為它具有隨機性,即結果可能是對的,也可能是錯的,無法可靠的完成任務
? 為了解決這一問題,我們要引入新的解決方案——同步和互斥,我們先來講互斥!
1.互斥
1.先來見一種現象(數據不一致問題)
? ?部分情況,線程使?的數據都是局部變量,變量的地址空間在線程棧空間內,這種情況,變量歸屬單個線程,其他線程?法獲得這種變量。
? 但有時候,很多變量都需要在線程間共享,這樣的變量稱為共享變量,可以通過數據的共享,完成線程之間的交互。
? 多個線程并發的操作共享變量,會帶來?些問題,比如說下面的一段模擬搶票的實驗代碼
// 操作共享變量會有問題的售票系統代碼
#include <iostream>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
?
int ticket = 100;
?
void *route(void *arg)
{char *id = (char *)arg;while (1){if (ticket > 0) // 1.判斷{usleep(1000); ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? // 模擬搶票化的時間printf("%s sells ticket:%d\n", id, ticket); // 2.模擬搶到了票ticket--; ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? // 3.票數--}else{break;}}return nullptr;
}
?
int main(void)
{pthread_t t1, t2, t3, t4;pthread_create(&t1, NULL, route, (void *)"thread 1");pthread_create(&t2, NULL, route, (void *)"thread 2");pthread_create(&t3, NULL, route, (void *)"thread 3");pthread_create(&t4, NULL, route, (void *)"thread 4");
?pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);
}
可以看到結果都把票干到負數了,這在現實中可是一件很糟糕的事情,比如說高鐵明明只有200個座位,卻有201的人搶到了票,這個人是沒有位置的,說明多個線程并發的操作共享變量,會帶來?些問題
2.如何解決上述問題
上面的代碼中
臨界區:
while (1){if (ticket > 0) // 1.判斷{usleep(1000); ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? // 模擬搶票化的時間printf("%s sells ticket:%d\n", id, ticket); // 2.模擬搶到了票ticket--; ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? // 3.票數--}else{break;}}
共享資源是:int ticket =1000;
其他代碼都屬于非臨界區
我們要想辦法保護臨界區:通過在臨界區中前后加鎖可以保護起來!
#include <iostream>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
?
int ticket = 100;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; // 對鎖進行初始化
?
void *route(void *arg)
{char *id = (char *)arg;while (1){pthread_mutex_lock(&lock);if (ticket > 0) // 1.判斷{usleep(1000); ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? // 模擬搶票化的時間printf("%s sells ticket:%d\n", id, ticket); // 2.模擬搶到了票ticket--; ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? // 3.票數--pthread_mutex_unlock(&lock);}else{pthread_mutex_unlock(&lock);break;}}return nullptr;
}
?
int main(void)
{pthread_t t1, t2, t3, t4;pthread_create(&t1, NULL, route, (void *)"thread 1");pthread_create(&t2, NULL, route, (void *)"thread 2");pthread_create(&t3, NULL, route, (void *)"thread 3");pthread_create(&t4, NULL, route, (void *)"thread 4");
?pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);
}
可以從結果看到,此時就不會出現票數為負的情況了,順利解決數據不一致的問題
3.理解為什么數據會不一致&&認識加鎖的接口
首先我們需要知道的是ticket--不是原子性的操作,它會被匯編代碼轉換成三條指令
? load :將共享變量ticket從內存加載到寄存器中
? update : 更新寄存器??的值,執?-1操作
? store :將新值,從寄存器寫回共享變量ticket的內存地址
比如:
0xFF00 載入 ebx ticket
0xFF02 減少 ebx 1
0xFF04 寫回 0x1111 ebx
假設我們有A、B兩線程,ticket初始是100,在cpu調度A線程執行到0xFF04時要發生線程切換,此時需要保存A的上下文數據:ebx(ticket)為99,cpu的pc指針保存0xFF04地址,然后cpu開始調度B線程,B線程運氣很好,在循環執行讓ticket減到1之后剛好才要被切換,保存上下文之后cpu又重新調度A,此時pc指針保存的0xFF04地址是要執行寫回內存的指令,那么這個時候的ticket又回到了99,這就發生了數據不一致問題,也說明了ticket--不是原子性的操作
[^] ?我們暫時這么去理解原子性:一條匯編就是原子的?
我們上面的票數減到負數其實主要的問題不是出在ticket--這個操作,而是出戰if條件判斷ticket>0這一操作上,對于ticket值是否大于0做判斷也是一種計算(邏輯計算,得到的是布爾值),執行時先載入cpu,再判斷;那么此時如果有3個線程,ticket此時為1,都完成1的載入后被切走了(因為加了休眠的時間,導致線程沒來及做--操作就讓下一個線程進來了),后面按順序喚醒線程時時并行判斷都是1就允許進入了,三個線程此時串行載入ticket,執行ticket--然后再寫回內存使得ticket此時從1->0->-1->-2就變成-2了
上面的問題告訴了我們:全局資源沒有加保護,可能會有并發問題——線程安全問題,同時要形成上面的問題需要在多線程中,制造更多的并發、更多的切換,切換的時間點:1.時間片到了 2.阻塞式IO 3.sleep等等...;選擇新的線程時間點:從內核態返回用戶態的時候,進行檢查
要解決以上問題,需要做到三點:
? 代碼必須要有互斥?為:當代碼進?臨界區執?時,不允許其他線程進?該臨界區。
? 如果多個線程同時要求執?臨界區的代碼,并且臨界區沒有線程在執?,那么只能允許?個線程進?該臨界區。
? 如果線程不在臨界區中執?,那么該線程不能阻?其他線程進?臨界區
要做到這三點,本質上就是需要?把鎖 ——pthread_mutex_t(互斥鎖/互斥量)
[^] ?pthread_mutex_init的第二個參數為鎖屬性,我們不用管設為nullptr就行?
加鎖規則:盡量加鎖的范圍粒度要比較細,盡可能不要包含太多的非臨界區代碼
對臨界區進行保護本質其實就是用鎖來對臨界區進行保護
問題1:如果有線程不遵守我們的規則,那就是一個bug,所有線程必須遵守!!
問題2:枷鎖之后,在臨界區內部允許線程切換嗎?切換了會怎么樣?
答:允許切換,但是不會怎么樣,因為我當前線程并沒有釋放鎖,該線程持有鎖被切換,
其他線程也必須等我被切換回來執行完代碼、釋放鎖了才能展開申請鎖的競爭,進而
進入臨界區(當然這樣就會導致多線程執行代碼的速度變慢)
加鎖和解鎖的本質就是把整個代碼塊進行原子化,讓其他無法中斷該線程
4.理解鎖
經過上?的例?,?家已經意識到單純的 i++或者 ++i都不是原?的,有可能會有數據?致性問題
鎖的原理:
-
硬件級實現:關閉時鐘中斷
-
軟件級實現:
為了實現互斥鎖操作,大多數體系結構都提供了swap或exchange指令(只有一條指令保證原子性),該指令的作用是把寄存器和內存單元的數據相交換
下面是一段鎖在匯編的偽代碼:
5.鎖的封裝
其實在c++中用鎖很簡單,我們只需要包含#include<mutex.h>頭文件,然后定義一個鎖被封裝好的mutex類的對象,然后就可以用這個對象調用這個mutex類中的lock、unlock接口實現申請鎖和解鎖等操作啦(我們其實在c++階段是學過的)
使用c++封裝的鎖來解決我們上面的搶票數據不一致問題代碼:
#include <iostream>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <mutex>
?
int ticket = 100;
// pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; // 對鎖進行初始化
std::mutex lock;
?
void *route(void *arg)
{char *id = (char *)arg;while (1){// pthread_mutex_lock(&lock);lock.lock();if (ticket > 0) // 1.判斷{usleep(1000); ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? // 模擬搶票化的時間printf("%s sells ticket:%d\n", id, ticket); // 2.模擬搶到了票ticket--; ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? // 3.票數--// pthread_mutex_unlock(&lock);lock.unlock();}else{// pthread_mutex_unlock(&lock);lock.unlock();break;}}return nullptr;
}
?
int main(void)
{pthread_t t1, t2, t3, t4;pthread_create(&t1, NULL, route, (void *)"thread 1");pthread_create(&t2, NULL, route, (void *)"thread 2");pthread_create(&t3, NULL, route, (void *)"thread 3");pthread_create(&t4, NULL, route, (void *)"thread 4");
?pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);
}
我們當然也可以自己造個輪子,也跟著封裝一個我們自己的鎖
Mutex.hpp
#pragma once
#include <pthread.h>
#include <iostream>
?
namespace MutexModle
{class Mutex{public:Mutex(){pthread_mutex_init(&_mutex, nullptr);}
?// 申請鎖void Lock(){// pthread_mutex_lock成功返回0,失敗返回錯誤碼int n = pthread_mutex_lock(&_mutex);if (n != 0){std::cerr << "申請鎖失敗" << std::endl;return;}}
?// 解鎖void Unlock(){int n = pthread_mutex_unlock(&_mutex);if (n != 0){std::cerr << "解鎖失敗" << std::endl;return;}}
?~Mutex(){pthread_mutex_destroy(&_mutex);}
?private:pthread_mutex_t _mutex;};
?// 實現RAII風格的互斥鎖class LockGuard{public:LockGuard(Mutex &mutex): _mutex(mutex){_mutex.Lock();}
?~LockGuard(){_mutex.Unlock();}
?private:Mutex &_mutex;};
}
TestMutex.cc
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include "Mutex.hpp"
using namespace MutexModle;
?
int ticket = 100;
?
// 我們自己封裝的鎖類
Mutex lock;
?
void *route(void *arg)
{char *id = (char *)arg;while (1){// 申請鎖// lock.Lock();// 通過LockGuard類構造對象調用構造函數中的申請鎖代碼實現自動加鎖// 這就是RAII風格的互斥鎖的實現LockGuard guard(lock);
?if (ticket > 0) // 1.判斷{usleep(1000); ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? // 模擬搶票化的時間printf("%s sells ticket:%d\n", id, ticket); // 2.模擬搶到了票ticket--; ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? // 3.票數--// 解鎖// lock.Unlock();// 通過guard臨時對象出作用域會自動調用析構函數進行自動解鎖}else{// lock.Unlock();// 通過guard臨時對象出作用域會自動調用析構函數進行自動解鎖break;}}return nullptr;
}
?
int main(void)
{pthread_t t1, t2, t3, t4;pthread_create(&t1, NULL, route, (void *)"thread 1");pthread_create(&t2, NULL, route, (void *)"thread 2");pthread_create(&t3, NULL, route, (void *)"thread 3");pthread_create(&t4, NULL, route, (void *)"thread 4");
?pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);
?return 0;
}
結果當然也是顯而易見的成功解決數據不一致問題啦!
我們上面其實實現了RAII風格(智能指針就是利用這個思想的)的互斥鎖