目录
1. 互斥量mutex
2. 互斥量的接口
2.1 初始化互斥量
2.2 销毁互斥量
2.3 互斥量加锁和解锁
2.4 互斥量实现原理探究
3. 可重入VS线程安全
4. 常见锁概念
5. 多线程抢票系统
Linux🌷
【临界资源】:能被多线程共享访问,但每次只能被一个线程访问的资源(打印机);【临界区】:访问临界资源的代码;【互斥】:在任意时刻,只允许一个线程访问临界资源,称为互斥;【同步】:在互斥的基础上各线程对临界资源的一个有序访问;【原子性】: 如果把一个事务看作是一个程序,它要么完整的被执行,要么完全不执行。这种特性就叫原子性。一条汇编语句便是原子的;
大部分情况,各线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。
但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。
多个线程并发的操作共享变量,会带来一些问题。
比如A、B两个线程先后对一个共享变量 int a=10; 进行自增操作,则可能出现以下几种情况:
A:a = 11;B:a = 12;
A:a = 12;B:a = 12;
在此不做全部情况展示,目的是说明问题:
为什么会出现这种情况呢?
因为对 a++;并不是原子操作,它会先将内存中a的值放入CPU中,然后在CPU中进行递增操作,最后将算好的值存回内存中,三个操作如果不是一气呵成的则便会出现错误;
我们通过查看汇编代码也可以看到:a++语句需要三句汇编才能实现(一句汇编才是原子的);
为了解决上述问题,我们引入了互斥量,使得在某一时刻内只允许一个线程访问共享资源;
1. 互斥量mutex
互斥量其实就是一把锁,使得各线程对临界资源的访问都是互斥的;
2. 互斥量的接口
2.1 初始化互斥量
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
参数:
mutex:要初始化的互斥量
attr:NULL
2.2 销毁互斥量
- 使用PTHREAD_ MUTEX_ INITIALIZER初始化的互斥量不需要销毁;
- 不要销毁一个已经加锁的互斥量;
- 已经销毁的互斥量,要确保后面不会有线程再尝试加锁;
int pthread_mutex_destroy(pthread_mutex_t *mutex);
2.3 互斥量加锁和解锁
//加锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
//解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:
成功返回0,失败返回错误号
- 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功;
- 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。
2.4 互斥量实现原理探究
经过上面的例子,大家已经意识到单纯的 i++ 或者 ++i 都不是原子的,可能会有数据一致性问题;锁其实也是一个临界资源,为了实现互斥锁操作,大多数体系结构都提供了 swap 或exchange 指令 ,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了锁的原子性。即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。
3. 可重入VS线程安全
1. 重入和线程安全的概念
- 线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。
- 重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。
- 不保护共享变量的函数;
- 函数状态随着被调用,状态发生变化的函数;
- 返回指向静态变量指针的函数;
- 调用线程不安全函数的函数;
- 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的;
- 类或者接口对于线程来说都是原子操作;
- 多个线程之间的切换不会导致该接口的执行结果存在二义性;
- 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的;
- 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构;
- 可重入函数体内使用了静态的数据结构;
- 不使用全局变量或静态变量;
- 不使用malloc或者new开辟出的空间;
- 不调用不可重入函数;
- 不返回静态或全局数据,所有数据都有函数的调用者提供;
- 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据;
- 函数是可重入的,那就是线程安全的;
- 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题;
- 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的;
- 可重入函数是线程安全函数的一种;
- 线程安全不一定是可重入的,而可重入函数则一定是线程安全的;
- 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的;
4. 常见锁概念
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。
【互斥条件】:一个资源每次只能被一个执行流使用;【请求与保持条件】:一个执行流因请求资源而阻塞时,对已获得的资源保持不放;【不剥夺条件】 : 一个执行流已获得的资源,在末使用完之前,不能强行剥夺;【循环等待条件】 : 若干执行流之间形成一种头尾相接的循环等待资源的关系;
- 破坏死锁的四个必要条件;
- 加锁顺序一致(如有多个锁,加锁和释放锁相对应);
- 避免锁未释放的场景;
- 资源一次性分配;
- 死锁检测算法
- 银行家算法
银行家算法、死锁检测算法实践
5. 多线程抢票系统
- makefile:
tickets:tickets.cc
g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -f tickets
- tickets.cc
#include <iostream>
#include <cstdio>
#include <string>
#include <ctime>
#include <mutex>
#include <cstdlib>
#include <unistd.h>
#include <pthread.h>
//票类
class Ticket
{
private:
//票数
int tickes;
//互斥量
pthread_mutex_t mtx;
public:
//构造函数
Ticket():tickes(1000)
{
pthread_mutex_init(&mtx,nullptr);
}
//抢票函数
bool GetTicket()
{
bool res=true;
//加锁
pthread_mutex_lock(&mtx);
//临界区
if(tickes>0)
{
usleep(1000);
std::cout<<"我是["<<pthread_self()<<"]我要抢的票是:"<<tickes<<std::endl;
tickes--;
}
else
{
std::cout<<"票已经抢空了"<<std::endl;
res=false;
}
pthread_mutex_unlock(&mtx);
return res;
}
//析构函数
~Ticket()
{
pthread_mutex_destroy(&mtx);
}
};
//线程执行的函数
void* ThreadRoutine(void* args)
{
Ticket* t=(Ticket*)args;
while(true)
{
if(!t->GetTicket())
{
break;
}
}
}
//主函数
int main()
{
//创建票对象
Ticket* t = new Ticket();
//创建线程
pthread_t tid[5];
for(int i=0;i<5;i++)
{
pthread_create(tid+i,nullptr,ThreadRoutine,(void*)t);
}
//等待线程
for(int i=0;i<5;i++)
{
pthread_join(tid[i],nullptr);
}
return 0;
}
- 运行结果:
如上便是一个简单的多线程抢票系统;
在结果中我们发现虽然创建了5个线程,但出现了一个线程连续多次抢票的情况:
这是因为这个线程比较活跃,总能申请到锁资源(其实单CPU情况下,也就是在时间片内该线程的运行情况)
如上代码是一个采用静态分配初始化互斥量,我们也可以采用动态分配初始化互斥量:
//票类
class Ticket
{
private:
int tickets;
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
//pthread_mutex_t mtx;
public:
//构造函数
Ticket():tickets(1000)
{
//pthread_mutex_init(&mtx,nullptr);
}
//抢票
bool GetTicket()
{
bool res=true;
//加锁
pthread_mutex_lock(&mtx);
//临界区
if(tickets>0)
{
usleep(1000);
std::cout<<"我是线程["<<pthread_self()<<"],我正在抢"<<tickets<<"号票"<<std::endl;
tickets--;
}
else
{
std::cout<<"票抢完了"<<std::endl;
res=false;
}
pthread_mutex_unlock(&mtx);
return res;
}
//析构函数
~Ticket()
{
//pthread_mutex_destroy(&mtx);
}
};
如果上述文章对您有所帮助的话,还请点赞👍,收藏😉,关注🎈