学院网站建设用户需求分析报告,刷外链工具,推广价格一般多少,邢台123生活最新帖子文章目录 前言1. 线程概念1.1 什么是线程1.2 线程比进程更加轻量化1.3 虚拟地址到物理地址的转化物理内存的管理页表 1.4 线程的优点1.5 线程的缺点1.6 线程异常1.7 线程用途 2. 进程 vs 线程3. 线程控制3.1 线程创建3.2 线程退出3.3 线程等待3.4 分离线程3.5 线程取消 4. 线程… 文章目录 前言1. 线程概念1.1 什么是线程1.2 线程比进程更加轻量化1.3 虚拟地址到物理地址的转化物理内存的管理页表 1.4 线程的优点1.5 线程的缺点1.6 线程异常1.7 线程用途 2. 进程 vs 线程3. 线程控制3.1 线程创建3.2 线程退出3.3 线程等待3.4 分离线程3.5 线程取消 4. 线程管理4.1 使用原生线程库进行简单的封装 5. 线程的互斥5.1 进程线程间的互斥相关背景概念5.2 互斥量mutex5.3 线程不互斥会发生什么样的错误5.4 互斥量加锁5.5 可重入和线程安全5.5.1 概念5.5.2 常见的线程不安全的情况5.5.3 常见的线程安全的情况5.5.4 常见不可重入的情况5.5.5 常见可重入的情况5.5.6 可重入与线程安全联系5.5.7 可重入与线程安全区别 6. 死锁6.1 概念6.2 死锁的四个必要条件6.3 避免死锁6.4 避免死锁的算法6.5 小问题 7. 线程同步7.1 条件变量7.2 同步概念与竞态条件7.3 条件变量函数7.4 为什么 pthread_cond_wait 需要互斥量? 8. 生产者消费者模型8.1 为何要使用生产者消费者模型8.2 生产者消费者模型优点8.3 基于BlockingQueue的生产者消费者模型 9. POSIX信号量9.1 认识接口9.2 基于环形队列的生产者消费者模型 10. 线程池11. 线程安全的单例模式11.1 什么是单例模式11.2 什么是设计模式11.3 单例模式的特点 12. STL智能指针和线程安全12.1 STL中的容器是否是线程安全的吗?12.2 智能指针是否是线程安全的? 13. 其他常见的各种锁 前言 本章所讲的线程原理仅仅是Linux下的线程。
1. 线程概念
1.1 什么是线程
在一个程序里的一个执行路线就叫做线程thread。更准确的定义是线程是“ 一个进程内部的控制序列 ”。一切进程至少都有一个执行线程。线程在进程内部运行本质是在进程地址空间内运行。在Linux系统中在CPU眼中看到的PCB都要比传统的进程更加轻量化。透过进程虚拟地址空间可以看到进程的大部分资源将进程资源合理分配给每个执行流就形成了线程执行流。 课本线程是比进程更加轻量级的一种执行流 / 线程是在进程内部执行的一种执行流。 我们线程是CPU调度的基本单元 / 进程是承担系统资源的基本实体。 左边是我们之前学习的进程PCB右边就是线程什么意思呢就是将一个进程的PCB内容进行切割将其中的数据分成若干份分别由一个线程来维护也就是说线程是通过进程将数据分割成多份形成的多个线程组成了一个进程那么必然的线程相较于进程更加轻量级。 当第一个线程被CPU调度时后面的线程其实只是参与了资源的分配也就是哪一部分的数据是由你这个线程维护的相当于仅仅只是声明实际上并没有在物理内存中开辟具体的资源因此在最开始初始化的时候它并不会一下子将所有进程的数据都初始化才开始进行调度只初始化一个线程大小的数据就可以开始调度了。相较于初始化完整个进程再调度优秀了很多。 而进程都有可能有多个那么线程只会比进程更多进程都需要被维护起来那么线程的维护也是必然的。OS如果真的支持线程那么就必须管理线程先描述再组织TCB、Thread。但是一个进程的相关数据描述在PCB中已经存在难度还要再重新创建一个结构体来重新再描述一遍吗那岂不是很麻烦。 并不需要的只需要对PCB进行复用就可以了这就是Linux中描述线程的实现方案。对于之前学习的进程我们的看法是它的内部只有一个执行流的进程现在的看法是内核中有多个执行流的进程。 我们通过代码来看一看 ps -aL指令是用来查看线程的。我们发现两个线程的pid是一样的这也验证了我们之前所说的线程是由进程分割而来的。 CPU的调度其实不是根据pid来调度的而是通过LWP来进行调度。也就是说CPU调度的并不是进程而是进程细化下的线程 我们再来看一个代码 我们发现我们新建的线程将gcnt进行减减操作主线程中的gcnt也进行了减减这就更近一步说明了线程是进程分化而来的每一个线程都共享了进程的大部分数据。在前面的进程章节中在父子进程中如果子进程将一个全局变量修改那么就会发生写时拷贝并不会影响父进程中的变量。
1.2 线程比进程更加轻量化 创建线程很简单只需要创建一个PCB就可以 因为创建线程的前提一定是先有进程。它并不需要像进程一样还需要初始化很多数据才可以被调度因此使用线程调度比进程更加快速。 还有另外一个重大原因在CPU中处理寄存器其实还有一块比较大的缓存cache里面缓存的是你当前访问的代码的附近的代码这是根据局部性原理原理的意思是当你访问当前代码时你大概率会继续访问当前代码附近的代码所有它会预加载避免多次访问。cache缓存是以进程单位的线程级的切换并不会影响cache的内容 而如果进程间切换就需要重新对数据进程缓存。 同时由于线程是共享进程的大部分代码的所有线程在切换时只需要更改少量寄存器中的数据大部分是不需要更改的。
1.3 虚拟地址到物理地址的转化
物理内存的管理 在文件系统中进行IO的基本单位是4KB也就是磁盘向物理内存中写入读出数据时都是以4KB大小为进本单位进行访存的。 物理内存有4GB换算下来一共有1048576个页框这么多的页框一定是需要被先描述再组织的。
页表 我们知道虚拟地址以16进制表示是全0到全F的也就是有2的32次方个地址而每一个虚拟地址映射到一个物理地址都需要至少实际字节的空间那么有那么多的虚拟地址如果都映射完光是页表的大小就都有几十GB了一点也不现实。 所以页表肯定不是像之前所画的那中样子那么是什么样的呢 这样我们只需要维护2^20次方的空间就可以了这样页表的体积就大大减小了。而这个页表项仅仅只是找到了对应的页框也就是该页框的起始地址但是页框有4KB大小如何才能找到我们具体的数据地址呢 那么就用到上面的还未使用的12位了。找到了页框的起始地址再以低12位充当偏移量以偏移量加上起始地址就可以找到我们需要的数据了2^ 12 2^ 10 * 2^ 2 1024 * 4 4KB)。所以通过低12位的偏移量再加上页框的起始地址是一定能找到一个页框4KB大小中的任意一个地址的。 从上图不难看出其实虚拟地址到物理地址的转化在CPU中就已经完成了。 所以线程的本质划分页表划分进程地址空间。
1.4 线程的优点
创建一个新线程的代价要比创建一个新进程小得多。与进程之间的切换相比线程之间的切换需要操作系统做的工作要少很多。线程占用的资源要比进程少很多。能充分利用多处理器的可并行数量。在等待慢速I/O操作结束的同时程序可执行其他的计算任务。计算密集型应用为了能在多处理器系统上运行将计算分解到多个线程中实现。I/O密集型应用为了提高性能将I/O操作重叠。线程可以同时等待不同的I/O操作。
1.5 线程的缺点
性能损失 一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多那么可能会有较大的性能损失这里的性能损失指的是增加了额外的同步和调度开销而可用的资源不变。健壮性降低 编写多线程需要更全面更深入的考虑在一个多线程程序里因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的换句话说线程之间是缺乏保护的。缺乏访问控制 进程是访问控制的基本粒度在一个线程中调用某些OS函数会对整个进程造成影响。编程难度提高 编写与调试一个多线程程序比单线程程序困难得多。
1.6 线程异常
单个线程如果出现除零野指针问题导致线程崩溃进程也会随着崩溃。线程是进程的执行分支线程出异常就类似进程出异常进而触发信号机制终止进程进程终止该进程内的所有线程也就随即退出。
1.7 线程用途
合理的使用多线程能提高CPU密集型程序的执行效率。合理的使用多线程能提高IO密集型程序的用户体验如生活中我们一边写代码一边下载开发工具就是多线程运行的一种表现。
2. 进程 vs 线程
进程是资源分配的基本单位。线程是调度的基本单位。线程共享进程数据但也拥有自己的一部分数据 线程ID 一组寄存器属于自己的硬件上下文 栈独立的栈 errno 信号屏蔽字 调度优先级 进程的多个线程共享同一地址空间因此Text Segment代码段、Data Segment数据区都是共享的如果定义一个函数在各线程中都可以调用如果定义一个全局变量在各线程中都可以访问到除此之外各线程还共享以下进程资源和环境
文件描述符表每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)当前工作目录用户id和组id 进程和线程的关系如下图
3. 线程控制 在Linux中是不存在真正的线程的只有轻量级进程的概念。所以Linux OS只会提高轻量级进程创建的系统调用不会直接提供线程创建的接口。
3.1 线程创建 线程创建
功能创建一个新的线程
原型
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);
参数
thread:返回线程ID
attr:设置线程的属性attr为NULL表示使用默认属性
start_routine:是个函数地址线程启动后要执行的函数
arg:传给线程启动函数的参数
返回值成功返回0失败返回错误码第一个参数是线程id第二个是线程的状态目前不考虑写为nullptr即可第三个参数是函数指针用来说明新线程用来做什么工作第四个参数就是函数指针的参数。 传统的一些函数是成功返回0失败返回-1并且对全局变量errno赋值以指示错误。 pthreads函数出错时不会设置全局变量errno而大部分其他POSIX函数会这样做。而是将错误代码通过返回值返回。 pthreads同样也提供了线程内的errno变量以支持其它使用errno的代码。对于pthreads函数的错误建议通过返回值业判定因为读取返回值要比读取线程内的errno变量的开销更小。
pthread_ create函数会产生一个线程ID存放在第一个参数指向的地址中。该线程ID和前面说的线程ID不是一回事。前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程是操作系统调度器的最小单位所以需要一个数值来唯一表示该线程。pthread_ create函数第一个参数指向一个虚拟内存单元该内存单元的地址即为新创建线程的线程ID属于NPTL线程库的范畴。线程库的后续操作就是根据该线程ID来操作线程的。线程库NPTL提供了pthread_ self函数可以获得线程自身的ID pthread_t pthread_self(void); pthread_t 到底是什么类型呢取决于实现。对于Linux目前实现的NPTL实现而言pthread_t类型的线程ID本质就是一个进程地址空间上的一个地址。
3.2 线程退出 如果需要只终止某个线程而不终止整个进程,可以有三种方法
从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。线程可以调用pthread_ exit终止自己。一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。 pthread_exit函数 功能线程终止 原型 void pthread_exit(void *value_ptr); 参数 value_ptrvalue_ptr不要指向一个局部变量。 返回值无返回值跟进程一样线程结束的时候无法返回到它的调用者自身 需要注意pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的不能在线程函数的栈上分配因为当其它线程得到这个返回指针时线程函数已经退出了。
3.3 线程等待 为什么需要线程等待 已经退出的线程其空间没有被释放仍然在进程的地址空间内。创建新的线程不会复用刚才退出线程的地址空间。 功能等待线程结束。 原型 int pthread_join(pthread_t thread, void **value_ptr); 参数 thread线程ID。 value_ptr:它指向一个指针后者指向线程的返回值。 返回值成功返回0失败返回错误码。 调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的总结如下:
如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_ CANCELED — void*-1。如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。
3.4 分离线程
默认情况下新创建的线程是joinable的线程退出后需要对其进行pthread_join操作否则无法释放资源从而造成系统泄漏。如果不关心线程的返回值join是一种负担这个时候我们可以告诉系统当线程退出时自动释放线程资源。
int pthread_detach(pthread_t thread);可以是线程组内其他线程对目标线程进行分离也可以是线程自己分离
pthread_detach(pthread_self());joinable和分离是冲突的一个线程不能既是joinable又是分离的。 分离之后pthread_join函数的返回值是22代表着这个线程是分离的。
3.5 线程取消 功能取消一个执行中的线程 原型 int pthread_cancel(pthread_t thread); 参数 thread线程ID 返回值成功返回0失败返回错误码 当一个线程一直在运行可以通过这个函数在主线程中取消它取消之后该线程的返回值是-1。如果一个线程先被分离再被取消那么它的返回值就是0。
4. 线程管理 在系统中是没有线程的概念的只有轻量级进程的概念所以我们上面使用的都不是系统直接提供的接口而是原生线程库pthread提供的接口。 这也就意味这线程的管理并不是由系统进行管理的而是由线程库来管理的。也就是说在用户和系统之间是需要通过线程库来当中间商的 线程要有独立的属性其中最重要的是上下文数据和各自独立的栈上下文数据还好说每一个线程都有自己的PCB各自可以独立进行维护但是在进程地址空间中栈只有一个而每一个线程又都要有自己独立的栈这该怎么办呢 在前文的动静态库中我们有说过对于C语言中的FILE结构体它内部是自己维护了一个缓存区的是由C语言维护的实际上也就是库维护的也就是说库是有维护一段空间的能力的而pthead库也是库自然也有这个能力。 对于pthread库它其实也是维护了一块地址空间是从堆上申请而来的也就是说pthead库是维护了一块堆上的空间。对于每个新线程的栈空间都是经由这块对空间分割而来的。也就是说对于每个线程独立的栈空间实际上是进程地址空间中的堆区的空间而进程地址空间上的栈区则交由主线程来使用。其实也可以使用静态的空间 如何理解pthread管理线程 每个线程的 pthread_t 线程id就是线程属性集合在库中的地址。 站在语言角度来理解pthread 其底层就是进行了封装如果是Linux系统就封装我们上面所学的而如果是windows系统如果windows系统支持真线程的概念那么就会封装windows系统中的接口。但是在语言层面是不变的都是上面的代码只不过在不同系统中底层封装的东西不一样而已。 C11内部的多线程本质上就是对原生线程库的封装。 如何理解线程的局部存储 当一个全局变量被一个线程修改时由于众多线程是共享一个进程地址空间的所以其他线程也是可以看到一个全局变量变化的。 我们加一个选项 我们发现主线程所看到的全局变量并没有发生变化。说明当我们加上__thread每一个线程就会私有化一份这个变量这个就是线程的局部存储 但是__thread只针对内置类型。
4.1 使用原生线程库进行简单的封装 5. 线程的互斥
5.1 进程线程间的互斥相关背景概念
临界资源多线程执行流共享的资源就叫做临界资源。临界区每个线程内部访问临界资源的代码就叫做临界区。互斥任何时刻互斥保证有且只有一个执行流进入临界区访问临界资源通常对临界资源起保护作用。原子性后面讨论如何实现不会被任何调度机制打断的操作该操作只有两态要么完成要么未完成。
5.2 互斥量mutex
大部分情况线程使用的数据都是局部变量变量的地址空间在线程栈空间内这种情况变量归属单个线程其他线程无法获得这种变量。但有时候很多变量都需要在线程间共享这样的变量称为共享变量可以通过数据的共享完成线程之间的交互。多个线程并发的操作共享变量会带来一些问题。
5.3 线程不互斥会发生什么样的错误 我们这里举个例子以抢票为例我们可以创建多个线程进行抢票 我们再来看结果 我们发现抢票竟然出现了负数这显然是不合理的这是因为每个线程在访问这个函数那么就会造成重入现象。就比如线程1走到了printf函数的位置此时线程1还没有运行ticket- - 的操作但是此时ticket的值已经为1但是此时发生了线程切换线程2、3、4都再次进入了这个函数但是此时票数其实已经为1了相当于票其实已经卖光了但是线程2、3、4依旧会继续被调度执行造成票数变成负数的情况。 数据再内存中本质上是被线程共享的但是如果数据被读入到寄存器中本质上就变成了线程上下文属于线程私有的数据了 - - 操作并不是原子操作而是对应三条汇编指令
load 将共享变量ticket从内存加载到寄存器中。update : 更新寄存器里面的值执行-1操作。store 将新值从寄存器写回共享变量ticket的内存地址。 要解决以上问题需要做到三点
代码必须要有互斥行为当代码进入临界区执行时不允许其他线程进入该临界区。如果多个线程同时要求执行临界区的代码并且临界区没有线程在执行那么只能允许一个线程进入该临界区。如果线程不在临界区中执行那么该线程不能阻止其他线程进入临界区。 要做到这三点本质上就是需要一把锁。Linux上提供的这把锁叫互斥量。
5.4 互斥量加锁 创建锁 加锁、解锁使用PTHREAD_MUTEX_INITIALIZER初始化 示例 ticket是全局变量我们上面创建的锁也是全局变量为什么就可以进行保护了呢 申请锁本身是安全的它是原子的在后面会再次进行细说。 原子到底是什么意思呢拿上面的 - - ticket 为例上面说这个操作实际上是在汇编层面是分三步走的在第一步完成后有可能发生线程切换所以 - - 操作并不是原子的因为它中间会被打断。而单个的汇编操作就是原子的它只有成功或者不做两种状态。个人理解一个操作会被划分为多个步骤来完成那么它可能就不是原子的因为在执行期间可能会被打断而如果一个操作无法被继续划分更细小的步骤那么它就是原子的只有成功和不做两种状态。 根据互斥的定义任何时刻只允许一个线程申请锁成功。多个线程申请锁失败失败的进程会在mutex上进行阻塞本质就是等待 这个函数就是用来尝试申请锁如果成功就会返回0如果失败就会返回错误信息。 一个线程在临界区中访问临界资源的时候可不可能发生线程切换答案是可能的是完全允许的。 加锁的本质 为了实现互斥锁操作大多数体系结构都提供了swap或exchange指令该指令的作用是把寄存器和内存单元的数据相交换由于只有一条指令保证了原子性即使是多处理器平台访问内存的总线周期也有先后一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。 现在我们把lock和unlock的伪代码改一下
//都是汇编语言都是原子的噢
lock:movb $0, %alxchgb %al, mutexif(al寄存器的内容0){return 0;}else{挂起等待;}goto lock;unlock:movb $1, mutex唤醒等待Metux的线程;return 0;我们知道寄存器是只有一套的但是寄存器的内容是有多套的它被保存在PCB中。同时众多线程是会共享大部分数据的。 当交换之后al寄存器中的值就变为了1内存中的值变为0注意多个线程是共享内存数据的然后进行判断如果al寄存器中的值为1就允许加锁否则就挂起等待。 解锁的过程与其相似。 关于加锁的原则谁加锁谁解锁。
5.5 可重入和线程安全
5.5.1 概念
线程安全多个线程并发同一段代码时不会出现不同的结果。常见对全局变量或者静态变量进行操作并且没有锁保护的情况下会出现该问题。重入同一个函数被不同的执行流调用当前一个流程还没有执行完就有其他的执行流再次进入我们称之为重入。一个函数在重入的情况下运行结果不会出现任何不同或者任何问题则该函数被称为可重入函数否则是不可重入函数。函数层面
5.5.2 常见的线程不安全的情况
不保护共享变量的函数。函数状态随着被调用状态发生变化的函数。返回指向静态变量指针的函数。调用线程不安全函数的函数
5.5.3 常见的线程安全的情况
每个线程对全局变量或者静态变量只有读取的权限而没有写入的权限一般来说这些线程是安全的。类或者接口对于线程来说都是原子操作。多个线程之间的切换不会导致该接口的执行结果存在二义性。
5.5.4 常见不可重入的情况
调用了malloc/free函数因为malloc函数是用全局链表来管理堆的。调用了标准I/O库函数标准I/O库的很多实现都以不可重入的方式使用全局数据结构。可重入函数体内使用了静态的数据结构。
5.5.5 常见可重入的情况
不使用全局变量或静态变量。不使用用malloc或者new开辟出的空间。不调用不可重入函数。不返回静态或全局数据所有数据都有函数的调用者提供。使用本地数据或者通过制作全局数据的本地拷贝来保护全局数据。
5.5.6 可重入与线程安全联系
函数是可重入的那就是线程安全的。函数是不可重入的那就不能由多个线程使用有可能引发线程安全问题。如果一个函数中有全局变量那么这个函数既不是线程安全也不是可重入的。
5.5.7 可重入与线程安全区别
可重入函数是线程安全函数的一种。线程安全不一定是可重入的而可重入函数则一定是线程安全的。如果将对临界资源的访问加上锁则这个函数是线程安全的但如果这个重入函数若锁还未释放则会产生死锁因此是不可重入的。
6. 死锁
6.1 概念 死锁是指在一组进程中的各个进程均占有不会释放的资源但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。
6.2 死锁的四个必要条件
互斥条件一个资源每次只能被一个执行流使用。请求与保持条件一个执行流因请求资源而阻塞时对已获得的资源保持不放。不剥夺条件一个执行流已获得的资源在末使用完之前不能强行剥夺。循环等待条件若干执行流之间形成一种头尾相接的循环等待资源的关系。
6.3 避免死锁
破坏死锁的四个必要条件。加锁顺序一致。避免锁未释放的场景。资源一次性分配。
6.4 避免死锁的算法
死锁检测算法(了解)银行家算法了解
6.5 小问题 多个锁可能会出现死锁那么一个锁会出现死锁问题吗
7. 线程同步
7.1 条件变量
当一个线程互斥地访问某个变量时它可能发现在其它线程改变状态之前它什么也做不了。例如一个线程访问队列时发现队列为空它只能等待直到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。
7.2 同步概念与竞态条件
同步在保证数据安全的前提下让线程能够按照某种特定的顺序访问临界资源从而有效避免饥饿问题叫做同步。竞态条件因为时序问题而导致程序异常我们称之为竞态条件。在线程场景下这种问题也不难理解。
7.3 条件变量函数 初始化
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrictattr);
参数
cond要初始化的条件变量
attrNULL销毁
int pthread_cond_destroy(pthread_cond_t *cond)等待条件满足
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
参数
cond要在这个条件变量上等待
mutex互斥量后面详细解释唤醒等待
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);7.4 为什么 pthread_cond_wait 需要互斥量?
条件等待是线程间同步的一种手段如果只有一个线程条件不满足一直等下去都不会满足所以必须要有一个线程通过某些操作改变共享变量使原先不满足的条件变得满足并且友好的通知等待在条件变量上的线程。条件不会无缘无故的突然变得满足了必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护。没有互斥锁就无法安全的获取和修改共享数据。wait会将等待的线程进行解锁唤醒时再加锁。
8. 生产者消费者模型 在其中生产者和生产者之前是竞争关系也就是互斥的。因为超市的空间是有限的如果多个生产者放入商品情况就类似于我们上面所讲的抢票都在争先恐后的放入商品很容易造成商品溢出的情况。 消费者和消费者之间也就是互斥的比如如果某一个商品只剩一个而有多个消费者需要那么该给谁呢所以是互斥关系。 生产者和消费者之间既有互斥又有同步。 互斥是因为我们以hello world为例生产线程生产者就是向内存空间超市中写入hello world消费线程消费者从内存空间超市中读hello world但是如果此时生产线程只写了hello消费线程就开始读取就会只读取hello而不是需要的hello world就会出错所以需要互斥。 同步是因为如果一个商品目前还没有生产出来是缺货的但是有许多消费者来拿这个商品由于缺货消费者每次都是无功而返的而消费者每次进入超市拿商品时都是需要加锁的。由于没货一个个的消费线程每次来都会无功而返但是每次都要加锁。而此时如果这个消费线程的优先级很高每次这个线程都优先被调度进行加锁然后无功而返由于前面那个线程优先级很高其他线程就会一直申请不到锁资源那么就会产生饥饿问题。所以就需要生产者先生成商品消费者再来消费。还有一种理解同步的方式还是消费线程优先级非常高疯狂的在内存空间种访问有没有这个商品访问就会加锁而由于加锁了并且消费线程优先级还比生成线程高那么生产线程就一直无法向内存空间种放入商品进而就会产生饥饿问题所以需要同步 记忆方式321原则 33种关系 22个角色 1一个交易场所内存空间
8.1 为何要使用生产者消费者模型 生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯而通过阻塞队列来进行通讯所以生产者生产完数据之后不用等待消费者处理直接扔给阻塞队列消费者不找生产者要数据而是直接从阻塞队列里取阻塞队列就相当于一个缓冲区平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。
8.2 生产者消费者模型优点
解耦支持并发支持忙闲不均 生产者消费者之间有互斥也有同步而同步不是会将效率降低吗为什么会说它会加快效率。我们不能只从局部来看如果不是生产者消费者模型就是平时的代码是不是只有生产了一个数据才能处理而只有处理完了才能继续生产。 但是如果在生产者消费者模型中生产者和消费者之间是互不干涉的你在生产的同时我就可以进行消费我还没有消费完生产者一样可以继续生产内存空间充足的情况下这就是它所高效的地方。还可以更进一步加快就是基于环形队列来实现原理就是由于临界资源并不是一个整体是可以支持多个消费线程同时访问的相较于下面的阻塞队列它一次只允许一个消费线程访问临界资源因为它是将一个队列当成了一个整体
8.3 基于BlockingQueue的生产者消费者模型 在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于当队列为空时从队列获取元素的操作将会被阻塞直到队列中被放入了元素当队列满时往队列里存放元素的操作也会被阻塞直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的线程在对阻塞队列进程操作时会被阻塞)。
9. POSIX信号量
信号量本质是一把计数器。申请信号的本质就是预定资源。PV操作是原子的。 上面我们写的是将公共资源当一个整体使用因此同一时间只允许一个线程访问但是如果公共资源是多份呢也就是不将公共资源当作一个整体比如有一个数据开辟了7块空间每一个空间都是临界资源的不同区域。 此时最多就支持7个线程在同一时间进行访问但是最多也只能支持7个线程怎么避免发生7个以上的线程同时访问呢那就是信号量每次访问临界资源都要先申请信号量再访问临界资源的特定位置再释放信号量。 也就是说只要信号量申请成功了那么临界资源中一定就会有你的一份并不需要再次判断内存空间中还有没有资源一定会有的不然信号量就不可能申请成功。
9.1 认识接口 POSIX信号量和SystemV信号量作用相同都是用于同步操作达到无冲突的访问共享资源目的。 但POSIX可以用于线程间同步。 初始化信号量
#include semaphore.h
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数
pshared:0表示线程间共享非零表示进程间共享
value信号量初始值销毁信号量
int sem_destroy(sem_t *sem);等待信号量
功能等待信号量会将信号量的值减1
int sem_wait(sem_t *sem); //P()发布信号量
功能发布信号量表示资源使用完毕可以归还资源了。将信号量值加1。
int sem_post(sem_t *sem);//V()9.2 基于环形队列的生产者消费者模型 10. 线程池
11. 线程安全的单例模式
11.1 什么是单例模式 单例模式是一种 “经典的常用的常考的” 设计模式。
11.2 什么是设计模式 IT行业这么火涌入的人很多。俗话说林子大了啥鸟都有大佬和菜鸡们两极分化的越来越严重为了让菜鸡们不太拖大佬的后腿于是大佬们针一些经典的常见的场景给定了一些对应的解决方案这个就是设计模式。
11.3 单例模式的特点 某些类只应该具有一个对象(实例)就称之为单例。 例如一个男人只能有一个媳妇。在很多服务器开发场景中经常需要让服务器加载很多的数据 (上百G) 到内存中此时往往要用一个单例的类来管理这些数据。 对于单例模式在我写的另一篇C设计特殊类中有更加详细的介绍此处就不进行过多解释了。
12. STL智能指针和线程安全
12.1 STL中的容器是否是线程安全的吗? 不是原因是 STL 的设计初衷是将性能挖掘到极致而一旦涉及到加锁保证线程安全会对性能造成巨大的影响。而且对于不同的容器加锁方式的不同性能可能也不同(例如hash表的锁表和锁桶)。因此 STL 默认不是线程安全。如果需要在多线程环境下使用往往需要调用者自行保证线程安全。
12.2 智能指针是否是线程安全的? 对于 unique_ptr由于只是在当前代码块范围内生效因此不涉及线程安全问题。但是它指向的对象可能会存在线程安全问题。 对于shared_ptr多个对象需要共用一个引用计数变量所以会存在线程安全问题。但是标准库实现的时候考虑到了这个问题基于原子操作(CAS)的方式保证 shared_ptr 能够高效原子的操作引用计数。
13. 其他常见的各种锁
悲观锁在每次取数据时总是担心数据会被其他线程修改所以会在取数据前先加锁读锁写锁行锁等当其他线程想要访问数据时被阻塞挂起。乐观锁每次取数据时候总是乐观的认为数据不会被其他线程修改因此不上锁。但是在更新数据前会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式版本号机制和CAS操作。CAS操作当需要更新数据时判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败失败则重试一般是一个自旋的过程即不断重试。自旋锁公平锁非公平锁