多线程编程的概念
理解Linux线程
相对于Unix操作系统40多年的光辉历史,线程算是出现得比较晚的。
在20世纪90年代线程才慢慢流行起来,而POSIX threads标准的确立已经是1995年的事情了。
Unix原本是不支持线程的,线程概念的引入给Unix家族带来了一些麻烦,很多函数都不是线程安全(thread-safe)的,需要重新定义,信号机制在线程加入以后也变得更加复杂了。
在单核CPU时代,多线程的需求并没有那么强烈,但是随着时间的流逝,事情发生了变化。2005年3月,Herb Sutter在Dobb’s Journal上发表了《The Free Lunch is over:A Fundamental Turn Toward Concurrency in Software》一文
文章分析处理器厂商改善CPU性能的传统方法,如提升时钟速度和指令吞吐量,基本已经走到了尽头,处理器开始向超线程和多核架构靠拢,多核的时代已然来临。
为了让代码运行得更快,单纯地依赖更快的硬件已经无法满足要求。
程序员需要编写并发代码,以便充分发挥多核处理器的强大功能,并且使程序的性能得到提升。
线程与进程
在Linux下,程序或可执行文件是一个静态的实体,它只是一组指令的集合,没有执行的含义。
进程是一个动态的实体,有自己的生命周期。
线程是操作系统进程调度器可以调度的最小执行单元。
进程之间,彼此的地址空间是独立的,但线程会共享内存地址空间。
同一个进程的多个线程共享一份全局内存区域,包括初始化数据段、未初始化数据段和动态分配的堆内存段——共享变量,也称为共享资源。
- 有了进程,为什么还要多线程?
- 多线程编程有哪些优点?
- 多线程编程主要用在什么地方?
- SMP、NUMA、MPP
- 多核、4核8线程
进程、线程、协程
这种共享给线程带来了很多的优势,但是处理起来也复杂来许多:
·创建线程花费的时间要少于创建进程花费的时间。
·终止线程花费的时间要少于终止进程花费的时间。
·线程之间上下文切换的开销,要小于进程之间的上下文切换。
·线程之间数据的共享比进程之间的共享要简单。
下面用一个简单的实验,来比较下创建10万个进程和10万个线程各自的开销。
创建进程的测试程序将会执行如下操作:
1)调用fork函数创建子进程,子进程无实际操作,调用exit函数立刻退出,父进程等待子进程退出。
2)重复执行步骤1,共执行10万次。
创建线程的测试程序则执行如下操作:
1)调用pthread_create创建线程,线程无实际操作;调用pthread_exit函数,立刻退出;主线程调用pthread_join函数等待线程退出。
2)重复执行步骤1,共执行10万次。
线程间的上下文切换,指的是同一个进程里不同线程之间发生的上下文切换。
由于线程原本属于同一个进程,它们会共享地址空间,大量资源共享,切换的代价小于进程之间的切换是自然而然的事情。
线程的弊端
多线程带来优势的同时,也存在一些弊端。
1)多线程的进程,因地址空间的共享让该进程变得更加脆弱多个线程之中,只要有一个线程不够健壮存在bug(如访问了非法地址引发的段错误),就会导致进程内的所有线程一起完蛋。正所谓:
覆巢之下,安有完卵
城门失火,殃及池鱼
相比之下,进程的地址空间互相独立,彼此隔离得更加彻底。
多个进程之间互相协同,一个进程存在bug导致异常退出,不会影响到其他进程。
2)线程模型作为一种并发的编程模型,效率并没有想象的那么高,会出现复杂度高、易出错、难以测试和定位的问题
目前存在的并发编程,基本可以分成两类:
共享状态式
消息传递式
线程模型采用的是第一种。从现在开始,停止幻想,欢迎来到真实的世界。
一个程序员碰到了一个问题,他决定用多线程来解决。现在两个他问题了有。
——关于线程的冷笑话
在真实的场景中,多线程编程是很复杂的。前面所说的多个任务并行不悖,互不依赖,在大多数情况下只是一种美好的幻想。
首先,多个线程之间,存在负载均衡的问题,现实中很难将全部任务等分给每个线程。想象一下,如果存在10个线程,一个线程承担了90%的任务,9个线程承担了10%的任务,整体的效率立刻就降了下来。
- 提高程序运行效率
- 模块细分,防止程序阻塞
- 高并发、多核、服务器
- 线程池、协程
准备工作
-
Pthread线程库
- 线程的实现:windows、Linux
- Pthread库:POSIX标准中的thread API
- Glibc与LinuxThread
- Glibc和NPTL
- Sgetconf GUN_LIBPTHREAD_VERSION
Linux的API
对象 操作 LINUX Pthread API 线程 创建
退出
等待pthread_create
pthread_exit
pthread_join互斥锁 创建
销毁
加锁
解锁pthread_mutex_init
pthread_mutex_destroy
pthread_mutex_lock
pthread_mutex_unlock条件变量 创建
销毁
触发
广播
等待pthread_cond_init
pthread_cond_destroy
pthread_cond_signal
pthread_cond_broadcast
pthread_cond_wait读写锁 创建
等待
销毁pthread_rwlock_init
pthread_rwlock_rdlock
pthread_rwlock_destroy
使用pthread库
- 安装man手册
- apt install glibc-doc manpages-posix-dev
- 程序的编译
- gcc main.c -lpthread
- /usr/lib/libpthread.a
- pthread常用API
在目录下查找文件
find -name libpthread.a
创建一个线程
API接口说明
函数原型
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
-函数功能:创建一个线程
-参数说明:
-thread:指向线程ID的指针
-attr:线程属性
-start_routine:线程执行实体入口
-arg:传递给线程的参数
-typedef unsigned long int pthread
当pthread_create成功返回时,新创建线程的线程ID会被设置成thread所指向的内存单元
attr参数用于制定各种不同的线程属性
新创建的线程从start_routine函数的地址开始运行,该函数只有一个无类型指针参数arg。如果需要向start_rtn函数传递一个以上参数,需要把这些参数放在一个结构体中。
线程的终止
- 线程终止的三种方式
- 从start_routine正常return
- 显示调用pthread_exit
- 函数原型:void pthread_exit(void *retval);
- 返回值通过参数retval传递
- 线程被pthread_cancel取消
单个线程可以通过3种方式退出,因此可以在不终止整个进程的情况下,停止它的控制流。
- 线程可以简单地从启动例程中返回,返回值是线程的退出码。
- 线程可以被同一个进程中的其他线程pthread_cancel取消。
- 线程调用pthread_exit。
线程pthread_exit与exit的区别
- 如果进程中的任意线程调用了exit、_Exit或者_exit,终止整个进程。
- 线程调用pthread_exit,只会结束当前线程,不影响程序的执行。
等待线程的终止
线程分两种
-Joinable
- PTHREAD_CREATE_JOINABLE
- 可通过pthread_join等待线程终止
- 调用pthread_join的线程会阻塞
- 一个Joinbale线程结束时,资源不会自动释放给系统(堆栈、exit状态等)
- 当线程终止时,pthread_join会回收该线程资源,然后返回
- 若无pthread_join参与“擦屁股”工作,该线程将变成僵尸线程
-Unjoinable
- PTHREAD_CREATE_DETACHED
- 可通过pthread_detach分离一个线程
- int pthread_detach(pthread_t thread);
- 当线程终止时,资源会自动释放给系统
API接口
- 函数原型:int pthread_detach(pthread_t thread);
- 函数功能:将指定线程与当前线程分离
- 参数说明:指定要分离的线程ID
线程的属性
默认参数
- 调度参数:
- 线程栈地址:
- 线程栈大小:8M
- 栈未尾警戒缓冲区大小:PAGESIZE
- 线程的分离状态:joinable、detached
- 继承性:PTHREAD_INHERIT_SCHED、PTHREAD_EXPLICIT_SCHED
- 作用域:PTHREAD_SCOPE_PROCESS、PTHREAD_SCOPE_SYSTEM
- 调度策略:SCHED_FIFO、SCHED_RR、SCHED_OTHER
相关API函数
- int pthread_attr_init(pthread_attr_t *attr);
- int pthread_attr_destroy(pthread_attr_t *attr);
- int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
- int pthread_attr_getstacksize(const pthread_attr_t *attr, size_t *stacksize);
- int pthread_attr_setstackaddr(pthread_attr_t *attr, void *stackaddr);
- int pthread_attr_getstackaddr(const pthread_attr_t *attr, void **stackaddr);
- int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
- int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);
线程调度与运行
-核心级线程
- 由内核调度,有利于并发使用多处理器资源
-用户级线程
- 由用户层调度,减少上下文切换开销
-线程模型
- 一对一模型
- 多对一模型
- 多对多模型
一对一模型
- 用户线程通过LWP关联内核线程
- 线程调度由内核完成
- SMP、并发使用CPU资源
- 线程间同步由用户层完成
多对一模型
- 多个用户线程与一个内核线程关联
- 线程管理由用户完成、CPU仍以进程为调度单位
- 单处理器
- Solaris线程库:Green thread
Linux下的线程
一对一线程模型
- 一个轻量进程(LWP)对应一个线程
- 每个LWP都与一个内核线程关联
- 内核线程,通过LWP绑定,调度用户线程
- 内核线程被阻塞,LWP也阻塞,用户线程也阻塞
- 调度由内核完成
- SCHED_OTHER:分时调度策略
- SCHED_FIFO:实时调度策略:FIFO
- SCHED_RR:实时调度策略:时间片轮转
- 创建线程、同步等API由用户线程库完成
- Linuxthreads:线程PID、信号处理存在不足
- NPTL(Native POSIX Thread Library)
线程调度与运行
LWP与普通用户进程比较
- LWP只有一个最小的执行上下文和调度程序需要的统计信息
- 用户进程有独立地址空间,LWP与父进程共享地址空间
- LWP可以像内核线程一样,全局范围内竞争处理器资源
- LWP调度可以跟用户进程、内核线程一样调度
- 每一个用户进程可能有一个或多个LWP
- 通过clone,各进程共享地址和资源
- CLONE_VM、CLONE_FS
- CLONE_FILES、CLONE_SIGHAND
- top -H -p
<pid>
- 查看某个指定PID进程下的线程运行
pthread_mutex_t fastmutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t recmutex = PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP;
pthread_mutex_t errchkmutex = PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP;
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
int pthread_mutex_destroy(pthread_mutex_t *mutex);