互斥锁
对于一个局部的锁,需要在对应作用域进行pthread_mutex_init()和pthread_mutex_destroy()
才能正常使用而对于一个全局的锁而言,就不要init和destroy,就可以直接使用。
#include <vector>
#include <cassert>
int tickets=1000; //共享资源火车票
#include "Mutex.hpp"
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; //对于全局的锁,必须使用init进行初始化,结束后要destroy
class ThreadData
{
public:
ThreadData(const std::string &threadname, pthread_mutex_t *mutex_p) : threadname_(threadname), mutex_p_(mutex_p)
{
}
~ThreadData() {}
public:
std::string threadname_;
pthread_mutex_t *mutex_p_;
};
void* getTick(void* args)
{
//ThreadData* td = static_cast<ThreadData*>(args);
const string usrname = static_cast<char *>(args);
while(true)
{
// pthread_mutex_lock(td->mutex_p_);
// pthread_mutex_lock(td->mutex_p_); //当对一临界区锁两次之后,会被阻塞
{
MutexGuard mutex(&lock); //这里是局部变量,构造函数上锁,作用域结束后,自动调用析构函数,完成解锁
if(tickets>0)
{
usleep(1234);
//cout<<td->threadname_<<" 正在抢票 "<<tickets<<endl;
cout<<usrname<<" 正在抢票 "<<tickets<<endl;
tickets--;
//pthread_mutex_unlock(td->mutex_p_);
}
else
{
//pthread_mutex_unlock(td->mutex_p_);
break;
}
}
}
}
int main()
{
#define NUM 4
//pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; //对于一个局部变量的锁,必须在该区域内对该所进行init以及destroy
//对于一个局部的锁如何让其他线程看到呢,
//这里封装一个类
//pthread_mutex_init(&lock,nullptr);
vector<pthread_t> tid(NUM);
for(int i=0;i<NUM;i++)
{
char buffer[64];
snprintf(buffer,sizeof buffer,"user%d",i);
// ThreadData* td = new ThreadData(buffer, &lock);
pthread_create(&tid[i],nullptr,getTick,buffer);
}
for(int i=0;i<NUM;++i)
{
pthread_join(tid[i],nullptr);
}
//pthread_mutex_destroy(&lock);
return 0;
}
对于上述四个线程而言,如果线程申请锁成功,进入临界区,访问临界资源期间,其他线程在阻塞等待。对于线程1而言,进入临界区,正在访问临界资源期间,可以被CPU切走,去执行其他任务,但是线程1会带走他的寄存器里面的内容一起被切走,再次切换回来的时候,也是带着自己寄存器的内容回来的,并且被切走的时候,是跟锁一块儿切走的,其他线程依旧无法申请锁成功,也无法向后执行,直到线程1释放这个锁。所以对于其他线程而言,有意义的锁的状态无非两种:1、申请锁前;2、释放锁后。站在其他线程的角度,看待当前线程持有锁的过程是原子的。
在线程(二)中说到锁也是一个资源,锁本身就是一个共享资源(临界资源),全局的变量是要受保护的,锁是用来保护全局资源的,锁本身也是全局资源,锁的安全谁来保护呢?
对于pthread_mutex_lock\pthread_mutex_unlock,加锁的过程必须是安全的,加锁的过程其实是原子的(对资源的访问要么不做,要么一下做完,叫做原子性),所以此时锁的安全就有了保障。对于锁的申请,如果申请成功,就继续向后执行,如果申请暂时没有成功,执行流会阻塞,挂起等待锁。还有另外一种申请锁的方式
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex); //非阻塞的申请锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
谁持有锁,谁就访问临界资源,加锁和解锁的一定是一个执行流(一个线程)。
如何理解加锁和解锁:加锁的过程是原子的,为什么是原子的呢?
在体系结构,汇编中存在指令swap和exchange,该指令的作用是把寄存器和内存单元的数据相互交换,由于只有一条指令,保证了原子性。
加锁的过程:锁本身是个变量,所以加锁时只需要将锁这个变量通过一条汇编指令exchange和swap将他交换到执行流的上下文,此时锁属于线程私有,除非我归还锁,其余线程才能拿到这个锁执行临界区的代码,访问临界资源。
解锁的过程就是:move把锁移到mutex区域,就是归还锁。
加锁和解锁之间的代码是临界区,并且对于加锁而言,要加锁,所有的访问这临界区的线程都必须加锁,不可以部分加,部分不加。
可重入函数和不可重入函数描述的是一个函数是否能够被一堆线程安全的调用,出了问题就是不可重入。对于一个代码片段,一堆线程去执行,有没有引起数据不安全问题,如果有就是不安全的。
死锁:一组执行流持有自己的锁资源,还去申请对方的锁,多执行流互相等待对方的锁,造成永久等待的情况就是死锁。
死锁的四个必要条件:
1、互斥条件:一个资源每次只能被一个执行流所使用
2、请求与保持:一个执行流因请求资源而阻塞时,对已经获得的资源保持不放;
3、不剥夺套件:一个执行流已经获得的资源,在未使用完之前,不能强行剥夺;
4、循环等待条件:若干执行流至今啊形成一种头尾相接的循坏等待资源的关系。
在写代码的时候,尽量少用锁。