文章目录
一、Linux线程
1.1 什么是线程
对应很多应用而言,需要同时执行多种活动。但是进程只有一个执行流,这就需要创建出更多的执行流来做不同的事。
线程(thread
)是操作系统能够进行运算调度的最小单位。大部分情况下,它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix System V及SunOS中也被称为轻量进程(lightweight processes
)(LWP),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。
同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈,自己的寄存器环境,自己的线程本地存储。
1.2 Linux下的线程
按道理来说,需要像进程那样给线程创建属于自己的数据结构(TCB)用于描述本身的属性。在Linux下,没有真正意义上的线程,它是
Linux中没有专门为线程设计TCB,而是用进程的PCB来模拟线程。在Linux下,线程也称作轻量级进程。
进程的概念也可更加简单的理解:是在进程内部(线程在进程的地址空间内)运行的一个执行分支(执行流)(CPU直接执行PCB),属于进程的一部分,粒度要比进程更加细和轻量化。
CPU在调度PCB时不会专门去区分到底是线程还是进程。
1.3 线程的优点
-
创建一个新线程的代价要比创建一个新进程小得多。
-
与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多。
线程在进程的地址空间内部运行,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器,因此同一进程内的各个线程间不需要切换进程运行环境和内存地址空间,线程之间的切换开销小。 -
线程占用的资源要比进程少很多。
-
能充分利用多处理器的可并行数量, 当然进程也可以。
-
在等待慢速I/O操作结束的同时,程序可执行其他的计算任务。
-
计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
-
I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
-
线程间通信的开销要比进程间通信少
1.4 线程的缺点
Linux线程因为是用进程模拟的,所以Linux下不会给用户提供直接操作线程的接口,而是提供在同一个地址空间内创建PCB的方法,分配资源给指定的PCB的接口。所以对用户不太友好,这是这样设计的缺点。(这里的用户指的是:系统级工程师)
-
性能损失: 由于线程之间资源师共享的,因此多线程的临界资源一定会变多,而临界资源我们需要保证它的安全性,因此需要进行一系列的加锁、解锁、互斥等动作。而这些动作都会带来副作用的,即导致性能的降低。如果线程数大于可以处理器数,增加了额外的同步和调度开销,而可用的资源不变,性能没有最大化。
-
健壮性降低: 编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了 不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
-
缺乏访问控制: 进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
-
编程难度提高: 每一个线程出错都可能会影响整个进程。所以编写与调试一个多线程程序比单线程程序困难得多
1.4 线程的私有数据
进程的多个线程共享同一地址空间,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到。除此之外,各线程还共享以下进程资源和环境:
- 文件描述符表
- 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
- 当前工作目录
- 用户id和组id
私有数据:
- 线程ID
- 调用栈
- 上下文(与寄存器相关联)
- errno
- 信号屏蔽字
- 调度优先级
1.5 线程用途
- 合理的使用多线程,能提高CPU密集型程序的执行效率
- 合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)
二、线程控制
由于Linux线程有进程PCB来模拟实现,系统提供的是创建PCB的接口,没有直接创建线程的接口,所以线程的控制是非常麻烦的。但是有系统工程师们对线程相关的系统调用进行封装,创建一套标准库,提供简易接口用于创建线程。库 <pthread.h>
2.1 创建线程
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
参数:
-
thread:输出型参数,线程的ID。但这个ID是库里面提供的ID,并不是系统底层的ID,系统底层称为LWP(轻量级进程)
-
attr:线程的属性,一般默认为NULL,交给库管理
-
start_routine:函数指针,该函数返回值为void*,参数为void*,这个指针指向的函数是线程的入口,多线程就是把进程的代码拆成很多块,我们的一个线程执行的就是多个代码块之中的某一块。
-
arg:给线程入口函数传递的参数。
返回值:成功返回0,出错返回错误码
在编译的时候需要链接pthread库-lpthread
库函数pthread_self
可以获得库里面的线程ID。
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
void* thread_run(void* args)
{
while(1){
printf("new thread PID: %d, 库ID: %x, 参数%s\n", getpid(), pthread_self(), args);
sleep(2);
}
return NULL;
}
int main()
{
pthread_t tid;
pthread_create(&tid, NULL, thread_run, "thread");
while(1){
printf("main thread PID: %d, 库ID: %x\n", getpid(), pthread_self());
sleep(2);
}
return 0;
}
进程PID相同,说明是在同一个进程内。
2.2 线程终止
线程终止的方案
-
函数中return
a. main函数退出return的时候代表(主线程and进程退出)
b.其他线程函数return,只代表当前线程退出。 -
某个线程异常终止,会导致进程终止,从而所有线程终止。
-
新线程通过pthread_exit终止自己。
exit是终止进程,使用之后整个进程包括所有线程都被终止。不管在任何地方。
void pthread_exit(void *value_ptr);
value_ptr
不要指向一个局部变量。
返回值:无返回值,跟进程一样,自己为调用者,被执行后线程结束,无法返回值。
需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。否则无法访问该变量。 -
取消目标线程。
thread
:线程库ID
返回值:成功返回0;失败返回错误码
2.3 线程等待
为什么需要线程等待?
- 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
- 创建新的线程不会复用刚才退出线程的地址空间。
- 可能会导致类似于"僵尸进程"的问题!
thread
:线程ID
value_ptr
:它指向一个指针,后者指向线程的返回值
返回值:成功返回0;失败返回错误码
调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:
- 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
- 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数
PTHREAD_ CANCELED,值为-1,在库文件里面定义。
- 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。
- 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。
如果提前是取消主线程,主线程将处理僵尸状态。
2.4 线程分离
如果不想等待线程终止呢?
默认情况下,新创建的线程是需要被等待退出的,否则会无法释放资源,如果不关心线程的返回值,这时线程等待就是一种负担,这个时候我们可以告诉操作系统进,当线程退出时,自动释放线程资源。这就是线程分离。
当新线程分离之后,主线程就不会再关注新线程的情况,新线程的资源就独立了,线程退出之后,自己就释放了自己的资源,不再往自己的PCB之中写入退出码,主线程也不再需要进行等待。
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离。
等待线程和线程分离是互相矛盾的,分离之后就不用等待了。
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
void* thread_run(void* args)
{
int cnt = 5;
while(cnt --){
printf("new thread PID: %d, 库ID: %x, 参数%s\n", getpid(), pthread_self(), args);
sleep(2);
}
printf("new thread exit\n");
// 运行完自己退出
pthread_exit(NULL);
}
int main()
{
pthread_t tid;
pthread_create(&tid, NULL, thread_run, "thread");
main_tid = pthread_self();
// 执行自己的
while(1){
printf("main thread PID: %d, 库ID: %x\n", getpid(), pthread_self());
sleep(2);
}
return 0;
}
三、进程和线程的区别
3.1 基本概念
- 进程是资源分配的基本单位
- 线程是调度的基本单位
线程共享进程数据,但也拥有自己的一部分数据:
线程ID
一组寄存器
栈
errno
信号屏蔽字
调度优先级
每个线程完成不同的任务,但是共享同一地址空间(也就是同样的动态内存,映射文件,目标代码等等),打开的文件队列和其他内核资源。资源分配给进程,同一进程的所有线程共享该进程的所有资源。同一进程中的多个线程共享
代码段(代码和常量)
数据段(全局变量和静态变量)
扩展段(堆)
文件描述符表
每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
当前工作目录
用户id和组id
3.2 区别
-
一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。线程依赖于进程而存在。
-
进程在执行过程中拥有独立的内存单元,而多个线程共享进程的内存。
-
进程是资源分配的最小单位,线程是CPU调度的最小单位;
-
系统开销: 由于在创建或撤消进程时,系统都要为之分配或回收资源,如内存空间、I/o设备等。因此,操作系统所付出的开销将显著地大于在创建或撤消线程时的开销。类似地,在进行进程切换时,涉及到整个当前进程CPU环境的保存以及新被调度运行的进程的CPU环境的设置。而线程切换只须保存和设置少量寄存器的内容,并不涉及存储器管理方面的操作。可见,进程切换的开销也远大于线程切换的开销。
-
通信: 由于同一进程中的多个线程具有相同的地址空间,致使它们之间的同步和通信的实现,也变得比较容易。进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。在有的系统中,线程的切换、同步和通信都无须操作系统内核的干预
-
进程编程调试简单可靠性高,但是创建销毁开销大;线程正相反,开销小,切换速度快,但是编程调试相对复杂。
-
进程间不会相互影响 ;线程一个线程挂掉将导致整个进程挂掉
-
进程适应于多核、多机分布;线程适用于多核