Linux学习笔记系列文章目录
前置知识篇 1. 进程 2. 线程
一、前言
上一篇讲到进程的基本概念,这篇讲一下线程的基本概念和与进程间的关系。内容有点多,实则只要掌握下如何创建和回收线程即可。
二、前置条件
UB18 + 一点点的基础知识
三、本文参考资料
Unix卷2 百度 野火Linux教程 https://mp.weixin.qq.com/s/-Ae8fmrWozZsxzMomPsBVA
四、正文部分
4.1 引入线程的原因
进程是资源管理的最小单位,线程是程序执行的最小单位。
在操作系统设计上,从进程演化出线程,最主要的目的就是减小进程上下文切换开销。 进程是资源管理的最小单位,那么每个进程都拥有自己的数据段、代码段和堆栈段, 这必然就造成了进程在进行切换时都需要有比较复杂的上下文切换等动作, 因为要保存当前进程上下文的内容, 还要恢复另一个进程的上下文,如果是经常切换进程的话,这样子的开销就过于庞大, 因为在进程切换上下文时, 需要重新映射虚拟地址空间、进出OS内核、寄存器切换,还会干扰处理器的缓存机制, 因此为了进一步减少CPU在进程切换时的额外开销,因此Linux进程演化出了另一个概念——线程。
Linux系统中的每个进程都有独立的地址空间, 一个进程崩溃后,在系统的保护模式下并不会对系统中其它进程产生影响, 而线程只是一个进程内部的一个控制序列,当进程崩溃后,线程也随之崩溃, 所以一个多进程的程序要比多线程的程序健壮,但在进程切换时, 耗费资源较大,效率要差一些。 但在某些场合下对于一些要求同时进行并且又要共享某些变量的并发操作, 只能用线程,不能用进程。
?
4.2 基本概念
线程是操作系统能够调度和执行的基本单位,在Linux中也被称之为轻量级进程。 在Linux系统中, 一个进程至少需要一个线程作为它的指令执行体,进程管理着资源(比如cpu、内存、文件等等), 而将线程分配到某个cpu上执行。
一个进程可以拥有多个线程,它还可以同时使用多个cpu来执行各个线程 ,以达到最大程度的并行,提高工作的效率; 同时,即使是在单cpu的机器上,也依然可以采用多线程模型来设计程序,使设计更简洁、功能更完备,程序的执行效率也更高。
线程的本质是一个进程内部的一个控制序列,它是进程里面的东西,一个进程可以拥有一个线程或者多个线程。
它们的关系就如图所示: 总的来说: 一个程序至少有一个进程,一个进程至少有一个线程。 线程使用的资源是进程的资源,进程崩溃线程也随之崩溃。 线程的上下文切换,要比进程更加快速,因为本质上,线程很多资源都是共享进程的,所以切换时,需要保存和切换的项是很少的。
?
4.3 线程与进程的差异
进行和线程之间的差异可以从下面几个方面来阐述:
-
调度 :在引入线程的操作系统中,线程是调度和分配的基本单位 ,进程是资源拥有的基本单位 。把传统进程的两个属性分开,线程便能轻装运行,从而可 显著地提高系统的并发程度 。在同一进程中,线程的切换不会引起进程的切换;在由一个进程中的线程切换到另一个进程中的线程时,才会引起进程的切换。 -
并发性 :在引入线程的操作系统中,不仅进程之间可以并发执行,而且在一个进程中的多个线程之间亦可并发执行,因而使操作系统具有更好的并发性,从而能 更有效地使用系统资源和提高系统吞吐量。 -
拥有资源 :不论是传统的操作系统,还是设有线程的操作系统,进程都是拥有资源的一个独立 单位,它可以拥有自己的资源。一般地说,线程自己不拥有系统资源(只有一些必不可少的资源,但它可以访问其隶属进程的资源。 -
系统开销:由于在创建或撤消进程时,系统都要为之分配或回收资源,因此,操作系统所付出的开销将显著地大于在创建或撤消线程时的开销。进程切换的开销也远大于线程切换的开销。 -
通信:进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性,因此共享简单。但是线程的数据同步要比进程略复杂。
?
4.4 多线程特点
-
优点: 更快,加快处理任务 更强,同时处理多任务 -
缺点: 难控制,编程困难 bug难定位,资源竞争 不当使用降低性能,线程切换 –> 主要存在绑核错误的情况 通过下面的命令可将进程proName程序绑在1核运行: taskset -c 1 ./proName
而如果只绑定了一个核,那么同一时刻,只有一个线程在运行,而线程之间的切换又会消耗资源,那么这种情况下反而会导致性能降低。 另外一种情况,就是设置的线程数大于总的逻辑CPU数: $ cat /proc/cpuinfo| grep "processor"| wc -l
8
这样的情况下,设置更多的线程并不会提高处理速度。 -
并不是线程数量越多越快,也不是单线程最快。 只有当线程数等于逻辑CPU数时,才能将线程上下文切换所带来的开销降到最小。 -
单线程有时候反而更快 多线程中间显然有切换导致的中断,单核CPU妄图使用多线程提高效率或者每个线程都需要竞争同一把锁而实际可能会导致更慢。
?
4.5 线程相关操作
-
创建线程pthread_create() pthread_create()函数是用于创建一个线程的,创建线程实际上就是确定调用该线程函数的入口点, 在线程创建后,就开始运行相关的线程函数。 参数有必要做一下说明 thread 线程ID指针,创建成功时,会保存在此
attr 线程属性,控制线程的一些行为
start_routine 线程运行起始地址,是一个函数指针
arg 函数的参数,只有一个参数,因此多个参数需要打包在一起
-
线程退出
-
线程函数返回 -
pthread_exit(),也可由主线程调用 使用pthread_exit()函数,让线程显式退出,这是线程的主动行为。 参数retval 是void*类型的指针,可以指向任何类型的数据,它指向的数据将作为线程退出时的返回值。如果线程不需要返回任何数据,将 retval 参数置为NULL即可。 retval 指针不能指向函数内部的局部数据(比如局部变量)。换句话说,pthread_exit() 函数不能返回一个指向局部数据的指针,否则很可能使程序运行结果出错甚至崩溃。 这里要注意的是,在使用线程函数时,不能随意使用exit()退出函数来进行出错处理, 这是因为exit()函数的作用是使调用进程终止,而一个进程往往包含多个线程, 因此,在使用exit()之后,该进程中的所有线程都会被退出,因此在线程中只能调用线程退出函数pthread_exit()而不是调用进程退出函数exit()。 一般情况下,进程中各个线程的运行是相互独立的,线程的终止并不会相互通知,也不会影响其他的线程, 终止的线程所占用的资源不会随着线程的终止而归还系统,而是仍为线程所在的进程持有,这是因为一个进程中的多个线程是共享数据段的。 -
调用pthread_cancel int pthread_cancel(pthread_t thread);
参数 thread 用于接收 Cancel 信号的目标线程。 pthread_cancel() 函数的功能仅仅是向目标线程发送 Cancel 信号, 至于目标线程是否接收该信号,何时响应该信号,全由目标线程决定。 对于接收 Cancel 信号后结束执行的目标线程,等同于该线程自己执行如下语句: pthread_exit(PTHREAD_CANCELED);
-
调用exit,或者主线程退出,所有线程终止 -
等待其他线程结束
-
pthread_join() 以阻塞的方式等待thread指定的线程结束,并且获取它的退出值 当函数返回时,被等待线程的资源将被收回,如果进程已经结束, 那么该函数会立即返回。 并且thread指定的线程必须是可结合状态的,该函数执行成功返回0,否则返回对应的错误代码。 需要注意的是一个可结合状态的线程所占用的内存仅当有线程对其执行了pthread_join()后才会释放, 因此为了避免内存泄漏, 所有线程的终止时,要么已被设为DETACHED,要么使用pthread_join()来回收资源。 -
pthread_barrier_wait() pthread_barrier_wait等待某一个条件达到(计数到达),一旦达到后就会继续往后执行。 当然了,如果你希望各个线程完成它自己的工作,主线程再进行合并动作,则你等待的数量可以再加一个。 pthread_barrier_t b;
/*计数为创建线程数+1*/
pthread_barrier_init(&b,NULL,threadNum + 1);
pthread_barrier_wait(&b);
pthread_barrier_destroy(&b);
?
4.6 线程属性
在Linux中线程属性结构如下:
typedef struct
{
int etachstate; //线程的分离状态
int schedpolicy; //线程调度策略
structsched_param schedparam; //线程的调度参数
int inheritsched; //线程的继承性
int scope; //线程的作用域
size_t guardsize; //线程栈末尾的警戒缓冲区大小
int stackaddr_set; //线程的栈设置
void* stackaddr; //线程栈的位置
size_t stacksize; //线程栈的大小
} pthread_attr_t;
注意:因为pthread并非Linux系统的默认库,而是POSIX线程库。 在Linux中将其作为一个库来使用, 因此编译时需要加上-lpthread(或-pthread)以显式指定链接该库。 函数在执行错误时的错误信息将作为返回值返回, 并不修改系统全局变量errno,当然也无法使用perror()打印错误信息。
线程的属性非常多,而且其属性值不能直接设置,须使用相关函数进行操作, 线程属性主要包括如下属性:
作用域(scope)
栈大小(stacksize)
栈地址(stackaddress)
优先级(priority)
分离的状态(detachedstate)
调度策略和参数(scheduling policy and parameters)
默认的属性为非绑定、非分离、1M的堆栈大小、与父进程同样级别的优先级。
如果不是特别需要的话,是可以不需要考虑线程相关属性的,使用默认的属性即可。
初始化线程对象属性 使用pthread_attr_init()函数可以初始化线程对象的属性,函数原型:
int pthread_attr_init(pthread_attr_t *attr);
参数:
attr:指向一个线程属性的指针
返回值:
若函数调用成功返回0,否则返回对应的错误代码。
销毁一个线程属性对象 pthread_attr_destroy()函数用于销毁一个线程属性对象。 若pthread_create()函数使用了已经销毁的线程属性对象创建线程,会返回错误。
int pthread_attr_destroy(pthread_attr_t *attr);
参数:
attr:指向一个线程属性的指针
返回值:
若函数调用成功返回0,否则返回对应的错误代码。
?
4.7 线程的分离状态
在任何一个时间点上,线程是可结合的(joinable), 或者是分离的(detached)。 一个可结合的线程能够被其他线程收回其资源和杀死;在被其他线程回收之前, 它的存储器资源(如栈)是不释放的。 相反,一个分离的线程是不能被其他线程回收或杀死的, 它的存储器资源在它终止时由系统自动释放。
总而言之:线程的分离状态决定一个线程以什么样的方式来终止自己。
进程中的线程可以调用pthread_join()函数来等待某个线程的终止,获得该线程的终止状态,并收回所占的资源, 如果对线程的返回状态不感兴趣,可以将rval_ptr设置为NULL。
int pthread_join(pthread_t tid, void **rval_ptr);
除此之外线程也可以调用pthread_detach()函数将此线程设置为分离状态,设置为分离状态的线程在线程结束时,操作系统会自动收回它所占的资源。 设置为分离状态的线程,不能再调用pthread_join()等待其结束。
int pthread_detach(pthread_t tid);
如果一个线程是可结合的,意味着这条线程在退出时不会自动释放自身资源,而会成为僵尸线程,同时意味着该线程的退出值可以被其他线程获取。 因此,如果不需要某条线程的退出值的话, 那么最好将线程设置为分离状态,以保证该线程不会成为僵尸线程。
如果在创建线程时就知道不需要了解线程的终止状态,那么可以通过修改pthread_attr_t结构中的detachstate属性,让线程以分离状态启动
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
如果想要获取某个线程的分离状态,那么可以通过pthread_attr_getdetachstate()函数获取:
int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);
参数:
attr:
指向一个线程属性的指针。
detachstate:
如果值为PTHREAD_CREATE_DETACHED,则表示线程是分离状态,
如果值为PTHREAD_CREATE_JOINABLE,则表示线程是结合状态。
返回值:
成功返回0,
否则返回对应的错误代码。
?
4.8 线程的调度策略
线程属性里包含了调度策略配置,POSIX 标准指定了三种调度策略:
分时调度策略,SCHED_OTHER。
这是线程属性的默认值,另外两种调度方式只能用于以超级用户权限运行的进程,因为它们都具备实时调度的功能,但在行为上略有区别。
实时调度策略,先进先出方式调度(SCHED_FIFO)。
基于队列的调度程序,对于每个优先级都会使用不同的队列,先进入队列的线程能优先得到运行,线程会一直占用CPU,
直到有更高优先级任务到达或自己主动放弃CPU使用权。
实时调度策略 ,时间片轮转方式调度(SCHED_RR)。
与 FIFO相似,不同的是前者的每个线程都有一个执行时间配额,
当采用SHCED_RR策略的线程的时间片用完,系统将重新分配时间片,并将该线程置于就绪队列尾,
并且切换线程,放在队列尾保证了所有具有相同优先级的RR线程的调度公平。
与调度相关的API接口如下:
/* 线程是否继承调度 */
int pthread_attr_setinheritsched(pthread_attr_t *attr, int inheritsched);
int pthread_attr_getinheritsched(const pthread_attr_t *attr, int *inheritsched);
/* 线程调度策略 */
int pthread_attr_setschedpolicy(pthread_attr_t *attr, int policy);
int pthread_attr_getschedpolicy(const pthread_attr_t *attr, int *policy);
参数:
attr:
指向一个线程属性的指针。
inheritsched:
线程是否继承调度属性,可选值分别为
PTHREAD_INHERIT_SCHED:调度属性将继承于创建的线程,attr中设置的调度属性将被忽略。
PTHREAD_EXPLICIT_SCHED:调度属性将被设置为attr中指定的属性值。
policy:
可选值为线程的三种调度策略,SCHED_OTHER、SCHED_FIFO、SCHED_RR。
返回值:
成功返回0,
否则返回对应的错误代码。
?
4.9 线程的优先级
线程优先级就是这个线程得到运行的优先顺序,在Linux系统中,优先级数值越小,线程优先级越高,Linux根据线程的优先级对线程进行调度,遵循线程属性中指定的调度策略。
获取、设置线程静态优先级(staticpriority)可以使用以下函数,当线程的调度策略为SCHED_OTHER时,其静态优先级必须设置为0。 该调度策略是Linux系统调度的默认策略,处于0优先级别的这些线程按照动态优先级被调度, 之所以被称为“动态”,是因为它会随着线程的运行,根据线程的表现而发生改变, 而动态优先级起始于线程的nice值,且每当一个线程已处于就绪态但被调度器调度无视时,其动态优先级会自动增加一个单位,这样能保证这些线程竞争CPU的公平性。
线程的静态优先级之所以被称为“静态”,是因为只要你不强行使用相关函数修改它,它是不会随着线程的执行而发生改变, 静态优先级决定了实时线程的基本调度次序,它们是在实时调度策略中使用的。
int pthread_attr_setschedparam(pthread_attr_t *attr, const struct sched_param *param);
int pthread_attr_getschedparam(const pthread_attr_t *attr, struct sched_param *param);
参数:
attr:指向一个线程属性的指针。
param:静态优先级数值。
线程优先级有以下特点: 新线程的优先级为默认为0。 新线程不继承父线程调度优先级(PTHREAD_EXPLICIT_SCHED) 当线程的调度策略为SCHED_OTHER时,不允许修改线程优先级, 仅当调度策略为实时(即SCHED_RR或SCHED_FIFO)时才有效, 并可以在运行时通过pthread_setschedparam()函数来改变,默认为0。
?
4.10 线程栈
线程栈是非常重要的资源,它可以存放函数形参、局部变量、线程切换现场寄存器等数据, 在前文我们也说过了,线程使用的是进程的内存空间,那么一个进程有n个线程,默认的线程栈大小是1M, 那么就有可能导致进程的内存空间是不够的, 因此在有多线程的情况下,我们可以适当减小某些线程栈的大小, 防止进程的内存空间不足, 而某些线程可能需要完成很大量的工作,或者线程调用的函数会分配很大的局部变量, 亦或是函数调用层次很深时,需要的栈空间可能会很大,那么也可以增大线程栈的大小。
设置、获取线程栈大小可以使用以下函数:
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
int pthread_attr_getstacksize(const pthread_attr_t *attr, size_t *stacksize);
参数:
attr:
指向一个线程属性的指针。
stacksize:
线程栈的大小。
?
4.11 绑核
为什么要绑核?
-
充分利用CPU,减少CPU之间上下文切换 -
指定程序运行在指定CPU,便于区分 $ taskset -c 1 ./proName
将proName绑定在第二个核。
$ taskset -c 1-3 ./proName
绑定运行在第二个到第四个核。
$ taskset -p 3569
pid 3569's current affinity mask: f
查看进程3569当前运行在哪个核上。 mask f转为二进制即为1111,因此四个核都有运行。 当然除了命令行,还有函数接口可以使用,这里就不再扩展了。
如何查看机器的CPU数量
-
物理CPU个数,就是你实际CPU的个数: $ cat /proc/cpuinfo | grep "physical id" | sort -u | wc -l
1
-
CPU物理核数,就是你的一个CPU上有多少个核心,现在很多CPU都是多核: $ cat /proc/cpuinfo | grep "core id" | sort -u | wc -l
2
-
CPU逻辑核数,一颗物理CPU可以有多个物理内核,加上超线程技术,会让CPU看起来有很多个: $ cat /proc/cpuinfo | grep "processor" | sort -u | wc -l
4
五、总结
为了进一步减少CPU在进程切换时的进程上下文的额外开销,因此Linux进程演化出了另一个概念——线程。 因为本质上,线程很多资源都是共享进程的,所以切换时,需要保存和切换的项是很少的。
进程:资源管理的最小单位,一个进程至少需要一个线程作为它的指令执行体,可多CPU处理 线程:程序执行的最小单位,操作系统能够调度和执行的基本单位,只能在某个CPU上执行
线程的本质是一个进程内部的一个控制序列,它是进程里面的东西,一个进程可以拥有一个线程或者多个线程。
/* 创建线程,线程为test_thread函数 */
res = pthread_create(&thread, NULL, test_thread, (void*)(unsigned long long)(arg));
/* 以阻塞方式等待线程终止 */
res = pthread_join(thread, &thread_return);
线程上下文切换是有开销的,如果它的收益不能超过它的开销,那么使用多线程来提高效率将得不偿失。因此不要盲目推崇多线程。如果为了提高效率采用多线程,那么线程中最多应为逻辑CPU数。也就是说如果你的程序绑在一个核上或者你只有一个CPU一个核,那么采用多线程只能提高同时处理的能力,而不能提高处理效率。
?
|