本章问题:什么是线程?线程的使用场景?什么是线程池?线程池是如何工作的?线程池共享了哪些资源?线程安全代码怎么写?什么是线程安全?
什么是线程?
线程是为了提高进程的效率。进程的地址空间中保存了cpu执行的机器指令以及函数运行时的堆栈信息,要想让程序运行起来,就要不拿main函数的第一条机器指令地址写入pc寄存器,从而形成一个指令的执行流。
进程的缺点在于只有一个入口函数,就是main()函数。因此进程中的机器指令一次只能被一个cpu执行,有没有办法让多个cpu执行同一个进程中的机器指令呢?
当然,pc寄存器既然可以指向main()函数,也可以指向其他函数,从而创建一个新的执行流。
至此,一个进程内可以有多个入口函数,也就是一个进程存在多个执行流。
更关键的是,这些执行流共享同一个进程地址空间,因此也不再需要进程间通信了。
对于每一条执行流,我们都称其为“线程”。
多线程与内存布局
函数在执行时依赖的信息包括函数参数,局部变量,返回地址等信息,这些信息都被保存在相应的栈帧中。每个函数在运行时都有自己的栈帧,随着函数的调用,以及返回,这些栈帧按照先进后出的顺序增长或减少。栈帧的增长或减少形成地址进程空间中的栈区。
在线程这个概念没有出现时,一个进程中只有一个执行流,因此只有一个栈区,现在有了线程之后就有了多个执行流,每个执行流都要有自己的栈区。
言而总之,想说的就是创建线程是要消耗进程内存看空间的,这一点值得注意。
线程的使用场景(引出线程池的概念)
从生命周期来看,线程要处理的任务有两种:长任务,短任务。
对于长任务没有什么好说的。收到哦一个请求就创建一个线程来处理任务,处理完成之后销毁该栈帧即可。
对于短任务来说,短任务的生命周期短,如一次网络请求一次数据库查询等。这类任务往往判断着一个特点:量大。
(1)对于每次收到一个短任务就创建一个线程,等周期结束再消耗栈帧,必然会消耗大量的时间。(线程的创建和销毁是会消耗时间的)
(2)每个线程都有独立的栈区,当创建大量线程时会消耗过多的内存。
(3)大量线程会使线程的切换开销增加。
因此我们可以提前创建一批线程,有任务就交给做这些线程进行处理。不需要频繁的创建,销毁,没任务就让这些线程的任务队列中阻塞等待。
线程池是如何工作的?
有了任务,有了线程,有了线程池,怎么把任务提交给线程池中的线程呢?
我们用数据结构中的队列来维护线程池。
在没有任务的时候线程池中的线程会在任务队列中阻塞等待,当生产者向任务队列中写入数据课后,线程池中的某个线程会被唤醒,该线程会从任务队列中取出上述结构体并执行该结构体中handle指向的处理函数:
线程到底共享了哪些进程资源?
在讨论线程共享了哪些资源前我们先想想线程都有哪些资源。
线程其实就是函数的执行,那么函数的执行都有哪些信息呢?
函数在运行时信息保存在栈帧中,栈帧组成了栈区,栈帧中保存了函数的返回值,调用其他函数的参数,该函数使用的局部变量及该函数适用的寄存器信息等。
所属线程的栈区,栈指针,程序计数器,以及执行函数时使用的寄存器信息都是线程私有的。
剩下的堆区,数据区,代码区都是共享的:
共享资源
代码区
代码区保存的是程序员写的代码,更准确的是编译后生成的可执行机器指令。
这些机器指令被存放在可执行程序中,程序启动时加载到进程的地址空间中。
线程之间共享代码区。
注意:
代码区只是只读的 ,任何线程在程序运行期间都不能修改代码区。
原因:
为了线程安全。
数据区
数据区存放全局变量。
所有线程都可以共享该全局变量。
堆区
只要知道变量的地址(指针),任何一个线程都可以访问指针指向的数据。
栈区
我们前面说栈区是线程的私有区,为什么这里又说栈区可以被线程共享呢?
首先我们要知道前面说的栈区是私有的是针对不同进程的线程之间来说的。因为不同进程的地址空间是相互隔离的,没有办法直接访问属于另一个进程地址空间中的数据。但是同一个进程的不同线程之间没有这种保护,因为一个线程可以访问另一个线程的栈区。
什么是线程安全?线程安全的代码到底是怎么编写的?
线程安全的定义:给定一段代码,不管其在多少个线程中被调用,也不管这些线程按照什么顺序调用,当其都能给出正确结果时,我们就称这段代码是线程安全的。
例如上面的代码段,像这种无论用多少线程同时调用,怎么调用,什么时候调用都会返回2,这段代码就是线程安全的。
线程安全的代码怎么编写:(1)线程使用自己的私有资源,能实现线程安全。
(2)线程使用共享资源时,必须要保证不影响其他线程。
只使用线程私有资源
最简单的,只要我们写一段代码不依赖任何全局变量,不依赖任何函数,且使用的局部变量都是线程的私有资源,那么写的代码就一定是线程安全,例如:
但是,在实际开发中我们肯定会调用外部函数和全局变量的,因此,我们重点还是要看一下使用共享资源时如何写出线程安全的代码。
主要分为几下一个点:
线程私有资源+函数参数
当传入参数为指针时要注意:
使用全局变量
如果写的代码没有修改全局变量,那么写出来的代码就是线程安全的,否则就不是。
线程局部存储
线程的局部存储是在全局变量前_thread关键词,此时线程就是安全的了。
函数返回值
如果函数返回的是值,一般都是安全的,如果函数返回的是指针,可能比较危险。