Linux操作系统——多线程互斥

目录

一、前言

二、线程互斥

三、多线程访问临界资源所导致的问题

四、Mutex互斥量 

1、锁的接口及其使用

定义一个锁(造锁)

初始化锁(改锁)

摧毁锁

上锁

解锁

锁的使用

五、锁的宏初始化 

六、锁的原理 

七、C++封装互斥锁

八、可重入与线程安全

常见的线程不安全的情况

常见的线程安全的情况

常见可重入的情况

可重入与线程安全联系

可重入与线程安全区别

九、死锁

死锁产生的必要条件

死锁的避免方法


一、前言

       我们已经学习了什么是多线程,以及多线程的控制和其优点,多线程可以提高程序的并发性和运行效率,可以充分利用计算机的多核资源。但是我们在前面学习的过程中,看到了有些多线程的程序的运行结果是有一些问题的,如出现了输出混乱、访问共享资源混乱等特点。所以我们下面提出的这个概念是关于这方面的——线程互斥

二、线程互斥

在正式认识线程互斥之前,我们先来介绍几个概念:

  • 临界资源:指的是不同的执行流都可以看到的同一份资源,就叫做临界资源。
  • 临界区:访问临界资源的代码段,就称为临界区。
  • 原子性:对于一个操作,如果只存在两种状态,已完成的和未完成的,却没有中间状态,则称这个操作是具有原子性的。

下面我们用这段代码来具体解释一下这几个概念

#include<iostream>
#include<unistd.h>
#include<pthread.h>
using std::endl;
using std::cout;
int tickets=10000;
void* Buyticket(void* argc)
{const char* name=static_cast<const char*>(argc);int cnt=10;while(cnt--){if(tickets>0){usleep(100000);printf("%s:%lu There are %d tickets!\n",name,pthread_self(),tickets);usleep(10000);}else{printf("There is no ticket!\n");break;}}return nullptr;
}
int main()
{pthread_t tid1,tid2,tid3,tid4;pthread_create(&tid1,nullptr,Buyticket,(void*)"Thread 1");pthread_create(&tid2,nullptr,Buyticket,(void*)"Thread 2");pthread_create(&tid3,nullptr,Buyticket,(void*)"Thread 3");pthread_create(&tid4,nullptr,Buyticket,(void*)"Thread 4");pthread_join(tid1,nullptr);pthread_join(tid2,nullptr);pthread_join(tid3,nullptr);pthread_join(tid4,nullptr);return 0;
}

  • tickets 这个变量可以被所有的进程看到,所以该变量为临界资源。
  •  所有的线程都不是直接访问该临界资源的,而是通过调用Buytickets()回调函数来访问的,但是整个回调函数并不能被称为临界区,只有访问了临界资源的那一部分的代码才能称为临界区,即
     if(tickets>0){usleep(100000);printf("%s:%lu There are %d tickets!\n",name,pthread_self(),tickets);usleep(10000);}

三、多线程访问临界资源所导致的问题

上面类似抢票的代码只是对临界资源进行了访问并没有修改临界资源,接下来我们对临界资源做修改,在回调函数内循环抢票,直至票数为0,下面我们看结果

#include<iostream>
#include<unistd.h>
#include<pthread.h>
using std::endl;
using std::cout;
int tickets=10000;
void* Buyticket(void* argc)
{const char* name=static_cast<const char*>(argc);while(true){if(tickets>0){usleep(100);printf("%s:%lu Get ticket:%d!\n",name,pthread_self(),tickets--);usleep(100);}else{printf("%s::%lu There is no ticket!\n",name,pthread_self());break;}}return nullptr;
}
int main()
{pthread_t tid1,tid2,tid3,tid4;pthread_create(&tid1,nullptr,Buyticket,(void*)"Thread 1");pthread_create(&tid2,nullptr,Buyticket,(void*)"Thread 2");pthread_create(&tid3,nullptr,Buyticket,(void*)"Thread 3");pthread_create(&tid4,nullptr,Buyticket,(void*)"Thread 4");pthread_join(tid1,nullptr);cout<<"Main thread has joined thread 1!"<<endl;pthread_join(tid2,nullptr);cout<<"Main thread has joined thread 2!"<<endl;pthread_join(tid3,nullptr);cout<<"Main thread has joined thread 3!"<<endl;pthread_join(tid4,nullptr);cout<<"Main thread has joined thread 4!"<<endl;return 0;
}

我们看到运行结果发现:明明我们在程序中设置的条件是只有 tickets>0 的时候才会进入临界区抢票,但是结果中怎么出现了2号线程抢到了0号票,1号线程竟然还抢到了一号票的情况呢?接下来我们就详细分析以下该过程。


事实上,在临界区中有两个地方涉及到对临界资源的访问,一个是判断tickets是否大于0,一个是对tickets--,造成线程抢到负票,是因为线程对 tickets>0 的计算出现了一些错误。

