文章目錄
- 線程鎖的本質
- 局部鎖的使用
- 鎖的封裝及演示
- 線程饑餓問題
- 線程加鎖本質
- 可重入和線程安全
- 死鎖問題
根據前面內容的概述, 上述我們已經知道了在linux下關于線程封裝和線程互斥,鎖的相關的概念, 下面就來介紹一下關于線程鎖的一些其他概念.
線程鎖的本質
當這個鎖是全局的或者是靜態屬性時,可以使用PTHREAD_MUTEX_INITIALIZER (initializer 初始化器(初始化列表那樣的東西))
,這個宏來進行初始化.
局部鎖的使用
局部的鎖就要使用pthread_mutex_init()
創建, pthread_mutex_destroy()
來銷毀
回調函數處:
鎖的封裝及演示
這邊引入鎖的封裝, 將線程名稱與鎖進行封裝的一種保護機制(lock guard):.
意義在于: 創建后再程序結束時會自動釋放鎖,方便使用
LockGuard.hpp定義
#pragma once
#include <iostream>
//不定義鎖,默認認為外部會給我們傳入鎖對象
class Mutex
{
public:Mutex(pthread_mutex_t *lock):_lock(lock)//包裝加鎖功能可以實現啟動自定義鎖時自定加鎖,然后對應的函數功能結束自動解鎖(利用構造函數和析構函數的性質實現){}void Lock(){pthread_mutex_lock(_lock);}void Unlock(){pthread_mutex_unlock(_lock);}~Mutex(){}private:pthread_mutex_t *_lock;
};class LockGuard
{
public:LockGuard(pthread_mutex_t *lock):_mutex(lock)//_mutex是Mutex的對象,該對象調用對應的方法{_mutex.Lock();//調用Mutex類的加鎖方法}~LockGuard (){_mutex.Unlock();}private:Mutex _mutex;
};
Thread.hpp 對pthread線程的封裝實現
#pragma once
#include <iostream>
#include <functional>
#include <pthread.h>
#include <string>using namespace std;
template<class T>
using func_t = function<void(T)>;//std::function 是C++標準庫中的一個模板類,可以包裝任何可調用目標(callable target),比如函數、lambda表達式、函數對象(functor)等。
template<class T>
class Thread
{
public:Thread(const string &name, func_t<T> func, T data): _name(name), _func(func), _data(data), _tid(0), _isrunning(false){}static void *ThreadRoutine(void *args)//子線程入口,接受參數為當前對象的指針{Thread *t = static_cast<Thread*>(args);//轉義為所需要的指針類型,當前的t和this一樣,但是不能與庫內的this進行重名t->_func(t->_data); //當前對象調用參數_func(他是一個function類創建的對象,這個類可以包裝任何內容,這邊包裝函數,_func是這個函數模板創建的對象),接受來自Thread創建時的第三個參數//到這邊是完成對整個類的包裝,模板概念已經結束,具體操作回到main內查看,對應的函數執行結束后,執行exit(0)exit(0);}bool Start(){int n = pthread_create(&_tid, nullptr, ThreadRoutine, this);//創建線程,加載輸出OS給的tid,默認方式創建(不設置分離狀態,棧大小等),子線程入口,傳入參數給子線程if(n == 0){_isrunning = true;return true;}else{return false;}}bool Join(){if(!_isrunning){return true;}int n = pthread_join(_tid, nullptr);if(n == 0){_isrunning = false;return true;}else{return false;}}~Thread(){}bool IsRunning(){return _isrunning;}private:pthread_t _tid;string _name;func_t<T> _func;T _data;bool _isrunning;
};
main.cc代碼演示
#include "Thread.hpp"
#include <unistd.h>
#include "LockGuard.hpp"class ThreadData
{
public:ThreadData(string name, pthread_mutex_t *pmutex): _name(name), pmutex(pmutex){}~ThreadData(){}public:string _name;pthread_mutex_t *pmutex;
};
int numsize = 10000;string GetThreadName()
{static int num = 1;return static_cast<string>("Thread-" + to_string(num++));
}
void Print(ThreadData *td)//執行Print方法,參數是來自線程創建函數的第四個參數,這一功能也由線程創建函數實現,功不可沒,十分可秒啊
{//全局內定義的參數進行--操作,驗證線程互斥問題while (true){{//將臨界區進行花括號包裹,代碼更加明顯LockGuard lockguard(td->pmutex);//利用鎖保護功能模塊進行加鎖(啟動鎖)// LockGuard lockguard(&mutex);// pthread_mutex_lock(mutex);if (numsize > 0){usleep(1000);std::cout << td->_name << ", the numsize is: " << numsize << std::endl;--numsize;// pthread_mutex_unlock(mutex);}else{// pthread_mutex_unlock(mutex);break;}}//加鎖, 解鎖功能結束,一個線程訪問臨界區的操作也結束,意味著后續線程可以訪問這個臨界區//在運行結果時會發現,有時候會出現一個線程把所有numsize都分完了,這是因為線程執行多久是由于時間片決定,當在多線程情況下把所有任務(同一份資源)都做完的情況叫做多線程饑餓問題}
}int main()
{pthread_mutex_t mutex; // 創建鎖初始化pthread_mutex_init(&mutex, nullptr);string name1 = GetThreadName();//獲取線程名稱ThreadData *td1 = new ThreadData(name1, &mutex); // 將鎖和線程的名字的信息寫入ThreadData,便于管理Thread<ThreadData *> t1(name1, Print, td1);//為線程創建進行加載對應信息string name2 = GetThreadName();ThreadData *td2 = new ThreadData(name2, &mutex);Thread<ThreadData *> t2(name2, Print, td2);string name3 = GetThreadName();ThreadData *td3 = new ThreadData(name3, &mutex);Thread<ThreadData *> t3(name3, Print, td3);string name4 = GetThreadName();ThreadData *td4 = new ThreadData(name4, &mutex);Thread<ThreadData *> t4(name4, Print, td4);string name5 = GetThreadName();ThreadData *td5 = new ThreadData(name5, &mutex);Thread<ThreadData *> t5(name5, Print, td5);t1.Start();//線程啟動t2.Start();t3.Start();t4.Start();t5.Start();t1.Join();//線程等待t2.Join();t3.Join();t4.Join();t5.Join();pthread_mutex_destroy(&mutex); // 消除鎖return 0;
}
基于上篇文章定義對main內的一些修改:
線程饑餓問題
再多線程創建后
在運行結果時會發現,有時候會出現一個線程把所有numsize都分完了,這是因為線程執行多久是由于時間片決定,當在多線程情況下把所有任務(同一份資源)都做完的情況叫做多線程饑餓問題.
要解決饑餓問題要讓線程在執行時,預備一定的順序性–這就是線程同步(下章見曉)
線程加鎖本質
原子性問題在軟硬件層面的體現
軟件方面
線程能被調度是因為OS以一種非常快的方式來受理時鐘中斷,這時就會執行調度進程
硬件方面
把中斷關掉,這時只執行進程,OS不會繼續執行,這時不會進行調度
大部分的體系結構(像X86,AMD芯片中)會提供swap和exchange匯編級的指令,作用是把寄存器的內容和內存單元的內容進行數據交換
1.exchange eax mem_addr //將eax 和 mem_addr的內容進行交換
直接進行交換,這一個操作是原子性的
2.什么是一把鎖?在代碼中是創建一個變量,首先把他想象成一個變量struct {int num = 1;}
利用偽代碼進行理解:
關于加鎖的原則: 誰加鎖,誰解鎖.
可重入和線程安全
可重入VS線程安全:
可重入還是不可重入描述的是函數的問題,跟線程無關,他描述的是函數的特點,無褒貶之分,函數大部分都是不可重入
線程安全,:
多個線程并發同一段代碼時,不會出現不同的結果,常見對全局變量或靜態變量進行操作,并且沒有鎖保護
的情況下,會出現該問題,它描述的是線程的特征
eg:線程訪問不可重入函數是線程不安全的情況之一
線程安全的操作:
對于一個全局的變量,在開始改變完他的值之后在退出這個函數之前將值恢復成開始的值,這樣來變相的達到線程安全的操作,這只是其中一個例子
可重入與線程安全是二義性
函數可重入意味著當線程進入這個函數是線程安全的
反之,當這個函數不可重入,那么就是線程不安全的
死鎖問題
問題解釋: 處于一組進程中的各個線程不會釋放資源,但因為相互申請被其他進程所占據不會釋放資源而處于一種永久等待的狀態(多個執行流在一段時間內因為相互牽制不會向后推進)
死鎖產生的四個必要條件:
互斥條件:一個資源只能被一個執行流使用(產生死鎖的根本原因)
請求與保持條件:一個執行流因請求資源而阻塞時,對已獲得的資源保持不放(把自己的鎖拿的緊緊地,還伸手向別人要鎖)
不剝奪條件:一個執行流已獲得的資源,在未使用之前,不能被強行剝奪(鎖2不能解鎖1)
循環等待條件:若干執行流之間形成一種頭尾相接的循環等待資源關系(互相申請對方的的鎖的問題,形成了申請的循環)
當上述四個條件都成立才會產生死鎖
如何避免死鎖呢?
避免死鎖:不用鎖
但是為了保護共享資源, 提出來的使用鎖
核心原理:
1.破壞4個必要條件中的一個或者多個
2.建議, 按照同樣的次序進行申請鎖的操作(加鎖循序盡量保持一致)
盡量把鎖的資源,按照申請的資源一次給申請線程了,這樣不易出現錯誤(目前用不到)
3.避免鎖未被釋放的場景發生
4.資源一次性分配
注:
一個線程也能實現死鎖:
比如:不下心把解鎖寫成了加鎖,這個時候就會出錯
這個時候是自己阻塞自己