Linux系统编程之线程(一)

介绍系统编程中线程的概念以及线程的特点。

前言

我们知道进程是操作系统管理资源的基本单位,而线程又是进程的一部分。所以我们可以说线程是操作系统管理资源的最小单位。

线程相较于进程来说还是有很多好处的:

  1. 线程消耗的资源要少于进程
  2. 程序逻辑和控制方式简单

同时其也有相应的缺点:

  1. 线程之间的同步和通信复杂
  2. 由于处于多个进程之下所以,线程的内存大小受到了限制
  3. 一个线程的崩溃可能影响到整个程序的稳定性

什么是线程池?。线程池是一种线程的管理方式,我们知道在创建线程是需要时间的,所以我们在进程启动的时候就创建好多个线程,这些线程就被放到一个线程池中,当有新的任务需要处理时,就从线程池中取出一个线程来处理,这样就避免了频繁的创建和销毁线程,提高了程序的运行效率。

线程的特点

线程不同于进程的主要一个原因就是线程实际上是附属于进程的,我们也可以说线程是轻量级的进程。同时同一个进程下的线程共享了下面这些内容:

  1. 整个进程的内存空间
  2. 打开的文件描述符
  3. 信号处理器
  4. 整个进程的地址空间

同时,不同的线程也有自己的资源:

  1. 每个线程都有自己的栈
  2. 程序计数器
  3. 局部变量

下面有一个实验来证明,pthread_create() 函数创建线程的速度要远快于使用函数 fork() 创建进程的速度。

实验环境:计时反映 50,000 个进程/线程创建,使用 time 实用程序执行,单位为秒,无优化标志。

下面是关于线程共享资源方面的内容:

还需要注意,同一个进程下的不同线程除开共享整个内存区之后还会共享 OS资源 比如说:打开的文件描述符、信号等等

进程是操作系统的资源分配单位

线程是CPU的基本执行单元

并发和并行

并发和并行是两个概念,并发是指两个或多个事件在同一时间间隔发生,并行是指两个或多个事件在同一时刻发生。

实际上可以这样理解:

并发:意味着应用程序会执行多个的任务,但是如果计算机只有一个 CPU 的话,那么应用程序无法同时执行多个的任务,但是应用程序又需要执行多个任务,所以计算机在开始执行下一个任务之前,它并没有完成当前的任务,只是把状态 暂存,进行任务切换,CPU 在多个任务之间进行切换,直到任务完成。如下图所示

并行:是指应用程序将其任务分解为较小的子任务,这些子任务可以并行处理,例如在多个CPU上同时进行。

并发为什么会出现?下面是计算机中不同存储器的访问速度:

程序是在内存中执行的,程序里大部分语句都要访问内存,有些还需要访问 I/O 设备,根据漏桶理论来说,程序整体的性能取决于最慢的操作也就是磁盘访问速度。

因为 CPU 速度太快了,所以为了发挥 CPU 的速度优势,平衡这三者的速度差异,计算机体系机构、操作系统、编译程序都做出了贡献,主要体现为:

  • CPU 使用缓存来中和和内存的访问速度差异
  • 操作系统提供进程和线程调度,让 CPU 在执行指令的同时分时复用线程,让内存和磁盘不断交互,不同的 CPU 时间片 能够执行不同的任务,从而均衡这三者的差异
  • 编译程序提供优化指令的执行顺序,让缓存能够合理的使用

用户级线程和内核级线程

线程是具有以上区分的,常见的用户级线程库包括:

  • Pthread
  • Cthread
  • Solaris UI-threads
  • Windows 线程库

用户级线程库为线程创建、终止、联接和调度提供所有支持。

同时由于具有这两种区分,所以操作系统也提供了多种线程模型:

  • 一对一模型:表示一个用户级线程对应一个内核级线程,现在 Linux 、MacOS 等操作系统都采用这种模型
  • 多对一模型:表示多个用户级线程对应一个内核级线程
  • 多对多模型:表示多个用户级线程对应多个内核级线程