代码中凡是关于算数计算的问题,实际上都是交给CPU进行执行的,这里面包括了加减乘除、逻辑运算、逻辑判断,最终CPU都是通过 位移运算 加法运算 来解决的。

 下面看看这个错误是如何发生的:

首先进行判断操作 tickets>0 ,CPU对该部分的操作会分成三步来执行

  1. CPU读取判断并放入内存中
  2. CPU执行数据判断
  3. CPU将结果返回到代码中

当 tickets为1时,且线程1进行判断时,正常情况是这样的:

CPU 进行逻辑判断, 其实是通过 判断式子, 计算出一个真值或假值, 进而返回到 判断语句中.

例如, 此例中 tickets = 1, 判断  tickets>0 ,CPU 就可能 计算  1+ -0  的结果, 然后将结果返回到 判断语句中。

此时判断结果为真,开始执行抢票语句,线程1执行 tickets-- 操作。但是上面我们所说的都是理想的情况下,在多线程的环境下还可能会出现其他的情况:

如果还是在 tickets=1 的时候,线程1需要判断,我们知道执行判断条件是需要花时间的,如果在CPU计算完成之后,此时还没有来得及将计算结果返回给线程1的代码的时候,却需要调度线程2了,然后操作系统会将CPU的计算结果保存到线程1的上下文数据中,将线程1从CPU上拿下来暂停运行,将线程2放在CPU上开始执行

然后线程2开始执行判断,但是线程1执行完后 tickets=0 的结果并没有返回给代码,所以此时的tickets仍然为1,然后像线程1一样,CPU根据 tickets为1进行计算,计算完成之后,还没有将结果返回给线程2的代码中时,线程3又要被调度了。接着CPU将线程2的运行结果放在线程2的上下文数据中,线程2暂停运行, 

紧接着线程3执行判断,类似上面的情况,现在的tickets仍然为1,计算完成之后,正常将结果返回给了线程3的代码中,结果为真,执行tickets-- 操作之后变成0,线程3抢到了编号为1的票。 

接着线程1被调度,操作系统恢复其上下文数据,其中就包含了上次计算出的结果,所以就将这个结果(上次CPU计算逻辑判断结果)返回到代码中,由于该结果是按照tickets为1计算的,所以结果为真,执行 tickets-- 操作,tickets变为-1,线程1就抢到了编号为0的票

类似这种方式,线程2又被调度, 操作系统恢复线程2的上下文数据, 将结果返回到代码中, 结果也是由 tickets 为1 计算的, 所以结果为真. 线程2也会执行抢票操作,tickets变为-2,线程2抢到了编号为-1的票。

上面就是该程序出错的原因,其主要原因是在判断 tickets>0 由于会调用其他线程出的错误,如果该错误发生在 tickets-- 操作上,则有可能对票的数量修改产生混乱。

造成这种结果的原因是什么呢?

  1. 我们对临界资源的访问和修改都不是原子的,这两个操作都会存在中间态,即CPU在计算的过程中需要读取、计算、返回等多个操作,一旦CPU执行某个线程处在某个中间状态的时候暂停了,其他线程可能会“趁虚而入”。
  2. 存在多个线程同时访问临界资源的情况。

四、Mutex互斥量 

     了解了程序出现问题的原因,下来我们就讨论如何解决它:我们先从防止多个线程同时访问临界资源开始

  1. 代码必须要有互斥行为:当一个线程进入临界资源中执行时,其他线程不能进入
  2. 如果多个线程同时要执行临界区的代码,并且临界区内没有线程在执行,那么执行允许一个线程进入该临界区。
  3. 如果某个线程不在临界区内执行,那么该线程不能阻止其他线程进入临界区。

要满足这三个条件,我们要引出一个关键工具——锁,通过给临界区上一把锁,从而阻止其他线程进入临界区,这种锁被称为互斥锁,给予代码互斥的效果。

就像现实生活中的锁一样,如果我们不想让很多人同时进入一个房间,那么我们就给该房间带上锁,当一个人进去后就将门锁起来防止其他人进去,等到那个人干完自己的事情出来之后,我们再解锁,接着下一个人再进入,这样就达到了互斥的效果。

1、锁的接口及其使用

pthread 库为我们提供了 “造锁/买锁”、“改锁”、“上锁”、“解锁”、“毁锁/卖锁” 的接口:

定义一个锁(造锁)

pthread_mutex_t mutex 是一个类型,可以来定义一个互斥锁。就像定义一个变量一样使用它定义互斥锁的时候,锁名可以随便设置。此锁,即为 互斥量。互斥锁的类型 pthread_mutex_t 是一个联合体。

初始化锁(改锁)

pthread_mutex_init() 是pthread库提供的一个初始化锁的一个接口,第一个参数传入的就是需要初始化的锁的地址。 第二个参数需要传入锁初始化的属性,在接下来的使用中暂时不考虑。成功返回0,否则返回错误码。

摧毁锁

pthread_mutex_destroy 是用来摧毁定义的锁,参数需要传入的是需要摧毁的锁的指针。成功则返回0,否则返回错误码。

上锁

