2020校招备战日记5.9 ---- 线程内存模型,线程池,信号量,基于线程实现事件驱动,多核执行多线程,线程安全函数,可重入函数

0.目标完成情况

又摸了几天的鱼,终于完成了3天前的目标,再加了一点

  • 牛客两道题,最好录个视频
  • csapp收尾
  • unp开始看
  • 侯捷C++视频看一节

1. 学习回顾

1.1 线程内存模型

线程是在进程的上下文中运行的,虽然线程有自己的上下文。
进程的上下文内容包括:

  1. 代码
  2. 数据
  3. 共享库
  4. 程序计数器
  5. 通用目的寄存器值
  6. 打开文件表

当不涉及多线程的时候,其实进程当中就是一个线程,称为主线程。所谓多线程,就是这个主线程创建了对等线程,“对等”二字是为了强调这些线程和主线程之间不是“父子”关系,任何一个线程可以回收其他线程。

一个进程中的多个线程是各自有各自独立的线程上下文的。线程上下文包括:

  1. 栈指针
  2. 程序计数器
  3. 通用目的寄存器的值
  4. 线程ID

可以看到,当涉及多线程的时候,线程是有自己的栈的,以及自己的通用目的寄存器值。
总的来说,多个线程之间共享同一进程的全部虚拟地址空间,包括代码、数据、堆和共享库,注意,虽然栈看起来是线程私有的,但是其实不同的线程是可以以某种方式直接访问其他线程的栈的,只不过通常这不是我们想要的。而寄存器的值则永远不是共享的

由线程的内存模型可知,线程的上下文开销要更小,更加方便的共享同一进程内部的代码、数据、共享库以及打开文件表。因此线程的上下文切换的开销更小,而操作系统的调度单位正是线程。

1.2 线程池

在之前讨论的服务器中,每当有连接请求到达的时候,我们总是新创建一个线程,来执行任务。这个创建的过程比较耗时。我们可以提前创建好一些线程,然后让这些线程从一个缓冲区中读取已连接的描述符,这些线程就是消费者,而负责监听并返回已连接描述符的主线程则为生产者,利用这样的生产者-消费者模型,就可以实现事件驱动的服务器 ----------- 一旦有连接请求,就启动工作,没有连接请求,线程就被挂起。

而生产者-消费者模型的实现主要依赖于信号量。见下节描述。

1.3 信号量、事件驱动的服务器

信号量就是一种非负的全局变量。Linux对这种非负的全局变量有一组特殊的操作:P操作和V操作。

  • P操作,其实就是将指定的信号量-1, 如果该信号量已经是0了,那么该操作就会导致线程被挂起。这里的-1操作和普通的-1操作的区别在于:P操作是一种元操作,不会被中断。相应的,普通的+1操作是可能被中断的,因为普通的-1操作是包含3个步骤:加载,更新,存储,这里的中断的意思是,比方说,程序加载了一个变量,准备更新,但是还没有更新的时候,控制权被转移到了其他程序,过了一会,控制权又回来,继续执行更新和存储的操作,这不是我们想要的。我们的P操作,是“加载、更新、存储”一气呵成的,相当于cpu最底层的一条指令。
  • V操作, 其实就是将制定的信号量+1 。 同样的,这是一个元操作。此外,V操作会导致其他由于P操作挂起的线程被唤醒一个。

初值被设为1的信号量称为二元信号量,通常用来实现对临界资源的互斥访问,因此也成为互斥锁,对互斥锁的P操作,称为加锁,对互斥锁的V操作称为解锁。

初值被设为大于1的信号量被称为计数信号量,用来表征可用的共享资源的数量。

P操作和V操作这种挂起和唤醒的行为,结合互斥锁和计数信号量,使得我们很容易实现一种生产者-消费者模型。其中,互斥锁用于实现对缓冲区的互斥访问,计数信号量则用于标识可用空间的数目和可用项目的数目。

所谓生产者-消费者模型,就是一个缓冲区结构体 + 一组函数。这组函数包括对缓冲区的初始化、销毁、插入和删除。这个缓冲区作为全局变量的共享对象,这组函数在访问缓冲区的时候需要完成一些同步操作,就是获得缓冲区的互斥锁等一些PV操作。

