Linux多线程编程
Linux多线程编程
lycheeKing认识cpu、核、进程与线程
author:lcw
物理CPU
- 物理CPU是相对于虚拟CPU而言的概念,指实际存在的CPU处理器,安装在PC主板或服务器上。
物理核
CPU中包含的物理内核(核心)个数,比如多核CPU,单核CPU(古老的CPU)。这个多核或者单核已经集成在CPU内部了。
在linux系统下面的/proc/cpuinfo文件的条目中:
有多少个不同的physical id就有多少个物理CPU。
cpu cores记录了对应的物理CPU(以该条目中的
physical id
标识)有多少个物理核。
1 | [root@zyshanlinux-01 ~]# cat /proc/cpuinfo |
- 物理核数量=cpu数(机子上装的cpu的数量) * 每个cpu的核心数
逻辑核(逻辑CPU或虚拟核)
所谓的4核8线程,4核指的是物理核心。用Intel的
超线程技术
(HT)将物理核虚拟而成的逻辑处理单元,现在大部分的主机的CPU都在使用HT技术,用一个物理核模拟两个虚拟核,即每个核两个线程,总数为8线程。- 超线程(Hyper-threading, HT):超线程可以在一个逻辑核等待指令执行的间隔(等待从cache或内存中获取下一条指令),把时间片分配到另一个逻辑核。高速在这两个逻辑核之间切换,让应用程序感知不到这个间隔,误认为自己是独占了一个核。
在windows系统下面看下图,我们看到有8个cpu记录,其实我们使用的四核CPU只是使用HT技术虚拟出来8个逻辑CPU;在linux系统下面的
/proc/cpuinfo
文件的条目中siblings
记录了对应的物理CPU(以该条目中的physical id标识)有多少个逻辑核。在操作系统看来是8个核,但是实际上是1个物理CPU中的4个物理内核。
通过超线程技术可以实现单个物理核实现线程级别的并行计算,但是比不上性能两个物理核。
单核cpu和多核cpu
- 都是一个cpu,不同的是每个cpu上的核心数。
- 多核cpu是多个单核cpu的替代方案,多核cpu减小了体积,同时也减少了功耗。
- 一个核心只能同时执行一个线程。
既然计算机多核与超线程模拟相关,所以实际上计算机的核数翻倍并不意味着性能的翻倍,也不意味着核数越多计算机性能会越来越好,因为超线程只是充分利用了CPU的空闲资源,实际上在应用中基于很多原因,CPU的执行单元都没有被充分使用。
查看电脑上的cpu配置
1.cmd模式下–》wmic—》cpu get *
2.任务管理器
进程和线程
理解
- 进程是操作系统进行资源(包括cpu、内存、磁盘IO等)分配的最小单位。
- 线程是cpu调度和分配的基本单位。
- 我们打开的聊天工具,浏览器都是一个进程。
- 进程可能有多个子任务,比如聊天工具要接受消息,发送消息,这些子任务就是线程。
- 资源分配给进程,线程共享进程资源。
对比
对比 | 进程 | 线程 |
---|---|---|
定义 | 进程是程序运行的一个实体的运行过程,是系统进行资源分配和调配的一个独立单位 | 线程是进程运行和执行的最小调度单位 |
系统开销 | 创建撤销切换开销大,资源要重新分配和收回 | 仅保存少量寄存器的内容,开销小,在进程的地址空间执行代码 |
拥有资产 | 资源拥有的基本单位 | 基本上不占资源,仅有不可少的资源(程序计数器,一组寄存器和栈) |
调度 | 资源分配的基本单位 | 独立调度分配的单位 |
安全性 | 进程间相互独立,互不影响 | 线程共享一个进程下面的资源,可以互相通信和影响 |
地址空间 | 系统赋予的独立的内存地址空间 | 由相关堆栈寄存器和和线程控制表TCB组成,寄存器可被用来存储线程内的局部变量 |
线程切换
cpu给线程分配时间片(也就是分配给线程的时间),执行完时间片后会切换都另一个线程。
切换之前会保存线程的状态,下次时间片再给这个线程时才能知道当前状态。
从保存线程A的状态再到切换到线程B时,重新加载线程B的状态的这个过程就叫上下文切换。
而上下切换时会消耗大量的cpu时间。
线程开销
- 上下文切换消耗
- 线程创建和消亡的开销
- 线程需要保存维持线程本地栈,会消耗内存
串行,并发与并行
串行
- 多个任务,执行时一个执行完再执行另一个。
- 比喻:吃完饭再看视频。
并发
- 多个线程在单个核心运行,同一时间一个线程运行,系统不停切换线程,看起来像同时运行,实际上是线程不停切换。
- 比喻: 一会跑去厨房吃饭,一会跑去客厅看视频。
并行
- 每个线程分配给独立的核心,线程同时运行。
- 比喻:一边吃饭一边看视频。
==多核下线程数量选择==
计算密集型
- 程序主要为复杂的逻辑判断和复杂的运算。
- cpu的利用率高,不用开太多的线程,开太多线程反而会因为线程切换时切换上下文而浪费资源。
IO密集型
- 程序主要为IO操作,比如磁盘IO(读取文件)和网络IO(网络请求)。
- 因为IO操作会阻塞线程,cpu利用率不高,可以开多点线程,阻塞时可以切换到其他就绪线程,提高cpu利用率。
==优化方案:==
- 提高性能的一种方式:提高硬件水平,处理速度或核心数。
- 另一种方式:根据实际场景,合理设置线程数,软件上提高cpu利用率。
多线程编程
如今,几乎所有的电脑(操作系统)都支持同时执行多个任务,比如一边用迅雷下载资源,一边听歌,一边用 QQ 和好友聊天,这样的执行方式简称“并发”或者“并行”。
并发和并行都指的是计算机可以同时执行多个任务,但严格来讲,它们是有区别的
- 并发:==一个处理器同时处理多个任务。==
- 并行:==多个处理器或者是多核的处理器同时处理多个不同的任务.==
前者是逻辑上的同时发生(simultaneous),而后者是物理上的同时发生.
来个比喻:并发和并行的区别就是一个人同时吃三个馒头和三个人同时吃三个馒头。
程序并行的常用实现方式有两种,分别叫做“多进程编程”和“多线程编程”。本节,我们教大家如何在 Linux 下进行多线程编程。
程序、进程和线程
学习多线程编程的实现方法之前,首先要搞清楚什么是线程,这就要从程序、进程和线程三者的关系和区别讲起。
大家常常编写程序,程序其实就是一系列指令(代码)的集合,我们通常将它编写在一个或者多个文件中。例如,C 语言程序通常编写在后缀名为 .c 的文件中,Python 程序编写在后缀名为 .py 的文件中,我们通常将存有程序的文件称为“源文件”。
程序以源文件的方式存储在外存(比如硬盘、U盘等)中,只有运行的时候才会被载入内存
。对于支持并行的操作系统来说,必须为每一个运行的程序分配所需的资源(内存空间、输入输出设备等),并确保同时运行的程序之间不会相互干扰,为此,操作系统将每一个运行着的程序视为一个进程:
- 操作系统以进程为单位,为每个进程分配执行所需要的资源;
- 原则上,各个进程之间不允许访问对方的资源;
- 操作系统实时监控着每个进程的执行状态,必要时可以强制其终止执行。
也就是说在操作系统看来,每个载入内存执行的程序都是一个进程。操作系统以进程为单位分配资源,各个进程相互独立,执行过程互不干扰。
同一时间,操作系统可以运行多个应用程序(进程),每个应用程序(进程)还可以同时执行多个任务,例如迅雷支持同时下载多个文件,QQ 也支持同时和多个好友聊天。同一进程中,执行的每个任务都被视为一个线程。
线程和进程之间的关系,与工厂和工人之间的关系非常相似。一个进程好比是一座工厂,一个线程就如同这个工厂中的一个工人。工厂可以容纳多个工人,每个工人负责完成一项具体的任务。工厂负责为所有工人提供必要的资源(电力、产品原料、食堂、厕所等),所有工人共享这些资源。
也就是说,一个进程中可以包含多个线程,所有线程共享进程拥有的资源。当然,每个线程也可以拥有自己的私有资源。下图给您展示进程和线程之间的关系:
图 1 进程和线程的关系
如图 1 所示,所有线程共享的进程资源有:
- 代码:即应用程序的代码;
- 数据:包括全局变量、函数内的静态变量、堆空间的数据等;
- 进程空间:操作系统分配给进程的内存空间;
- 打开的文件:各个线程打开的文件资源,也可以为所有线程所共享,例如线程 A 打开的文件允许线程 B 进行读写操作。
各个线程也可以拥有自己的私有资源,包括寄存器中存储的数据、线程执行所需的局部变量(函数参数)等。
进程和线程运行状态
多线程编程的实现方法
了解了程序、进程和线程之间的关系后,多线程的含义就很容易理解了,它指的是一个进程中拥有多个(≥2)线程。通常,我们将编写多线程程序的过程称为“多线程编程”。
Linux 上编写多线程程序,可以借助 <pthread.h>
头文件提供的一些函数,常用的函数有如下几个:
创建线程pthread_create()
==注:编译时需要指定链接库 -lpthread==
pthread_create() 函数专门用来创建线程,语法格式如下:
1 | int pthread_create(pthread_t *thread, |
各个参数的含义是:
- thread:接收一个 pthread_t 类型变量的==地址==,每个 pthread_t 类型的变量都可以表示一个线程。
- attr:手动指定新线程的==属性==,我们可以将其置为 NULL,表示新建线程遵循默认属性。
- start_routine:以函数指针的方式指明新建线程需要执行哪个函数。
- arg:向 start_routinue() 函数的形参传递数据。将 arg 置为 NULL,表示不传递任何数据。
如果成功创建线程,pthread_create()
函数返回数字 0,否则返回一个非零值。各个非零值都对应着不同的宏,指明创建失败的原因,常见的宏有以下几种:
- EAGAIN:系统资源不足,无法提供创建线程所需的资源。
- EINVAL:传递给 pthread_create() 函数的 attr 参数无效。
- EPERM:传递给 pthread_create() 函数的 attr 参数中,某些属性的设置为非法操作,程序没有相关的设置权限。
以上这些宏都定义在 ==<errno.h>==头文件中,如果想使用这些宏,需提前引入此头文件。
示例:
1 |
|
==*gcc thread.c -o thread -lpthread*==thread.c
为你些的源文件,不要忘了加上头文件#include<pthread.h>
退出线程pthread_exit()
函数原型:
void pthread_exit(void *retval);
函数功能:
终止调用它的线程并通过形参返回一个指向某个对象的指针
形 参: void *retval — 线程需要返回的地址
返回值: 无
==注:线程结束必须释放线程堆栈,也就是线程函数必须调用pthread_exit()结束,否则直到主进程函数退出才释放。==
示例:
1 |
|
等待线程结束pthread_join()
int pthread_join(pthread_t thread, void **retval);
函数功能:
以阻塞方式等待thread指定线程结束,当函数返回值,被等待线程的资源被回收。若线程已经结束,则立即返回。并且thread指定的线程必须是joinable(结合属性)属性。
形 参: thread — 线程标志符(线程ID)。线程唯一标志,类型为:pthread_t
retval — 用户定义的指针,用来存储被等待线程返回的地址
返回值: 成功返回0,失败返回错误编号。
示例:
1 |
|
获取当前线程标志符pthread_self()
函数原型:
**pthread_t pthread_self(void);**
函数功能:
获取线程自身ID。形 参: 无
返回值: 返回当前线程标志符。
pthread_t
类型为unsigned long int(无符号长整型)
,打印应 %lu。
示例:
1 |
|
自动清理线程资源
函数原型:
==//注册清理函数==
void pthread_cleanup_push(void (*routine)(void *),void =*arg);
==//释放清理函数==void pthread_cleanup_pop(int execute);
函数功能:
线程清除处理函数,用于程序异常退出的时候做善后的资源清理。自动释放资源。
注:pthread_cleanup_push
函数与pthread_cleanup_pop
函数需要成对调用。
形 参:
void (*=routine)(void *) — 处理程序函数入口
void *arg — 传递给处理函数形参
int execute — 执行的状态值,0 – 不调用清理函数;1 – 调用清理函数。
返回值: 无
==导致调用清理函数条件:==
1.调用pthread_exit()函数
2.Pthread_claenup_pop的形参为1
==注:return不会导致清理函数调用。==
示例:
1 |
|
==注:子线程退出时,return退出不会触发线程清理函数==
线程取消函数pthread_cancel()
**int pthread_cancel(pthread_t thread);**
函数功能:
取消同一进程中的其他线程。
形 参:
pthread_t thread — 线程描述符
返回值: 0 — 成功,其他值 — 失败
1 |
|
线程分离属性pthread_detach()
创建一个线程默认的状态是joinable
(结合属性),如果一个线程结束但没有调用pthread_join,则它的状态类似于进程中的zombie process(僵尸进程),即还有一部分资源没有被回收(退出状态码),所以创建线程时应该使用函数pthread_join来等待线程运行结束,并可得到线程的退出代码,回收其资源(类似进程中的wait、waitpid)。但是调用pthread_join(pthread_id)函数后,如果该线程没有运行结束,调用者会被阻塞,有些情况下我们并不希望如此。pthread_detach函数可以将该线程状态设置为detached(分离状态),则该线程运行结束后自动会释放所有资源。
函数原型:
==int pthread_detach(pthread_t thread);==
形 参:
pthread_t thread — 线程标志符
返回值: 0 — 成功,其它值 – 失败
1 |
|
设置线程栈空间
查看线程堆栈空间:
1 | [wbyq@wbyq ~]$ ulimit -s |
8192单位是KB,也就是默认栈空间大小为8M
通过命令ulimit -a查看线程栈空间详细信息
1 | [wbyq@wbyq ~]$ ulimit -a |
通过命令ulimit -s <栈空间大小>
1 | [wbyq@wbyq ~]$ ulimit -s 10240 |
每个线程的栈空间都是独立的,如果堆栈空间溢出程序会出现段错误。如果一个进程有10个线程,那么分配的栈空间大小为10*<每个线程栈空间大小>
示例:
1 |
|
通过函数设置和查询线程栈空间
1 |
|
扩展
如果对Linux进程线程实现原理感兴趣的,参考以下
(进一步了解)http://t.csdn.cn/XMUmJ