pthread_mutex_lock() ,阻塞式上锁,即 线程执行此接口,指定的锁已经被锁上了,那么线程就进入阻塞状态,直到解锁之后,此线程再上锁。

pthread_mutex_trylock(),非阻塞式上锁,即 线程执行此接口,尝试上锁,如果指定的锁已经被锁上,那么线程就先不上锁,先去执行其他代码。

上锁的接口即为抢锁的接口,哪个线程可以抢到,哪个线程就能进入被锁上的区域。这两个接口,一般用于进入临界区之前。当上锁成功,则返回0,否则返回一个错误码。

解锁

pthread_mutex_unlock() ,作用是解锁接口,一般用于出临界区的时候。当解锁成功,返回0,否则返回一个错误码。


锁的使用

下面我们来使用锁

#include<iostream>
#include<unistd.h>
#include<pthread.h>
using std::endl;
using std::cout;
int tickets=10000;
pthread_mutex_t mutex;
void* Buyticket(void* argc)
{const char* name=static_cast<const char*>(argc);while(true){pthread_mutex_lock(&mutex);if(tickets>0){usleep(100);printf("%s:%lu Get ticket:%d!\n",name,pthread_self(),tickets--);usleep(100);pthread_mutex_unlock(&mutex);}else{printf("%s::%lu There is no ticket!\n",name,pthread_self());break;}}return nullptr;
}
int main()
{pthread_mutex_init(&mutex,nullptr);pthread_t tid1,tid2,tid3,tid4;pthread_create(&tid1,nullptr,Buyticket,(void*)"Thread 1");pthread_create(&tid2,nullptr,Buyticket,(void*)"Thread 2");pthread_create(&tid3,nullptr,Buyticket,(void*)"Thread 3");pthread_create(&tid4,nullptr,Buyticket,(void*)"Thread 4");pthread_join(tid1,nullptr);cout<<"Main thread has joined thread 1!"<<endl;pthread_join(tid2,nullptr);cout<<"Main thread has joined thread 2!"<<endl;pthread_join(tid3,nullptr);cout<<"Main thread has joined thread 3!"<<endl;pthread_join(tid4,nullptr);cout<<"Main thread has joined thread 4!"<<endl;return 0;
}

 可以看到加了锁之后的程序已经能正常的进行抢票了,但是在抢票结束之后程序竟然还没有退出,而是卡住了,需要我们手动ctrl+c来结束进程。这是为什么呢?事实上我们这里遗漏了一个点,即进程在进入临界区之前进行上锁操作,接下来判断tickets是否大于0,如果大于0,就执行抢票操作(tickets--),然后解锁,接着再次进入临界区执行相同的操作,但是如果此时tickets已经变成0了呢?这时候进程就要执行else后面的代码,接着break出去,但是我们是不是忘记了上锁之后,还没有解锁,而我们的pthread_mutex_lock()是阻塞式上锁的,如果执行的时候,所已经被锁上了,那就会阻塞式等待,线程就会暂停运行,上锁的线程已经退出,且没有解锁,其他线程会因为阻塞式等待一直暂停运行,所以程序会卡住。所以我们还需要在else语句后面加一个解锁操作。

可以看到修改程序后的运行结果就没有了程序卡住的情况。

上述的现象是一种 死锁 现象,是指在多线程运行时,每个线程都在等待其他的线程释放资源,从而导致所有的线程都无法执行的一种状态。


接着我们观察运行结果,发现运行结果好像并不符合多线程运行的特征,反而像是排着队执行的,就像是线程1先进行几百次的抢票,然后线程2再接着执行几百次的抢票,再接着其他线程执行执行,而不是“交叉执行“的,如线程1抢一次,接着线程4抢一次,以此类推线程无规则地混合着执行抢票。这又是什么原因呢?

        这是因为在一个线程加锁进入临界区后,其他的线程就会进入阻塞状态,在该线程的时间片内,该线程会一直进出临近区,会一直执行加锁、解锁的操作,虽然此线程会在出临界区的时候解锁,但是它又会立马进入下一个循环,再次上锁。其他的线程想要申请到锁的话,是需要先被CPU调度的,线程的调度的消耗比上锁和解锁的消耗大得多,所以线程调度并没有上锁和解锁快,所以在一个线程的时间片内其他的线程是很难申请到锁资源的。

接下来我们让一个线程解锁之后等一会,不让它立马进入下一个循环,让它在它的时间片时间段里多等一会。看看结果 

 while(true){pthread_mutex_lock(&mutex);if(tickets>0){printf("%s:%lu Get ticket:%d!\n",name,pthread_self(),tickets--);pthread_mutex_unlock(&mutex);usleep(10);}else{printf("%s::%lu There is no ticket!\n",name,pthread_self());pthread_mutex_unlock(&mutex);break;}}

可以看到运行结果如同我们预期。


