目录
前言
在前面对线程的概念和控制的学习过程中,我们知道了线程是共享地址空间的,也就是会共享大部分资源,那么这个时候就会产生新的问题------并发访问,最直观的感受就是每次运行得出的结果值大概率不一致,这种执行结果不一致的现象是非常致命,因为它具有随机性,即结果可能是对的,也可能是错的,无法可靠的完成任务

为了解决这一问题,我们要引入新的解决方案------同步和互斥,我们先来讲互斥!
1.互斥

1.先来见一种现象(数据不一致问题)
• ⼤部分情况,线程使⽤的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程⽆法获得这种变量。
• 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。
• 多个线程并发的操作共享变量,会带来⼀些问题,比如说下面的一段模拟抢票的实验代码
cpp
// 操作共享变量会有问题的售票系统代码
#include <iostream>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
int ticket = 100;
void *route(void *arg)
{
char *id = (char *)arg;
while (1)
{
if (ticket > 0) // 1.判断
{
usleep(1000); // 模拟抢票化的时间
printf("%s sells ticket:%d\n", id, ticket); // 2.模拟抢到了票
ticket--; // 3.票数--
}
else
{
break;
}
}
return nullptr;
}
int main(void)
{
pthread_t t1, t2, t3, t4;
pthread_create(&t1, NULL, route, (void *)"thread 1");
pthread_create(&t2, NULL, route, (void *)"thread 2");
pthread_create(&t3, NULL, route, (void *)"thread 3");
pthread_create(&t4, NULL, route, (void *)"thread 4");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
}

可以看到结果都把票干到负数了,这在现实中可是一件很糟糕的事情,比如说高铁明明只有200个座位,却有201的人抢到了票,这个人是没有位置的,说明多个线程并发的操作共享变量,会带来⼀些问题
2.如何解决上述问题
上面的代码中
临界区:
cpp
while (1)
{
if (ticket > 0) // 1.判断
{
usleep(1000); // 模拟抢票化的时间
printf("%s sells ticket:%d\n", id, ticket); // 2.模拟抢到了票
ticket--; // 3.票数--
}
else
{
break;
}
}
共享资源是:int ticket =1000;
其他代码都属于非临界区
我们要想办法保护临界区:通过在临界区中前后加锁可以保护起来!
cpp
#include <iostream>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
int ticket = 100;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; // 对锁进行初始化
void *route(void *arg)
{
char *id = (char *)arg;
while (1)
{
pthread_mutex_lock(&lock);
if (ticket > 0) // 1.判断
{
usleep(1000); // 模拟抢票化的时间
printf("%s sells ticket:%d\n", id, ticket); // 2.模拟抢到了票
ticket--; // 3.票数--
pthread_mutex_unlock(&lock);
}
else
{
pthread_mutex_unlock(&lock);
break;
}
}
return nullptr;
}
int main(void)
{
pthread_t t1, t2, t3, t4;
pthread_create(&t1, NULL, route, (void *)"thread 1");
pthread_create(&t2, NULL, route, (void *)"thread 2");
pthread_create(&t3, NULL, route, (void *)"thread 3");
pthread_create(&t4, NULL, route, (void *)"thread 4");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
}

可以从结果看到,此时就不会出现票数为负的情况了,顺利解决数据不一致的问题
3.理解为什么数据会不一致&&认识加锁的接口
首先我们需要知道的是ticket--不是原子性的操作,它会被汇编代码转换成三条指令
• load :将共享变量ticket从内存加载到寄存器中
• update : 更新寄存器⾥⾯的值,执⾏-1操作
• store :将新值,从寄存器写回共享变量ticket的内存地址
比如:
0xFF00 载入 ebx ticket
0xFF02 减少 ebx 1
0xFF04 写回 0x1111 ebx
假设我们有A、B两线程,ticket初始是100,在cpu调度A线程执行到0xFF04时要发生线程切换,此时需要保存A的上下文数据:ebx(ticket)为99,cpu的pc指针保存0xFF04地址,然后cpu开始调度B线程,B线程运气很好,在循环执行让ticket减到1之后刚好才要被切换,保存上下文之后cpu又重新调度A,此时pc指针保存的0xFF04地址是要执行写回内存的指令,那么这个时候的ticket又回到了99,这就发生了数据不一致问题,也说明了ticket--不是原子性的操作

\^\] 我们暂时这么去理解原子性:一条汇编就是原子的
我们上面的票数减到负数其实主要的问题不是出在ticket--这个操作,而是出战if条件判断ticket\>0这一操作上,对于ticket值是否大于0做判断也是一种计算(**逻辑计算,得到的是布尔值**),执行时先载入cpu,再判断;那么此时如果有3个线程,ticket此时为1,都完成1的载入后被切走了(因为加了休眠的时间,导致线程没来及做--操作就让下一个线程进来了),后面按顺序唤醒线程时时并行判断都是1就允许进入了,三个线程此时串行载入ticket,执行ticket--然后再写回内存使得ticket此时从1-\>0-\>-1-\>-2就变成-2了
上面的问题告诉了我们:**全局资源没有加保护,可能会有并发问题------线程安全问题**,同时要形成上面的问题需要在多线程中,制造更多的并发、更多的切换,切换的时间点:1.时间片到了 2.阻塞式IO 3.sleep等等...;选择新的线程时间点:从内核态返回用户态的时候,进行检查
要解决以上问题,需要做到三点:
> • 代码必须要有互斥⾏为:当代码进⼊临界区执⾏时,不允许其他线程进⼊该临界区。
>
> • 如果多个线程同时要求执⾏临界区的代码,并且临界区没有线程在执⾏,那么只能允许⼀个线程进⼊该临界区。
>
> • 如果线程不在临界区中执⾏,那么该线程不能阻⽌其他线程进⼊临界区
**要做到这三点,本质上就是需要⼀把锁 ------pthread_mutex_t(互斥锁/互斥量)**

\[\^\] pthread_mutex_init的第二个参数为锁属性,我们不用管设为nullptr就行
加锁规则:尽量加锁的范围粒度要比较细,尽可能不要包含太多的非临界区代码

**对临界区进行保护本质其实就是用锁来对临界区进行保护**
问题1:如果有线程不遵守我们的规则,那就是一个bug,所有线程必须遵守!!
问题2:枷锁之后,在临界区内部允许线程切换吗?切换了会怎么样?
答:允许切换,但是不会怎么样,因为我当前线程并没有释放锁,该线程持有锁被切换,
其他线程也必须等我被切换回来执行完代码、释放锁了才能展开申请锁的竞争,进而
进入临界区(当然这样就会导致多线程执行代码的速度变慢)

**加锁和解锁的本质就是把整个代码块进行原子化,让其他无法中断该线程**
### 4.理解锁
经过上⾯的例⼦,⼤家已经意识到单纯的 i++或者 ++i都不是原⼦的,有可能会有数据⼀致性问题
锁的原理:
1. 硬件级实现:关闭时钟中断
2. 软件级实现:
为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令(只有一条指令保证原子性),该指令的作用是把寄存器和内存单元的数据相交换
下面是一段锁在汇编的伪代码:


### 5.锁的封装
其实在c++中用锁很简单,我们只需要包含#include\