有了这个模型,我们就可以实现事件驱动的服务器了,也就是说,一个线程可能会由于没有已连接描述符而被挂起,而一旦有了已连接描述符,主线程往缓冲区插入这个已连接描述符,这个插入操作中的V操作会唤醒一个消费者线程,也就是说这个消费者线程的取描述符操作不再被阻塞,从而可以执行任务。

1.4 多核执行多线程

多线程不一定需要多核,但是多核是实现多线程并行的唯一方式,注意,不是并发。

执行多核并行处理程序是一门单独的艺术(技术),他有一个任务分发的问题,主线程在创建多个对等线程的时候,会分配线程id,而线程函数则根据自己的线程id来“领取”数据,执行处理过程,最后主线程再将各个线程的结果做合并,从而得到最终结果。

多线程程序的执行由于需要完成一些同步操作,这些操作的开销不容小觑,在处理不当的情况下,可能多线程的并行程序反而比单线程更慢。

衡量多线程程序优劣有一些指标,除了绝对的运行时间外,还有加速比和效率。其中加速比主要描述将程序并行化带来的时间提升。

1.5 线程安全函数

“线程安全”是一个形容词,修饰函数的。当且仅当多个线程重复调用一个函数,而这个函数总是能够产生正确的结果的时候,我们称这个函数是线程安全的。

我们当然希望所有的函数都是线程安全的,但是事实并不如此。非线程安全的函数可以分为4类:

  1. 不使用PV操作来保护共享对象的函数。这样,多个线程以任意顺序执行并访问共享变量的时候,就可能会出现读写错误,从而不能得到正确的结果。例如,两个线程都对一个共享变量执行+1操作,那么执行完后,可能最终结果并不是+2. (这里要补充共享对象的概念:可以被多个线程引用的对象,可以是全局变量,静态变量等。)
  2. 每次执行结果依赖于上一次执行的函数。在多线程的时候,由于多个线程都会执行这个函数,而线程彼此的执行次序是未知的,因此“上一次”执行的是哪个步骤就会无法确定。例如rand函数,他生成的每个数字依赖于上一次调用rand的结果,而上一次调用rand可能发生在另一个线程中,而这并不是我们想要的。因此rand函数并不是线程安全的。
  3. 用静态变量来存储返回值的函数。在一个线程中执行该函数,在返回之前,可能跳转到另一个线程,也执行了这个函数,并且将结果也存储在了这个静态变量中,那么第一个线程的执行过程就白费了,这不是我们想要的。要将这种非线程安全函数改变成线程安全函数其实很简单,只要用一组PV操作将整个函数包起来,然后用局部变量来转储结果,即可得到一个线程安全的版本。缺点是严重影响效率。
  4. 调用非线程安全函数的函数。注意,这种函数并不一定就是非线程安全的,典型的例子就是上一条说的,我们正是利用这种调用来实现从非线程安全到线程安全的转化。但是,如果调用的是第2种非线程安全的函数,那么无论如何操作,这个函数也还是非线程安全的。
1.6 可重入函数

可重入函数是线程安全函数的特例。从前面的介绍可以发现,函数的非线程安全都是和共享变量有关系,那么,如果一个函数不涉及任何共享变量,那么他每次执行的结果就一定是可以预见的,因此是绝对的线程安全,我们称之为可重入函数。

如何判断一个函数是否可重入?两种方法:

  1. 如果一个函数的参数不涉及引用或指针,并且内部不涉及任何共享变量,那么肯定是可重入的,这叫做显式可重入。
  2. 如果一个函数的参数有引用或指针,并且内部不涉及任何共享变量,那么也可能是可重入的,需要小心处理。这时候的可重入称为隐式可重入。

明日目标

  1. unp第2章和第3章看完。因为这部分知识比较基础,之前都看过了。复习起来应该比较快,主要解答一些疑惑就够了。大概过一遍,能看懂,后面忘了再回来查。
  2. 看看面经吧,毕竟要面试了,同时也涨涨视野。
  3. LeetCode一道题。
    任务有点多,早睡早起!晚安~
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值