下面实现一个抢票代码,多线程共同抢票,都访问同一个全局变量tickets,每次访问都 - -tickets:
#include
#include
#include
#include
#include
#include
#include
using namespace std;
int tickets = 1000;
void *getTickets(void *args)
{
(void)args;
while (true)
{
if(tickets > 0)
{
usleep(1000);
printf("%p: %d\n", pthread_self(), tickets);
tickets--;
}
else
{
break;
}
}
}
int main()
{
pthread_t t1, t2, t3;
pthread_create(&t1, nullptr, getTickets, nullptr);
pthread_create(&t2, nullptr, getTickets, nullptr);
pthread_create(&t3, nullptr, getTickets, nullptr);
pthread_join(t1, nullptr);
pthread_join(t2, nullptr);
pthread_join(t3, nullptr);
return 0;
}
运行结果:
最终将tickets的数量减到了-1,但我们发现判断条件是tickets > 0才执行 - -操作;
并发访问的时候,导致了数据不一致的问题;
为了解决多线程引发的数据不一致问题,可以为临界区代码加锁:
在临界区加锁:加锁的意义在于,在一个时刻,只允许一个执行流访问加锁的代码,将这段代码变成串行运行的;
任何一个时刻,只允许一个线程获得这把锁,其他线程都在等待;
直到拿到锁的线程最终释放掉,其他线程才可以拿到;
相当于加锁和解锁之间的代码只可以串行通信,其他代码都可以并行;
全局静态的锁:
//全局静态的锁,使用宏初始化
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;//pthread_mutex_t就是原生线程库提供的一个数据类型
void *getTickets(void *args)
{
(void)args;
while (true)
{
pthread_mutex_lock(&mtx);//为临界区代码加锁
if(tickets > 0)
{
usleep(1000);
printf("%p: %d\n", pthread_self(), tickets);
tickets--;
}
else
{
break;
}
}
}
解锁:
不能在这里解锁,因为如果线程执行完break之后,就不会执行解锁代码,而这把锁是全局的,还处于被该线程修改的状态,其他线程就无法拿到锁了;
应该在下面的地方解锁:
//全局静态的锁,使用宏初始化
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;//pthread_mutex_t就是原生线程库提供的一个数据类型
void *getTickets(void *args)
{
(void)args;
while (true)
{
pthread_mutex_lock(&mtx);//为临界区代码加锁
if(tickets > 0)
{
usleep(1000);
printf("%p: %d\n", pthread_self(), tickets);
tickets--;
pthread_mutex_unlock(&mtx);//解锁
}
else
{
//如果线程加锁后直接运行到这里,在这里也可以解锁
pthread_mutex_unlock(&mtx);//解锁
break;
}
}
}
加锁和解锁之间的代码叫做临界区;
运行:
固定休眠时间可能会导致只有一个线程在跑,可以随即休眠时间;
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
int tickets = 1000;
//全局静态的锁,使用宏初始化
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;//pthread_mutex_t就是原生线程库提供的一个数据类型
void *getTickets(void *args)
{
(void)args;
while (true)
{
pthread_mutex_lock(&mtx);//为临界区代码加锁
if(tickets > 0)
{
usleep(rand()%1500);
printf("%s: %d\n", (char*)args, tickets);
tickets--;
pthread_mutex_unlock(&mtx);//解锁
}
else
{
//如果线程加锁后直接运行到这里,在这里也可以解锁
pthread_mutex_unlock(&mtx);//解锁
break;
}
usleep(rand()%2000);
}
}
int main()
{
srand((unsigned long)time(nullptr) ^ getpid());
pthread_t t1, t2, t3;
pthread_create(&t1, nullptr, getTickets, (void*)"thread one");
pthread_create(&t2, nullptr, getTickets, (void*)"thread two");
pthread_create(&t3, nullptr, getTickets, (void*)"thread three");
pthread_join(t1, nullptr);
pthread_join(t2, nullptr);
pthread_join(t3, nullptr);
return 0;
}
结果:
注:加锁的时候,一定要保证加锁的粒度越小越好,因为加锁会导致进程互斥,造成临界区代码只能串行访问,影响效率;
局部的锁:
第一个参数是锁的地址,第二个是锁的属性;
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
int tickets = 1000;
//全局静态的锁,使用宏初始化
//pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;//pthread_mutex_t就是原生线程库提供的一个数据类型
#define THREAD_NUM 5
class ThreadData
{
public:
ThreadData(const string &n, pthread_mutex_t *pm)
: tname(n),
pmtx(pm)
{}
public:
string tname;//线程名
pthread_mutex_t *pmtx;//锁
};
void *getTickets(void *args)
{
ThreadData* td = (ThreadData*)args;//接收对象
while (true)
{
pthread_mutex_lock(td->pmtx);//为临界区代码加锁
if(tickets > 0)
{
usleep(rand()%1500);
printf("%s: %d\n", td->tname.c_str(), tickets);
tickets--;
pthread_mutex_unlock(td->pmtx);//解锁
}
else
{
//如果线程加锁后直接运行到这里,在这里也可以解锁
pthread_mutex_unlock(td->pmtx);//解锁
break;
}
usleep(rand()%2000);
}
delete td;
return nullptr;
}
int main()
{
pthread_mutex_t mtx;
pthread_mutex_init(&mtx, nullptr);//局部锁初始化
srand((unsigned long)time(nullptr) ^ getpid());
pthread_t t[THREAD_NUM];
//多线程抢票逻辑
for(int i = 0; i < THREAD_NUM; i++)
{
string name = "thread ";
name += to_string(i + 1);
ThreadData *td = new ThreadData(name, &mtx);//创建对象
pthread_create(t + i, nullptr, getTickets, (void*)td);//创建线程的时候,穿的参数也可以是对象指针
}
for(int i = 0; i < THREAD_NUM; i++)
{
pthread_join(t[i], nullptr);
}
pthread_mutex_destroy(&mtx);//局部锁的销毁
return 0;
}
在上面的代码中,创建了一个类保存线程的信息和锁的指针,创建线程的时候,给回调函数传的参数也可以传这个对象,这样就把线程属性和局部锁的指针都传进去了,在回调函数中就可以进行使用;
加锁之后,线程在临界区中是否会切换?
会被切换,但是不会出问题;因为该线程是持有锁被切换的,所以其他抢票线程要执行临界区代码,也必须先申请锁,但是锁已经被该线程申请了,其他线程就无法申请成功,因此,就不会让其他线程进入临界区,保证了临界区中数据的一致性;
一个线程,不申请锁,就是单纯的访问临界资源,这是错误的编码方式;
当一个线程持有锁,在其他线程看来,该线程就是原子的;
所本身就是一种共享资源,那么谁来保证锁的安全呢?
为了保证锁的安全,申请和释放锁,必须是原子的;
exchange或swap汇编指令:
以一条汇编指令的方式,将内存和CPU寄存器的数据进行交换;站在汇编的角度,只有一条汇编语句,就认为该语句的执行是原子的;
在执行流视角,是如何看待COU上面的寄存器的?
CPU内部的寄存器,本质叫做当前执行流的上下文,这些寄存器的空间是被所有执行流共享的,但是寄存器的内容,是被每一个执行流私有的,当执行流切换的时候,会将寄存器内的数据(上下文数据)一并带走;
加锁和解锁的汇编代码:(伪代码)
核心的语句就是下面这句:
将寄存器的内容和锁的内容交换,这是一行汇编代码,是原子的;
多线程申请锁的可能的情况:
内存mutex中的1只能被一个线程交换,如果A线程已经执行了这一条指令,将al寄存器的值(0)和mutex的值(1)交换;
交换完成后,mutex的值就变为了0,相当于锁已经被A线程拿走了,此时线程A被切换了,连带着寄存器al中的值一起带走;
当另一个线程B来的时候,内存mutex中这个1已经被上一个线程交换了;
现在mutex中的值是0,第二个线程交换完后将0交换到了寄存器al中,因此只能等待;
释放锁就是再将线程寄存器al的内容和内存mutex的内容再交换回来;
死锁:是指再一组线程中的各个线程均占有不会释放的资源,但因互相申请被其他进程所占的资源而初一的一种永久等待的状态;
同一个线程反复申请同一把锁,也会造成死锁:
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
int tickets = 1000;
#define THREAD_NUM 5
class ThreadData
{
public:
ThreadData(const string &n, pthread_mutex_t *pm)
: tname(n),
pmtx(pm)
{}
public:
string tname;//线程名
pthread_mutex_t *pmtx;//锁
};
void *getTickets(void *args)
{
ThreadData* td = (ThreadData*)args;//接收对象
while (true)
{
int n = pthread_mutex_lock(td->pmtx);//为临界区代码加锁
assert(n == 0);
if(tickets > 0)
{
usleep(rand()%1500);
printf("%s: %d\n", td->tname.c_str(), tickets);
tickets--;
int n = pthread_mutex_lock(td->pmtx);//听一个进程反复申请同一把锁,也会造成死锁
assert(n == 0);
}
else
{
int n = pthread_mutex_lock(td->pmtx);
assert(n == 0);
break;
}
usleep(rand()%2000);
}
delete td;
return nullptr;
}
int main()
{
pthread_mutex_t mtx;
pthread_mutex_init(&mtx, nullptr);//局部锁初始化
srand((unsigned long)time(nullptr) ^ getpid());
pthread_t t[THREAD_NUM];
//多线程抢票逻辑
for(int i = 0; i < THREAD_NUM; i++)
{
string name = "thread ";
name += to_string(i + 1);
ThreadData *td = new ThreadData(name, &mtx);//创建对象
pthread_create(t + i, nullptr, getTickets, (void*)td);//创建线程的时候,穿的参数也可以是对象指针
}
for(int i = 0; i < THREAD_NUM; i++)
{
pthread_join(t[i], nullptr);
}
pthread_mutex_destroy(&mtx);//局部锁的销毁
return 0;
}
(1)破坏死锁的四个必要条件的其中一个;
(2)加锁顺序一致;
(3)避免锁未释放的场景;
(4)资源一次性分配;