五、锁的宏初始化 

 在上面我们已经简单学习了锁的使用,关于锁的初始化上面用到的是pthread库提供的接口:pthread_mutex_init() ,但是在系统中还存在另一种初始化锁的方法,还方法只针对全局锁进行初始化,使用该宏初始化的锁是不需要手动销毁的,即不需要我们调用 pthread_mutex_destroy() 接口

下面演示该宏定义的全局锁的使用:

#include<iostream>
#include<unistd.h>
#include<pthread.h>
using std::endl;
using std::cout;
int tickets=10000;
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
void* Buyticket(void* argc)
{const char* name=static_cast<const char*>(argc);while(true){pthread_mutex_lock(&mutex);if(tickets>0){printf("%s:%lu Get ticket:%d!\n",name,pthread_self(),tickets--);pthread_mutex_unlock(&mutex);usleep(10);}else{printf("%s::%lu There is no ticket!\n",name,pthread_self());pthread_mutex_unlock(&mutex);break;}}return nullptr;
}
int main()
{pthread_t tid1,tid2,tid3,tid4;pthread_create(&tid1,nullptr,Buyticket,(void*)"Thread 1");pthread_create(&tid2,nullptr,Buyticket,(void*)"Thread 2");pthread_create(&tid3,nullptr,Buyticket,(void*)"Thread 3");pthread_create(&tid4,nullptr,Buyticket,(void*)"Thread 4");pthread_join(tid1,nullptr);cout<<"Main thread has joined thread 1!"<<endl;pthread_join(tid2,nullptr);cout<<"Main thread has joined thread 2!"<<endl;pthread_join(tid3,nullptr);cout<<"Main thread has joined thread 3!"<<endl;pthread_join(tid4,nullptr);cout<<"Main thread has joined thread 4!"<<endl;return 0;
}

运行结果也没有问题。

那么如果我们此时使用static关键字修饰锁,将该锁在主线程内定义并初始化,合不合理呢?

 在主线程中定义一个由 static 修饰的锁的话,其实线程执行的函数是看不到的。如果我们想让它在线程执行的函数中被看到需要怎么操作呢?我们可以将主线程中定义的锁通过pthread_mutex_create() 该接口的第四个参数传过去。

#include<iostream>
#include<unistd.h>
#include<pthread.h>
using std::endl;
using std::cout;
int tickets=10000;
void* Buyticket(void* argc)
{pthread_mutex_t* mutex=(pthread_mutex_t*)argc;while(true){pthread_mutex_lock(mutex);if(tickets>0){printf("thread:%lu Get ticket:%d!\n",pthread_self(),tickets--);pthread_mutex_unlock(mutex);usleep(10);}else{printf("thread::%lu There is no ticket!\n",pthread_self());pthread_mutex_unlock(mutex);break;}}return nullptr;
}
int main()
{static pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;pthread_t tid1,tid2,tid3,tid4;pthread_create(&tid1,nullptr,Buyticket,(void*)&mutex);pthread_create(&tid2,nullptr,Buyticket,(void*)&mutex);pthread_create(&tid3,nullptr,Buyticket,(void*)&mutex);pthread_create(&tid4,nullptr,Buyticket,(void*)&mutex);pthread_join(tid1,nullptr);cout<<"Main thread has joined thread 1!"<<endl;pthread_join(tid2,nullptr);cout<<"Main thread has joined thread 2!"<<endl;pthread_join(tid3,nullptr);cout<<"Main thread has joined thread 3!"<<endl;pthread_join(tid4,nullptr);cout<<"Main thread has joined thread 4!"<<endl;return 0;
}


现在可以在主线程内利用宏初始化锁了,但是我们使用了pthread_mutex_create()的第四个参数传锁的地址,此时我们打印出来的结果又没有了线程的名字,如果我们两个都想要呢?

C语言的话我们可以使用结构体,C++的话我们可以使用类,通过第四个参数将创建出的类或者结构体对象传过去,通过调用结构体或者类内成员实现我们想做的。

下面我们用结构体实现一下:

#include<iostream>
#include<unistd.h>
#include<cstring>
#include<pthread.h>
using std::endl;
using std::cout;
int tickets=10000;
typedef struct MutexInfo{char _name[64];pthread_mutex_t* _mutex;
}MutexInfo;
void* Buyticket(void* argc)
{MutexInfo* mutex=(MutexInfo*)argc;while(true){pthread_mutex_lock(mutex->_mutex);if(tickets>0){printf("%s:%lu Get ticket:%d!\n",mutex->_name,pthread_self(),tickets--);pthread_mutex_unlock(mutex->_mutex);usleep(10);}else{printf("%s::%lu There is no ticket!\n",mutex->_name,pthread_self());pthread_mutex_unlock(mutex->_mutex);break;}}return nullptr;
}
int main()
{static pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;;pthread_t tid1,tid2,tid3,tid4;MutexInfo* m1=new MutexInfo();MutexInfo* m2=new MutexInfo();MutexInfo* m3=new MutexInfo();MutexInfo* m4=new MutexInfo();m1->_mutex=&mutex;m2->_mutex=&mutex;m3->_mutex=&mutex;m4->_mutex=&mutex;strcpy(m1->_name,"thread 1");strcpy(m2->_name,"thread 2");strcpy(m3->_name,"thread 3");strcpy(m4->_name,"thread 4");pthread_create(&tid1,nullptr,Buyticket,(void*)m1);pthread_create(&tid2,nullptr,Buyticket,(void*)m2);pthread_create(&tid3,nullptr,Buyticket,(void*)m3);pthread_create(&tid4,nullptr,Buyticket,(void*)m4);pthread_join(tid1,nullptr);cout<<"Main thread has joined thread 1!"<<endl;pthread_join(tid2,nullptr);cout<<"Main thread has joined thread 2!"<<endl;pthread_join(tid3,nullptr);cout<<"Main thread has joined thread 3!"<<endl;pthread_join(tid4,nullptr);cout<<"Main thread has joined thread 4!"<<endl;return 0;
}

