linux系统编程——线程

1.线程是什么?

1.1 线程和进程

进程:二进制程序的抽象,包括:加载的二进制程序,虚拟内存,内核资源(如打开的文件),关联用户等
线程:进程内的执行单元,包括:虚拟处理器,堆栈,程序状态。

进程是运行的二进制程序,线程是操作系统调度器可以调度的最小单元

1.2 虚拟内存和虚拟处理器

现代os包括两种用户空间基础抽象:虚拟内存 虚拟处理器

虚拟内存是进程相关的,与线程无关。所以每个进程有独立的内存空间,进程内所有线程共享这份空间。

虚拟处理器是线程相关的,与进程无关,每个线程都是独立调度的实体,支持单个进程并行处理多个工作。

2. 线程的优缺点

2.1 线程的优点

  • 编程抽象
    把工作分成多个模块,每个分块分配一个执行单元(线程)。
    利用这种方法的设计模式包括 "每个连接一个线程" 和 线程池模式。
    Alan Cox说 : 线程是为那些不会用状态机编程的人设计的。也就是说,理论上,所有线程可以解决的问题都可以通过状态机解决。
  • 并发性
    对于多处理器计算机,线程提供了真正并发的高效方式。
    每个线程有自己的虚拟处理器,多个处理器上可以同时运行多个线程,提供系统的吞吐量。
    由于线程的并发性,所以 线程是为那些不会用状态机编程的人设计的 的说法不成立。
  • 提高响应能力
    即使单处理器上,多线程也可以提高进程的响应能力。
    一个长时间运行的任务会影响应用对用户输入的响应,导致应用看起来像死了。有了多线程,可以将这些操作委托给workder进程,至少有一个线程可以响应用户输入并执行UI操作。
  • IO阻塞
    IO阻塞会影响整个进程。多线程,异步IO,非阻塞IO可解决此问题
  • 上下文切换
    进程上下文切换开销大,线程上下文切换开销小
  • 内存保存
    线程可以共享内存,同时利用多个执行单元的高效方式。

总之,虽然线程效果可以使用其他技术实现,但是多核处理器的发展让线程无可替代。

2.2 上下文切换

进程间切换 开销大于 进程内切换,
在非linux系统上非常明显,在linux系统上,进程切换代价不高,但线程切换成本接近0.

计算机体系结构对进程切换有影响,但线程不存在此问题,因为进程切换涉及把一个虚拟地址空间切换到另一个虚拟地址空间。如x86系统上,当切换时,TLB必须清空。在某些场景下,TLB丢失对系统有极大影响,有些ARM机器上,必须把整个cpu缓存清空。但对于线程,不存在这些代价。因为线程到线程切换不会交出虚拟地址空间。

2.3 线程的缺点

多线程的代价:bug。多线程程序的设计,编写,理解,调试 的复杂度远高于单线程。

  • 同步开销和设计
    由于线程并发使用同一块内存,时间和顺序不可预测,容易输出脏数据。由于理解和调试多线程代码非常困难,所以线程模型和同步策略必须从一开始就是系统设计的一部分。
  • 不稳定性
    由于线程开发容易出bug,而一个线程的bug会导致整个进程死掉。

2.4 是否应该使用多线程

多线程可以使用其他技术代替,这取决于使用多线程目的。
希望低延迟高IO吞吐,可以用IO多路复用,非阻塞IO,异步IO实现。这些技术支持进程并发IO操作,不阻塞进程。
希望实现并发,使用N个进程也可以同样利用处理器资源,除了增加资源消耗代价和上下文切换开销。
希望共享内存,linux提供了一些IPC,比起多线程,他们有更严格的方式共享内存。

虽然有很多技术能代替线程,但是上下文切换的优势是无法替代的。因此多线程很常见。如底层内核,高层GUI。

