多线程
- 1.线程的概念和理解
- 1.1线程的优点
- 1.2线程的缺点
- 1.3线程的设计
- 1.4线程 VS 进程
- 2.线程控制
- 2.1线程等待
- 2.2 线程终止
- 2.3 线程分离
- 3.线程互斥
- 3.1背景
- 3.2抢票代码演示
- 3.3保护公共资源(加锁)
- 3.3.1创建锁/销毁锁
- 3.3.2申请锁/尝试申请锁/解锁
- 3.4解决抢票的出错逻辑
- 3.5 理解锁
- 4.线程同步(条件变量)
1.线程的概念和理解
在一个程序中的一个执行路线就是线程,更官方的定义就是线程是一个进程内部的控制序列
单线程中代码都是串行调用的。
我们想要实现并发调用也可以使用创建多进程的方法,但是创建进程是比较消耗资源的,要创建进程结构,页表,并建立映射关系,成本比较高;
但是线程就不一样了,在地址空间内创建的“进程”就叫做线程,只需要创建内核数据结构即可。
线程:就是进程内部的一个执行分支
1.1线程的优点
1.2线程的缺点
1.3线程的设计
线程也是要被管理的,进程有PCB,线程也有类似的东西(TCB)
但是这样创建非常的复杂,线程和进程有高度的相似性,没有必要单独设计这个算法,所以使用进程来模拟线程。
1.4线程 VS 进程
从内核的角度出发,进程就是承担分配系统资源的基本实体。
那么多执行流是如何进行代码划分的?
一个32位分成了3个部分大小分别为10 10 12
前10个是页目录
中间10个是页表
页表如何找到相应的内存中的数据?
页表中的地址+ 虚拟地址后12位对应的数据(页内偏移)
给线程分配不同的区域,本质是就是给不同的线程,各自看到全部页表的子集
2.线程控制
linux中没有真线程,只有轻量级进程的概念,但是用户只认线程;
所以linux中没有线程相关的系统调用,只有轻量级进程的系统调用,
pthread库—原生线程库——>将轻量级进程的系统调用进行封装,转换成线程相关的接口提供给用户。
所以我们在编写线程代码时必须 -pthread
线程演示代码:
void *threadrun(void *arg)
{
int cnt = 5;
while (cnt)
{
cout << "新线程正在运行:" << cnt << "pid:" << getpid() << endl;
sleep(1);
cnt--;
}
return nullptr;
}
int main()
{
// int pthread_create(pthread_t * thread, const pthread_attr_t *attr,
// void *(*start_routine)(void *), void *arg);
pthread_t tid;
pthread_create(&tid, nullptr, threadrun, nullptr);
int cnt = 10;
while (cnt)
{
cout << "主线程正在运行:" << cnt << "pid:" << getpid() << endl;
sleep(1);
cnt--;
}
return 0;
}
可以看出两个循环是一起运行的,所以进程中是有两个执行流的,并且他们是属于同一个进程。
主线程退出==进程退出,所有线程都要结束运行退出。
- 一般来说为了保证线程完成我们预期的工作,都是要保证主线程最后结束
- 线程也是需要被等待的,不然就会产生类似于进程退出的内存泄漏问题。
补充:
ps -aL :会列出系统中所有具有终端的进程以及线程
2.1线程等待
void *retval:输出型参数,他就是线程的返回值(返回值为void)类型的
2.2 线程终止
同一个进程内,大部分资源都是共享的,其中就包括了地址空间。
线程出异常:
多线程中任何一个线程出现了异常,都会导致整个进程退出。
- 一个线程出问题,导致其他线程也出现问题,导致整个进程退出----线程安全问题
- 多线程中,公共函数被多个线程同时进入—出现重入问题
线程退出的时候不会像进程退出一样拿到退出信息,因为一但线程出异常之后,整个进程都会退出,没有时间来进行线程等待。
注意:线程退出不可以使用exit,这样会导致整个进程退出。
线程退出的三种方式
- return
- pthread_exit
- pthread_cancel:取消线程(让主线程去取消目标线程)
2.3 线程分离
默认情况下线程也是要被等待的,但是线程也是可以手动设置分离的。
如果主线程不关系新线程的执行结果,我们可以把新线程设置为分离状态。
被分离之后的线程就不可以再join了,不然就会出错。
线程分离:底层依旧是一个进程,一般都希望主线程最后一个退出,所以在线程分离中,主线程一般都是永远不退出的。
3.线程互斥
3.1背景
多个执行流共享的资源是共享资源,但是我们把他保护起来,一次只允许一个线程访问,这个就叫做临界资源
在代码中,访问临界资源的代码就叫做临界区
互斥:任何时刻,只允许一个执行流进入临界区,访问临界资源。
原子性:不会被任何调度机制大端,只有两种形态,要么完成,要么未完成。
3.2抢票代码演示
tickets>0;这是一个逻辑运算,当处理时,cpu会把内存中的数据拷贝到寄存器中,但是当进行到usleep时,这个线程就会进入等待队列,并且带走自己的上下文数据,没有执行到tickets–的位置,就会导致判断失误,会有很多的线程进入到这个抢票逻辑中取。
当进入到–操作时,把数据从内存读取到cpu,cpu进行内部–,再重新写回内存。
我们再从编译的角度去理解原子性
如果一个代码转换成汇编只有一条语句那他就是原子的
例如 - -操作,就不是原子,他会被转化为3条语句
3.3保护公共资源(加锁)
3.3.1创建锁/销毁锁
如果定义的锁是静态的或者是全局的,就不需要初始化也不需要销毁
直接:
pthread_mutex_tmutex=PTHREAD_MUTEX_INITIALIZER;
3.3.2申请锁/尝试申请锁/解锁
申请锁/尝试申请锁,区别就是申请锁出错会阻塞,而另一个出错直接返回。
3.4解决抢票的出错逻辑
出现并发访问的问题,本质就是因为多个执行流并发访问全局数据的代码导致的,保护共享资源本质就是保护临界区
加锁的本质就是把并发访问的代码,变成串行访问,并且加锁的粒度要越细越好。
注意:有些平台会出现上面的问题,加锁之后,不同线程对锁的竞争强度不同,这算是一个bug,原则上竞争锁是自由的,竞争锁的能力太强就会导致饥饿问题。
3.5 理解锁
4.线程同步(条件变量)
一个线程跑完就接着下一个,解决饥饿问题。
pthread_cond_t cond=PTHREAD_COND_INITIALIZER;
条件变量是在加锁内使用的。
从条件变量的函数来看,其实他和锁的用法极其的相似。