具体的:

多对一:

一对一:

多对多:

协程

协程是一种用户态的轻量级线程,协程的调度完全由用户控制,因此,协程能充分利用线程的优点,但又不受线程的缺点的限制。

协程这个概念近年流行起来。尤其 golang 语言问世之后,内置的协程特性,完全屏蔽了操作系统线程的复杂细节;甚至 go 开发者“只知有协程,不知有线程”

内核线程

  • 内核线程由内核直接支持。内核在内核空间中执行线程创建、终止、加入和调度。
  • 内核线程通常比用户线程慢。
  • 但是,阻塞一个线程不会导致同一进程的其他线程阻塞。内核只运行其他线程。
  • 在多处理器环境中,内核可以在不同的处理器上调度线程

由内核管理的线程包(注意:POSIX Pthreads 库支持创建内核线程)

其他线程库

其实我们知道线程库是非常多的,在不同的 OS 中或提供不同的线程库,包括有些编程语也会进行提供:

  • Java 线程库
  • C/C++ 线程库
  • Windows 线程库
  • Linux 线程库
    等等。

Linux 线程库

  • Linux 将它们称为任务而不是线程。
  • 线程创建是通过 clone() 系统调用完成的。
  • Clone() 允许子任务共享父任务(进程)的地址空间fork () 和 clone() 有什么区别?

但是我们需要知道,clone() 函数是不具有可移植性的,所以我们一般不会使用这个函数,而是使用 pthread_create() 函数来创建线程。

Pthread线程库

相关函的定义:

1
2
3
4
5
6
7
8
9
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
int pthread_join(pthread_t thread, void **retval);
int pthread_detach(pthread_t thread);
int pthread_cancel(pthread_t thread);
void pthread_testcancel(void);
void pthread_cleanup_push(void (*routine)(void *), void *arg);
void pthread_cleanup_pop(int execute);

现在一一介绍相关的函数作用:

  • pthread_create() 函数用于创建线程,其参数如下:
    • thread:指向 pthread_t 类型的指针,用于存储线程 ID。
    • attr:指向 pthread_attr_t 类型的指针,用于设置线程属性。使用参数 NULL 时,系统会使用默认的线程属性。
    • start_routine:线程函数的入口地址。
    • arg:线程函数的参数。
  • pthread_join() 函数用于等待线程终止。其参数如下:
    • thread:pthread_t 类型,表示要等待的线程 ID。
    • retval:指向 void* 类型的指针,用于存储线程的返回值。
  • pthread_detach() 函数用于分离线程,使线程脱离父进程的控制。其参数如下:
    • thread:pthread_t 类型,表示要分离的线程 ID。
  • pthread_cancel() 函数用于取消线程的执行。其参数如下:
    • thread:pthread_t 类型,表示要取消的线程 ID。
  • pthread_testcancel() 函数用于测试是否有线程取消请求。
  • pthread_cleanup_push() 函数用于注册线程清理函数,当线程终止时,会自动调用清理函数。其参数如下:
    • routine:线程清理函数的入口地址。
    • arg:线程清理函数的参数。
  • pthread_cleanup_pop() 函数用于弹出线程清理函数。其参数如下:
    • execute:如果为 0,则清理函数不会被调用。如果为 1,则清理函数会被调用。

杂项

  1. 后续 Pthreads 系列函数均以 0 表示成功,返回一个正值表示失败,这个正值与传统 Unix 的 errno 的值含义一样。在编译调用了 Pthreads 函数的程序需要添加 -lpthread 以链接此库。

参考资料

-Java之编程—并发编程


Linux系统编程之线程(一)
https://ysc2.github.io/ysc2.github.io/2024/08/17/Linux系统编程之线程-一/
作者
Ysc
发布于
2024年8月17日
许可协议