六、锁的原理 

前边说了这么多有关于锁的介绍,那么锁究竟是什么呢?我们说只要对临界区上了锁,当一个线程进入临界区后其他线程就不允许进入临界区了,为什么上了锁之后其他线程就不能进入临界区了呢?假设这么一种情况,当一个线程拿到锁之后对临界区上锁,在还没有出临界区的时候,此时CPU要调度其他线程了,该线程会被切走吗?此时会出现线程安全问题吗?

        对于第一个问题我们在后面讲解锁的原理的时候再做解释。先来简单回答一下第二个问题:线程是会被切走的,但是其他线程仍旧不会进入临界区,因为锁是一个可以被其他所有线程共同看到的、唯一的一个变量,一旦有线程申请到锁,即使它被CPU切走,那也相当于是”带着“锁走的,其他线程拿不到即申请不到该锁就无法进入临界区。因为可以被所有线程看到,所以锁实际上也是一个”临界资源“,多个线程不断尝试申请锁,也是一种对未保护的临界资源的访问,这里为什么不出现错误呢?因为互斥锁的上锁、解锁操作已经被设计成了原子的


原理:

互斥锁本身是一个结构体的类型数据,这我们也知道,我们只需要关注它的成员变量。对于上锁、解锁这两个原子操作我们深入学习一下。

1、这两个操作是怎么设计成原子操作的呢?

首先我们需要知道:为了实现互斥性, 大多数的体系结构都提供有 swapexchange 等指令. 此指令的作用是, 直接将寄存器中的数据与内存中的数据做交换. 只有一条指令, 此指令是原子的.

下面我们看看上锁和解锁的伪代码

// lock 伪代码 al表示寄存器 mutex表示锁
movb $0, %al  //把0存入al寄存器中
xchgb %al, mutex  //交换al寄存器和内存中的mutex的数据,相当于上锁的操作
if(al > 0) {      //如果al>0,则申请锁成功,返回0return 0;
}
else 阻塞等待;      //否则阻塞等待
goto lock;

如图所示线程1在执行上锁的操作

如果 没有上锁时, 锁的值是1.

那么 执行 xchgb %al, mutex 将 al 中的0 与 mutex 的值交换, 其实就是 将 锁给了执行此语句的线程

为什么这样说呢.

首先, 线程的属性中是有维护寄存器的数据的, 即 线程的上下文数据.

也就是说 al寄存器中的数据, 其实就是 线程的上下文数据. 如果没有解锁, 那么 此线程从CPU 切走时, 会将 寄存器数据(上下文数据)维护好, 一起切走. 而此时寄存器中的数据可能没有被清除, 因为可能只是将寄存器数据存储到线程上下文数据中

那么可能就有人觉得会有问题:既然寄存器中数据还有, 那么下一个线程被调度的时候, 不会直接读取寄存器中的数据吗?

不会, 新的线程被调度的时候, 首先要做的就是将线程自己的有关寄存器上下文数据恢复到寄存器中. 也就是说, 当新线程被调度之后, CPU寄存器中会变为此线程的上下文数据.

所以, 寄存器只是一个工具, 每个线程被调度时, 都会将自己上次维护的上下文数据恢复到寄存器中.

那么, 就可以说如果在mutex为1时(即锁没有被申请), 执行 xchgb %al, mutex 就是将锁给了执行此语句的线程.

如果锁被带走了, 就意味着内存中 mutex 的值为0. 那么, 如果其他线程被调度, 再次执行上面的伪代码的类似逻辑, 那么 此线程的al上下文数据就会是0. 就代表着申请锁失败.

那么也就是说, 将内存中的数据换入寄存器中, 本质上就是 将内存中的数据 从共享状态 变为了线程私有. 因为 换入寄存器的数据, 基本当前线程的上下文数据

七、C++封装互斥锁

 我们使用C++的类来封装一个锁并使用