总之,若技术强则使用多线程,否则使用多进程。
比如ngx使用多进程(主进程做管理,子进程负责工作) envoy使用多线程(主线程做管理,其他线程做工作),结果envoy性能强于ngx。
当然apache也使用多线程,但他的线程模型是一个连接对应一个线程(同步IO),所以连接数多了,内存消耗大(一个线程增加8M内存消耗),而envoy使用多线程和异步IO,所以一个工作线程服务多个连接。

3. 线程模型

3.1 1:1 线程模型

内核线程数和用户线程数 1:1.
即线程库通过 clone() 创建新线程,返回给进程,直接作为用户空间线程。

3.2 N:1 线程模型

也叫用户级线程,即 N个线程映射到一个内核进程。
该模型由用户空间进行管理调度。
优点:上下文切换成本几乎为0
缺点:内核线程只有一个,无法利用多个处理器

3.3 N:M 混合模型

太多复杂,已放弃

3.4 协程

和用户线程模型类型,属于用户空间,但是不存在用户空间对协同程序的调度。
他们是协作式调度,支持显示放弃一个程序而去执行另一个(这里的程序指普通c/c++函数)。
协同程序更侧重于程序流控制,而不是并发。

linux本身不支持协同程序,可能是其上下文切换已经非常快,不需要比内核线程性能更好的结构。
Go为linux提供类似协同程序的支持。

4. 线程模式

核心多线程编程模式:

  • 每个连接对应一个线程
  • 事件驱动(多个连接对应一个线程)

4.1 每个连接对应一个线程

该模式又有两种工作方式:

  • 线程在服务一个连接时,不会服务其他连接(线程池)
  • 线程服务一个连接,并运行到结束

该模式下使用同步IO

该模式通常需要设置线程数上限,若正在执行的线程数到达上限,新的连接可能被加入队列,也可能被拒绝服务,
直到正在执行的连接数下降到上限值以下。

该模式本身不需要线程,可用 进程 替换 线程。如 apache就这么做的。

事件驱动的线程模式

增加线程是有固有成本的,主要是内核和用户空间栈。所以线程数必须有上限(尤其是32位系统)。
并且 线程数 超出系统处理器个数,并不会带来任何好处。

所以使用 IO多路复用和回调函数。

该模式本身也不需要线程。

事件驱动模式是设计多线程服务器的最佳选择,例如当前除了apache外,其他服务器都是事件驱动模式。
设计多线程系统时首先考虑事件驱动:异步IO,回调,事件循环,一个很小的线程池(每个处理器一个线程)

5. 并发和竞争

x++;
的汇编代码可能如下:

若两个线程并发执行x++,x=5,以下是可能的输出
正确的情况

脏数据的情况

所以即使对变量加一操作——一行代码,一旦有多个线程并发执行,都会充满竞争条件。

6.同步

解决竞争的方法是 实现临界区原子性访问。

常见的方法是 锁。
锁的要领是 锁数据,而不是代码。 不要说锁保护这个函数,而是将数据和锁关联起来。
原因是 锁和代码关联时,锁的语义就很难理解,随着时间的变化,锁和数据之间的关系就不清晰了,程序员会引入新数据,而忘记相关锁。
把锁和数据关联,这个映射就会很清晰。

6.1 死锁

当两个线程互相持有对方需要的锁,就导致死锁。

要避免死锁,就要在 设计时 定义好锁,比如常见死锁为 ABBA ,即 一个线程获得A,一个线程获得B,然后死锁了。
解决方式是,必须总是先获得A,然后获得B.

7. pthread

linux内核只为线程提供底层原语clone,多线程库在用户空间。 posix实现为 Pthreads

pthreads标准是一堆文字描述,标准实现是glibc提供,glibc提供了两种phthreads实现:linuxThreads 和 NPTL
NPTL是优越的线程库,是linux2.6和glibc2.3中引入。

7.1 pthread API

phtread API 定义了构建一个多线程程序的方方面面,有100多个接口,由于过于庞大和丑陋,有不少骂声,但是它依然是核心线程库,并且很多线程库都是构建在phtread上的。

posted on 2021-08-25 13:57  开心种树  阅读(123)  评论(0编辑  收藏  举报