Linux系统编程之线程(一)
介绍系统编程中线程的概念以及线程的特点。
前言
我们知道进程是操作系统管理资源的基本单位,而线程又是进程的一部分。所以我们可以说线程是操作系统管理资源的最小单位。
线程相较于进程来说还是有很多好处的:
- 线程消耗的资源要少于进程
- 程序逻辑和控制方式简单
同时其也有相应的缺点:
- 线程之间的同步和通信复杂
- 由于处于多个进程之下所以,线程的内存大小受到了限制
- 一个线程的崩溃可能影响到整个程序的稳定性
什么是线程池?。线程池是一种线程的管理方式,我们知道在创建线程是需要时间的,所以我们在进程启动的时候就创建好多个线程,这些线程就被放到一个线程池中,当有新的任务需要处理时,就从线程池中取出一个线程来处理,这样就避免了频繁的创建和销毁线程,提高了程序的运行效率。
线程的特点
线程不同于进程的主要一个原因就是线程实际上是附属于进程的,我们也可以说线程是轻量级的进程。同时同一个进程下的线程共享了下面这些内容:
- 整个进程的内存空间
- 打开的文件描述符
- 信号处理器
- 整个进程的地址空间
同时,不同的线程也有自己的资源:
- 每个线程都有自己的栈
- 程序计数器
- 局部变量
下面有一个实验来证明,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 |
|
现在一一介绍相关的函数作用:
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,则清理函数会被调用。
杂项
- 后续 Pthreads 系列函数均以 0 表示成功,返回一个正值表示失败,这个正值与传统 Unix 的 errno 的值含义一样。在编译调用了 Pthreads 函数的程序需要添加
-lpthread
以链接此库。