#pragma once
#include<iostream>
#include<pthread.h>
using std::cout;
using std::endl;class myMutex{public:myMutex(){pthread_mutex_init(&_lock,nullptr);}void lock(){pthread_mutex_lock(&_lock);}void unlock(){pthread_mutex_unlock(&_lock);}~myMutex(){pthread_mutex_destroy(&_lock);}private:pthread_mutex_t _lock;
};class LockGuard//为了更加简单的上锁和解锁
{public:LockGuard(myMutex* myMutex):_myMutex(myMutex){_myMutex->lock();cout<<"Locked!"<<endl;}~LockGuard(){_myMutex->unlock();cout<<"Unlocked!"<<endl;}private:myMutex* _myMutex;
};
#include<iostream>
#include<unistd.h>
#include<pthread.h>
#include"threadlock.hpp"int tickets=10000;
myMutex mymutex;
bool grabTickets()
{bool ret=false;LockGuard guard(&mymutex);//后面没有手动解锁,因为创建出来的对象生命周期到的时候会自动调用析构函数,而该类的析构函数就是解锁。if(tickets>0){printf("%lu Get ticket:%d!\n",pthread_self(),tickets--);ret=true;usleep(100);}return ret;
}
void* Buyticket(void* argc)
{const char* name=static_cast<const char*>(argc);while(true){if(!grabTickets()){break;}printf("%s:Grab tickets success!\n",name);sleep(1);}return nullptr;
}
int main()
{pthread_t tid1,tid2,tid3,tid4;pthread_create(&tid1,nullptr,Buyticket,(void*)"Thread 1");pthread_create(&tid2,nullptr,Buyticket,(void*)"Thread 2");pthread_create(&tid3,nullptr,Buyticket,(void*)"Thread 3");pthread_create(&tid4,nullptr,Buyticket,(void*)"Thread 4");pthread_join(tid1,nullptr);cout<<"Main thread has joined thread 1!"<<endl;pthread_join(tid2,nullptr);cout<<"Main thread has joined thread 2!"<<endl;pthread_join(tid3,nullptr);cout<<"Main thread has joined thread 3!"<<endl;pthread_join(tid4,nullptr);cout<<"Main thread has joined thread 4!"<<endl;return 0;
}

 

可以看到运行结果没有问题。 

上面的封装我们使用了一些C++语言的特性,还可以如下这样使用

一个类的生命周期是在其所在的控制块内

#include <iostream>
#include "threadLock.hpp"int cnt = 0;
myMutex mymutex;void* startRoutine(void* args) {// 如果我们需要统计线程执行此函数了多少次, 我们只需要使用下面这段代码块{lockGuard myLock(&mymutex);cnt++;}// 这也是一个控制块, myLock对象 出此控制块会自动调用析构函数, 即出此控制块会自动解锁.// …… 其他代码
}

八、可重入与线程安全

  1. 线程安全:多线程并发运行同一段代码时,单一线程并不会影响到其他线程或者整个进程的运行结果,就成为线程安全。
  2. 可重入:同一个函数被不同执行流调用, 在一个执行流执行没结束时, 有其他执行流再次执行此函数, 这个现象叫 重入。

常见的线程不安全的情况
  • 不保护共享变量的函数

  • 函数状态随着被调用 会发生变化的函数

    比如, 我们在函数内部定义了一个静态变量, 然后不加锁的++, 用来统计线程调用此函数的次数

  • 返回指向静态变量指针的函数

  • 调用线程不安全函数的函数

常见的线程安全的情况
  • 每个线程对全局变量或者静态变量 只有读取权限,没有写入权限,一般来说这些线程是安全的
  • 类或接口对于线程来说都是原子操作
  • 多个线程之间的切换不会导致该接口的执行的结果存在二义性
常见可重入的情况
  • 不使用全局变量或静态变量
  • 不使用用malloc或者new开辟出的空间
  • 不调用不可重入函数
  • 不返回静态或全局数据,所有数据都有函数的调用者提供
  • 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
可重入与线程安全联系
  • 函数是可重入的,那就是线程安全的
  • 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
  • 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的
可重入与线程安全区别
  • 重入函数是线程安全函数的一种
  • 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
  • 如果将对临界资源的访问加上锁,则这个函数是线程安全的. 但如果这个重入函数 若锁还未释放则可能会产生死锁,因此是不可重入的

九、死锁

我们之前也提到过死锁,即在一组进程、线程中的各个进程、线程 均占有不会释放的资源,但是因相互申请被其他进程、线程所占用的不会释放的资源而处于一种永久等待的状态。

我们来举个例子:

