更新时间: 2018-01-11 21:09:41       分类: 系统编程


线程

基础概念

线程是进程内的执行单元(比进程更低一层的概念),具体包括 虚拟处理器,堆栈,程序状态等。

可以认为 线程是操作系统调度的最小执行单元。

现代操作系统对用户空间做两个基础抽象:虚拟内存和虚拟处理器。这使得进程内部“感觉”自己独占机器资源。

虚拟内存

系统会为每个进程分配独立的内存空间,这会让进程以为自己独享全部的RAM。

但是同一个进程内的所有线程共享该进程的内存空间。

虚拟处理器

这是一个针对线程的概念,它让每个线程都“感觉”自己独享CPU。实际上对于进程也是一样的。

多线程

多线程的好处

多线程的代价

调试难度极大。

在同一个内存空间内并发性的读写操作会引发多种问题(如脏数据),对多进程情景下的资源同步变得困难,而且多个独立运行的线程其时间和顺序具有不可预测性,会导致各种各样奇怪的问题。

这一点可以参考并发带来的问题。

线程模型

线程的概念同时存在于内核和用户空间中。

内核级线程模型

每个内核线程直接转换成用户空间的线程。即内核线程:用户空间线程=1:1

用户级线程模型

这种模型下,一个保护了n个线程的用户进程只会映射到一个内核进程。即n:1。

可以减少上下文切换的成本,但在linux下没什么意义,因为linux下进程间的上下文切换本身就没什么消耗,所以很少使用。

混合式线程模型

上述两种模型的混合,即n:m型。

很难实现。

*协同程序

‌提供了比线程更轻量级的执行单位。

线程模式

每个连接对应一个线程

也就是阻塞式的I/O,实际就是单线程模式

线程以串行的方式运行,一个线程遇到I/O时线程必须被挂起等待直到操作完成后,才能再继续执行。

事件驱动的线程模式

单线程的操作模型中,大部分的系统负荷在于等待(尤其是I/O操作),因此在事件驱动的模式下,把这些等待操作从线程的执行过程中剥离掉,通过发送异步I/O请求或者是I/O多路复用,引入事件循环和回调来处理线程和I/O之间的关系。

有关I/O的几种模式,参考这里

简要概括一下,分为四种:

并发,并行,竞争!

并发和并行

并发,是指同一时间周期内需要运行(处理)多个线程。

并行,是指同一时刻有多个线程在运行。

本质上,并发是一种编程概念,而并行是一种硬件属性,并发可以通过并行的方式实现,也可以不通过并行的方式实现(单cpu)。

竞争

并发编程带来的最大挑战就是竞争,这主要是因为多个线程同时执行时,执行结果的顺序存在不可预料性

解决竞争的手段:同步

简要的说,就是在会发生竞争的资源上,取消并发,而是采用同步的方式访问和操作。

最常见的,处理并发的机制,就是锁机制了,当然系统层面的锁比DBMS等其他一些复杂系统的锁要简单一些(不存在共享锁,排他锁等一些较为复杂的概念)。

但是锁会带来两个问题:死锁饿死

解决这两个问题需要一些机制以及设计理念。具体有关锁的部分可以参考DBMS的并发笔记。

关于锁,有一点要记住。

锁住的是资源,而不是代码

编写代码时应该切记这个原则。

系统线程实现:PThreads

原始的linux系统调用中,没有像C++11或者是Java那样完整的线程库。

整体看来pthread的api比较冗余和复杂,但是基本操作也主要是 创建、退出等。

需要留意的一点是linux机制下,线程存在一个被称为joinable的状态。下面简要了解一下:

Join和Detach

这块的概念,非常类似于之前父子进程那部分,等待子进程退出的内容(一系列的wait函数)。

linux机制下,线程存在两种不同的状态:joinableunjoinable

如果一个线程被标记为joinable时,即便它的线程函数执行完了,或者使用了pthread_exit()结束了该线程,它所占用的堆栈资源和进程描述符都不会被释放(类似僵尸进程),这种情况应该由线程的创建者调用pthread_join()来等待线程的结束并回收其资源(类似wait系函数)。默认情况下创建的线程都是这种状态。

如果一个线程被标记成unjoinable,称它被分离(detach)了,这时候如果该线程结束,所有它的资源都会被自动回收。省去了给它擦屁股的麻烦。

因为创建的线程默认都是joinable的,所以要么在父线程调用pthread_detach(thread_id)将其分离,要么在线程内部,调用pthread_detach(pthread_self())来把自己标记成分离的。


评论

还没有评论