进程线程间的互斥相关背景概念
注:临界资源可能会因为多个线程同时访问这块资源导致数据错乱。
#include
#include
#include
#include
#include
#include
using namespace std;
// int 票数计数器
int tickets = 1000; // 临界资源,可能会因为共同访问,造成数据不一致的问题
void *getTickets(void *args)
{
const char *name = static_cast<const char *>(args);
while (true)
{
// 临界区
if (tickets > 0)
{
usleep(1000);
cout << name << " 抢到了票, 票的编号: " << tickets << endl;
tickets--;
}else
{
break;
}
}
return nullptr;
}
int main()
{
pthread_t tid1;
pthread_t tid2;
pthread_t tid3;
pthread_t tid4;
pthread_create(&tid1, nullptr, getTickets, (void *)"thread 1");//线程1
pthread_create(&tid2, nullptr, getTickets, (void *)"thread 2");//线程2
pthread_create(&tid3, nullptr, getTickets, (void *)"thread 3");//线程3
pthread_create(&tid4, nullptr, getTickets, (void *)"thread 4");//线程4
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
pthread_join(tid3,NULL);
pthread_join(tid4,NULL);
return 0;
}
可以看到,明明就设置了1000张票,怎么会出现编号为-1、-2
的票呢?
这是因为tickets--
的操作不是原子性的,而是分为三个步骤:
- 将共享变量tickets从内存加载到寄存器中
- 更新寄存器里面的值 执行-1操作
- 将新值从寄存器写回共享变量tickets的内存地址
汇编代码如下:
既然–操作要经历三个步骤才能完成 那么有可能thread1
刚刚把1000读进cpu就被切换了
当该线程被切换的时候会保存它对应的上下文信息 1000这个数据当然也在里面 之后thread1
被挂起。
之后我们的thread2
进程就被调度了 因为当thread1
被切换的时候内存中的tickek值并没有被改变 所以说thread2
看到的值还是1000
我们假设thread2
的竞争性比较强 它执行了100次之后才被切换 那么此时的ticket的值就由1000变成了900。
当thread2切换挂起之后我们的thread1回来继续执行 此时恢复它的上下文数据
由于上次保存时它寄存器中的数据是1000 所以说再经历23两步操作之后变为999之后加载到内存中
于是内存中的数据便从900变成999了 相当于此时多了1000张票。
从上面的流程中我们可以看出 --ticket
这个操作并不是原子性的
那么我们如何解决上面的问题呢?
其实思路很简单 我们只需要将--ticke
t这个操作变成原子性的就好了
那么怎么将它变成原子性的呢? 我们的策略是加锁。
要解决如上的问题就得做到以下三点:
我们可以使用如下代码来定义一个互斥量。
pthread_mutex_t mutex;
#include
int pthread_mutex_destroy(pthread_mutex_t *mutex);
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_init
函数对Mutex
做初始化,参数attr
设定Mutex
的属性,如果attr
为NULL
则表示缺省属性。用pthread_mutex_init
函数初始化的Mutex
可以用pthread_mutex_destroy
销毁。如果Mutex
变量是静态分配的(全局变量或static
变量),也可以用宏定义PTHREAD_MUTEX_INITIALIZER
来初始化,相当于用pthread_mutex_init
初始化并且attr
参数为NULL
。
#include
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
一个线程可以调用pthread_mutex_lock
获得Mutex
,如果这时另一个线程已经调用pthread_mutex_lock
获得了该Mutex
,则当前线程需要挂起等待,直到另一个线程调用pthread_mutex_unlock
释放Mutex
,当前线程被唤醒,才能获得该Mutex
并继续执行。
如果一个线程既想获得锁,又不想挂起等待,可以调用pthread_mutex_trylock
,如果Mutex
已经被另一个线程获得,这个函数会失败返回EBUSY
,而不会使线程挂起等待。
#include
#include
#include
#include
#include
#include
using namespace std;
// int 票数计数器
int tickets = 1000; // 临界资源,可能会因为共同访问,造成数据不一致的问题
//pthread_mutex_t Mutex=PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t Mutex;
void *getTickets(void *args)
{
const char *name = static_cast<const char *>(args);
while (true)
{
pthread_mutex_lock(&Mutex);
// 临界区
if (tickets > 0)
{
usleep(100);
cout << name << " 抢到了票, 票的编号: " << tickets << endl;
tickets--;
}else
{
break;
}
pthread_mutex_unlock(&Mutex);
}
return nullptr;
}
int main()
{
pthread_mutex_init(&Mutex,NULL);
pthread_t tid1;
pthread_t tid2;
pthread_t tid3;
pthread_t tid4;
pthread_create(&tid1, nullptr, getTickets, (void *)"thread 1");//线程1
pthread_create(&tid2, nullptr, getTickets, (void *)"thread 2");//线程2
pthread_create(&tid3, nullptr, getTickets, (void *)"thread 3");//线程3
pthread_create(&tid4, nullptr, getTickets, (void *)"thread 4");//线程4
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
pthread_join(tid3,NULL);
pthread_join(tid4,NULL);
return 0;
}
这样就不会出现票数小于等于0的情况了,但是为什么一直都是thread 4
抢到票呢?其他的线程为什么不能抢票?
这是因为锁的钥匙已经被线程4早早地拿走了,其他线程没有钥匙是打不开锁的,就只能在锁区域外等候。
锁是否需要被保护呢?
锁是不许要被保护的,因为从汇编层面保证了加锁是原子性的,争夺锁只有两种可能,要么拿到了,要么没有拿到。
为了实现互斥锁的操作,大多数体系结构都提供了swap
或exchange
指令该指令的作用就是把寄存器和内存单元的数据相交换。
以加锁示例,这是由多态汇编语句执行的,上述
%al
是寄存器,mutex
就是内存中的一个变量。每个线程申请锁时都要执行上述语句,执行步骤如下:
(
movb $0
,%al
)先将al寄存器中的值清0。该动作可以被多个线程同时执行,因为每个线程都有自己的一组寄存器(上下文信息),执行该动作本质上是将自己的al寄存器清0。注意:凡是在寄存器中的数据,全部都是线程的内部上下文!多个线程看起来同时在访问寄存器,但是互不影响。(
xchgb %al
,mutex
)然后用此一条指令交换al寄存器和内存中mutex的值,xchgb是体系结构提供的交换指令,该指令可以完成寄存器和内存单元之间数据的交换。最后判断al寄存器中的值是否大于0。若大于0则申请锁成功,此时就可以进入临界区访问对应的临界资源;否则申请锁失败需要被挂起等待,直到锁被释放后再次竞争申请锁。
示例:假设内存中有一个变量
mutex
为1,cpu
内部有%al寄存器,我们有threadA
和threadB
俩线程,**示例:**现在线程A要开始加锁,执行上述语句。首先(
movb $0
,%al
),线程A把0读进al
寄存器(清0寄存器),然后执行第二条语句(xchgb %al
,mutex
),将al
寄存器中的值与内存中mutex
的值进行交换。
al
的值为1,内存中mutex
的值为0。此时这个过程就是加锁mutex
的数据先前已经被线程A交换至寄存器,然后保存到线程A的上下文了,现在的mutex
为0,而线程B执行交换动作,拿寄存器al
的0去换内存中mutex
的0。即使我线程A在执行第一条语句把寄存器清0后就发生了线程切换(切至线程B),线程A保存上下文数据(0),此时线程B执行第一条语句把0写进寄存器,随后线程B执行第二条语句xchgb
交换:
此时线程A执行第三条语句if判断失败,只能被挂起等待,线程A只能把自己的上下文数据保存,重新切换至线程B,也就是说我线程B只要不运行,你们其它所有线程都无法申请成功。线程B恢复上下文数据(1)到内存,然后执行第三条语句if成功,返回结果
**注意:**上述xchgb
就是申请锁的过程。申请锁是将数据从内存交换到寄存器,本质就是将数据从共享内存变成线程私有。
mutex
就是内存里的全局变量,被所有线程共享,但是一旦用一条汇编语句将内存的mutex
值交换到寄存器,寄存器内部是哪个线程使用,那么此mutex
就是哪个线程的上下文数据,那么就意味着交换成功后,其它任何一个线程都不可能再申请锁成功了,因为mutex
已经独属于某线程私有了。mutex = 1
就如同令牌一般,哪个线程先交换拿到1,那么哪个线程就能申请锁成功,所以加锁是原子的mutex
置回1。使得下一个申请锁的线程在执行交换指令后能够得到1,形象地说就是“将锁的钥匙放回去”。Mutex
的线程。唤醒这些因为申请锁失败而被挂起的线程,让它们继续竞争申请锁。总结:
al
寄存器中的值清0,这不会造成影响,因为每次线程在申请锁时都会先将自己al
寄存器中的值清0,再执行交换指令。mutex
通过交换指令,原子性的交换到自己的al
寄存器中。一般情况下,如果同一个线程先后两次调用lock,在第二次调用时,由于锁已经被占用,该线程会挂起等待别的线程释放锁,然而锁正是被自己占用着的,该线程又被挂起而没有机会释放锁,因此就永远处于挂起等待状态了,这叫做死锁(Deadlock)。另一种典型的死锁情形是这样:线程A获得了锁1,线程B获得了锁2,这时线程A调用lock试图获得锁2,结果是需要挂起等待线程B释放锁2,而这时线程B也调用lock试图获得锁1,结果是需要挂起等待线程A释放锁1,于是线程A和B都永远处于挂起状态了。
写程序时应该尽量避免同时获得多个锁,如果一定有必要这么做,则有一个原则:如果所有线程在需要多个锁时都按相同的先后顺序(常见的是按
Mutex
变量的地址顺序)获得锁,则不会出现死锁。比如一个程序中用到锁1、锁2、锁3,它们所对应的Mutex
变量的地址是锁1<锁2<锁3,那么所有线程在需要同时获得2个或3个锁时都应该按锁1、锁2、锁3的顺序获得。如果要为所有的锁确定一个先后顺序比较困难,则应该尽量使用pthread_mutex_trylock
调用代替pthread_mutex_lock
调用,以免死锁。
Case1:因为自己拿了锁,又去申请这把锁,导致死锁了。
#include
#include
#include
#include
#include
#include
using namespace std;
// int 票数计数器
int tickets = 1000; // 临界资源,可能会因为共同访问,造成数据不一致的问题
//pthread_mutex_t Mutex=PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t Mutex;
void *getTickets(void *args)
{
const char *name = static_cast<const char *>(args);
while (true)
{
pthread_mutex_lock(&Mutex);
pthread_mutex_lock(&Mutex);
// 临界区
if (tickets > 0)
{
usleep(100);
cout << name << " 抢到了票, 票的编号: " << tickets << endl;
tickets--;
}else
{
break;
}
pthread_mutex_unlock(&Mutex);
pthread_mutex_unlock(&Mutex);
}
return nullptr;
}
int main()
{
pthread_mutex_init(&Mutex,NULL);
pthread_t tid1;
pthread_create(&tid1, nullptr, getTickets, (void *)"thread 1");//线程1
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
pthread_join(tid3,NULL);
pthread_join(tid4,NULL);
return 0;
}
Case2:互相申请对方的锁。
#include
#include
#include
#include
#include
#include
using namespace std;
// int 票数计数器
int tickets = 1000; // 临界资源,可能会因为共同访问,造成数据不一致的问题
pthread_mutex_t Mutex=PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t Mutex1=PTHREAD_MUTEX_INITIALIZER;
void *getTickets(void *args)
{
const char *name = static_cast<const char *>(args);
while (true)
{
pthread_mutex_lock(&Mutex);
sleep(1);
pthread_mutex_lock(&Mutex1);
// 临界区
if (tickets > 0)
{
usleep(100);
cout << name << " 抢到了票, 票的编号: " << tickets << endl;
tickets--;
}else
{
break;
}
pthread_mutex_unlock(&Mutex1);
pthread_mutex_unlock(&Mutex);
}
return nullptr;
}
void *getTickets1(void *args)
{
const char *name = static_cast<const char *>(args);
while (true)
{
pthread_mutex_lock(&Mutex1);
sleep(1);
pthread_mutex_lock(&Mutex);
// 临界区
if (tickets > 0)
{
usleep(100);
cout << name << " 抢到了票, 票的编号: " << tickets << endl;
tickets--;
}else
{
break;
}
pthread_mutex_unlock(&Mutex);
pthread_mutex_unlock(&Mutex1);
}
return nullptr;
}
int main()
{
pthread_t tid1;
pthread_t tid2;
pthread_create(&tid1, nullptr, getTickets, (void *)"thread 1");//线程1
pthread_create(&tid2, nullptr, getTickets1, (void *)"thread 2");//线程2
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
return 0;
}
注意 这是四个必要条件 也就是说四个条件全部满足才能够形成死锁