#include <iostream>
#include <unistd.h>
#include <pthread.h>int cnt = 0;
pthread_mutex_t mutexA = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutexB = PTHREAD_MUTEX_INITIALIZER;void* startRoutineA(void* args) {while (true) {pthread_mutex_lock(&mutexA);sleep(1);pthread_mutex_lock(&mutexB);cnt++;printf("cnt: %d", cnt);pthread_mutex_unlock(&mutexA);pthread_mutex_unlock(&mutexB);}return nullptr;
}void* startRoutineB(void* args) {while (true) {pthread_mutex_lock(&mutexB);sleep(1);pthread_mutex_lock(&mutexA);cnt++;printf("cnt: %d", cnt);pthread_mutex_unlock(&mutexB);pthread_mutex_unlock(&mutexA);}return nullptr;
}int main() {pthread_mutex_init(&mutexA, nullptr);pthread_mutex_init(&mutexB, nullptr);pthread_t tidA, tidB;pthread_create(&tidA, nullptr, startRoutineA, (void*)"threadA");pthread_create(&tidB, nullptr, startRoutineB, (void*)"threadB");pthread_join(tidA, nullptr);pthread_join(tidB, nullptr);return 0;
}

 看到运行之后程序会卡住,这是因为  线程A, 先申请 锁A, 等1s, 再申请 锁B.线程B, 先申请 锁B, 等1s, 再申请 锁A.而 线程A和B 几乎是同时运行代码的. 也就是说 线程A 申请到锁A 和 线程B 申请到 锁B,几乎是同时的.线程A 拿着锁A, 在申请锁B线程B 拿着锁B, 在申请锁A. 他俩都申请不到直接造成死锁.

死锁产生的必要条件
  1. 互斥条件:: 一个资源每次只能被一个执行流使用
  2. 请求与保持条件: 一个执行流因请求资源而阻塞时,对已获得的资源保持不放
  3. 不剥夺条件: 一个执行流已获得的资源,在末使用完之前,不能强行剥夺
  4. 循环等待条件: 若干执行流之间形成一种头尾相接的循环等待资源的关系
死锁的避免方法


最直接有效的避免方法是 不使用锁. 虽然锁可以解决一些多线程的问题, 但是可能会造成死锁, 而且 上锁和解锁的过程是需要消耗资源的. 如果不停的上锁和解锁, 一定会托慢进程的运行速度.

所以, 在需要使用锁的场景, 最好先不要考虑如何设置锁, 可以先考虑一下是否可以不用锁

如果非要使用锁, 那就得考虑避免死锁:

  1. 破坏死锁的四个必要条件
  2. 加锁顺序一致
  3. 避免锁未释放的场景
  4. 资源一次性分配

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/66518.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

conda指定路径安装虚拟python环境

DataBall 助力快速掌握数据集的信息和使用方式&#xff0c;会员享有 百种数据集&#xff0c;持续增加中。 需要更多数据资源和技术解决方案&#xff0c;知识星球&#xff1a; “DataBall - X 数据球(free)” -------------------------------------------------------------…

鸿蒙 ArkUI实现地图找房效果

常用的地图找房功能&#xff0c;是在地图上添加区域、商圈、房源等一些自定义 marker&#xff0c;然后配上自己应用的一些筛选逻辑构成&#xff0c;在这里使用鸿蒙 ArkUI 简单实现下怎么添加区域/商圈、房源等 Marker. 1、开启地图服务 在华为开发者官网&#xff0c;注册应用&…

Kubernetes开发环境minikube | 开发部署apache tomcat web单节点应用

minikube是一个主要用于开发与测试Kubernetes应用的运行环境 本文主要描述在minikube运行环境中部署J2EE tomcat web应用 minikube start --force minikube status 如上所示&#xff0c;在Linux中启动minikube运行环境 service docker start docker version service docker …

Kafka为什么要放弃Zookeeper

1.Kafka简介 Apache Kafka最早是由Linkedin公司开发&#xff0c;后来捐献给了Apack基金会。 Kafka被官方定义为分布式流式处理平台&#xff0c;因为具备高吞吐、可持久化、可水平扩展等特性而被广泛使用。目前Kafka具体如下功能&#xff1a; 消息队列,Kafka具有系统解耦、流…

KUKA机器人如何修改程序并下载到机器人控制器中?

KUKA机器人如何修改程序并下载到机器人控制器中? 如下图所示,首先将使用的网卡的IP地址设置为自动获得, 打开workvisual软件,点击搜索,正常情况下可以搜索到项目文件,选中后双击进入, 如下图所示,此时,workvisual会自动从机器人控制器中下载项目文件到电脑上,耐心等待…

51单片机——8*8LED点阵

LED 点阵的行则为发光二极管的阳极&#xff0c;LED 点阵的列则为发光二极管的阴极 根据 LED 发光二极管导通原理&#xff0c;当阳极为高电平&#xff0c;阴极为低电平则点亮&#xff0c;否则熄灭。 因此通过单片机P0口可控制点阵列&#xff0c;74HC595可控制点阵行 11 脚 SR…

《Rust权威指南》学习笔记(三)

泛型和trait 1.泛型可以提高代码的复用能力&#xff0c;泛型是具体类型或其他属性的抽象代替&#xff0c;可以看成是一种模版&#xff0c;一个占位符&#xff0c;编译器在编译时会将这些占位符替换成具体的类型&#xff0c;这个过程叫做“单态化”&#xff0c;所以使用泛型的…

CentOS: RPM安装、YUM安装、编译安装(详细解释+实例分析!!!)

目录 1.什么是RPM 1.1 RPM软件包命名格式 1.2RPM功能 1.3查询已安装的软件&#xff1a;rpm -q 查询已安装软件的信息 1.4 挂载&#xff1a;使用硬件&#xff08;光驱 硬盘 u盘等&#xff09;的方法&#xff08;重点&#xff01;&#xff01;&#xff01;&#xff09; 1…

【玩转全栈】----Django连接MySQL

阅前先赞&#xff0c;养好习惯&#xff01; 目录 1、ORM框架介绍 选择建议 2、安装mysqlclient 3、创建数据库 4、修改settings&#xff0c;连接数据库 5、对数据库进行操作 创建表 删除表 添加数据 删除数据 修改&#xff08;更新&#xff09;数据&#xff1a; 获取数据 1、OR…

Supermaven 加入 Cursor:AI 编码新篇章

引言 2024 年 11 月 11 日&#xff0c;我们迎来了一个激动人心的时刻——Supermaven 正式加入 Cursor&#xff01; 这一合作标志着 AI 编程工具进入了一个新的发展阶段&#xff0c;为开发者提供更智能、更高效的编码体验。本文将带您了解此次合并的背景、意义以及未来的发展方…

CM3/4启动流程

CM3/4启动流程 1. 启动模式2. 启动流程 1. 启动模式 复位方式有三种&#xff1a;上电复位&#xff0c;硬件复位和软件复位。 当产生复位&#xff0c;并且离开复位状态后&#xff0c;CM3/4 内核做的第一件事就是读取下列两个 32 位整数的值&#xff1a; 从地址 0x0000 0000 处取…

限时特惠,香港服务器,低至53元/年

家人们谁懂啊&#xff01;香港服务器这价格简直逆天了&#xff0c;居然比内地的还便宜&#xff01;就拿阿里云来说&#xff0c;人家最低配置的服务器&#xff0c;价格都很难做到这么亲民。 最低配的就不说了&#xff0c;2 核 4G 的配置&#xff0c;应对日常业务稳稳当当&#x…

【STM32】点击下载按钮时,提示No ST-LINK detected

00. 目录 文章目录 00. 目录01. 问题描述02. 问题分析03. 问题解决04. 问题验证05. 附录 01. 问题描述 点击下载按钮时&#xff0c;提示No ST-LINK detected。 02. 问题分析 电脑没有检测到有ST-LINK设备&#xff0c;一般是ST-LINK驱动的问题。 03. 问题解决 \1. 先确保ST…

SAP BC 同服务器不同client之间的传输SCC1

源配置client不需要释放 登录目标client SCC1

【前端开发常用网站汇总-01】

1、仿mac界面代码截图 https://codeimg.io/?utm_sourceappinn.com 2、可视化大屏汇总(在线Demo) https://www.xiongze.net/viewdata/index.html 3、在线Photoshop(实现简单P图) https://ps.gaoding.com/#/ 4、在线生成ico图标(png转icon文件) https://www.bitbug.net/in…

win10 VS2019上libtorch库配置过程

win10 VS2019上libtorch库配置过程 0 引言1 获取libtorch2 在VS上配置使用libtorch库3 结语 0 引言 &#x1f4bb;&#x1f4bb;AI一下&#x1f4bb;&#x1f4bb;   libtorch库是一个用于深度学习的C库&#xff0c;是PyTorch的官方C前端。它提供了用于构建和训练深度学习模…

Unity学习笔记(六)使用状态机重构角色移动、跳跃、冲刺

前言 本文为Udemy课程The Ultimate Guide to Creating an RPG Game in Unity学习笔记 整体状态框架(简化) Player 是操作对象的类&#xff1a; 继承了 MonoBehaviour 用于定义游戏对象的行为&#xff0c;每个挂载在 Unity 游戏对象上的脚本都需要继承自 MonoBehaviour&#x…

4. 多线程(2)---线程的状态和多线程带来的风险

文章目录 前言1. 线程的状态1.1. 观察线程的所有状态1.2. 通过不同线程的状态&#xff0c;来调试代码&#xff0c;观察现象 2. 多线程的带来的风险---线程不安全2.1.观察线程不安全的现象2.2 线程不安全的原因2.3.线程不安全的原因 前言 上一篇博客我们学习了&#xff0c;线程…

UE5失真材质

渐变材质函数&#xff1a;RadialGradientExponential&#xff08;指数径向渐变&#xff09; 函数使用 UV 通道 0 来产生径向渐变&#xff0c;同时允许用户调整半径和中心点偏移。 用于控制渐变所在的位置及其涵盖 0-1 空间的程度。 基于 0-1 的渐变中心位置偏移。 源自中心的径…

嵌入式中QT实现文本与线程控制方法

第一:利用QT进行文件读写实现 利用QT进行读写文本的时候进行读写,读取MP3歌词的文本,对这个文件进行读写操作。 实例代码,利用Qfile,对文件进行读写。 //读取对应文件文件,头文件的实现。 #ifndef MAINWINDOW_H #define MAINWINDOW_H#include <QMainWindow> #incl…