浅析Linux多线程的一些概念

192 阅读7分钟

线程概念

线程实际上就是进程的一个执行流,也就是一个程序的内部执行序列。 线程在进程的地址空间中运行,而且对于线程而言他比进程更加轻量化。透过进程虚拟地址空间,可以看到进程的大部分资源,,将进程资源合理的分配给每个执行流,就形成的线程的执行流。

线程的优点

  1. 轻量化,比创建一个进程的代价要小的多。
  2. 线程之间的切换耗费的资源少
  3. 可以充分利用多处理器的并行数量
  4. 可以对于很多程序提高性能,比如那种IO密集型的程序

缺点

  1. 会使程序的健壮性降低,因为一个线程出现错误,整个程序就完蛋了
  2. 增加了项目的复杂度,要时刻关注项目会不会死锁,并发的问题,会使编码变得麻烦。
  3. 线程能提高的性能有限,当线程达到最佳性能时,线程越多,线程切换的代价越大。

线程的作用

  1. 合理使用多线程可以提高CPU密集型程序的执行效率
  2. 可以提高IO密集型的效率,比如一边写代码,一边下载工具。

进程与线程

  1. 进程是资源分配的基本单位
  2. 线程是调度的基本单位
  3. 线程和进程中很多数据是共享的,但是线程也有它自己资源比如上下文,和栈

PID和LWP

PID是进程ID,LWP实际上是线程ID,当我们使用ps -aL时,如果看见PID==LWP时,就是进程,其他的就是线程, LWP: light weight process 轻量级进程

image.png

Linux线程系统接口的关系

Linux因为是用进程模拟的,所以linux下,不会给我们提供直接操作线程的接口,而是提供,在同一个地址空间创建PCB的方法,分配资源给指定的PCB的接口,对于这个接口做封装,打包成库,直接使用库的接口。

LWP和线程ID

LWP实际上是虚拟内存中的线程ID,真正的线程ID是线程PCB经过页表映射到物理内存中的地址

image.png

线程创建

功能:创建一个新的线程

原型

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *

(*start_routine)(void*), void *arg);

参数

thread:返回线程ID

attr:设置线程的属性,attr为NULL表示使用默认属性

start_routine:是个函数地址,线程启动后要执行的函数

arg:传给线程启动函数的参数

返回值:成功返回0;失败返回错误码

线程的终止

三种方法可以终止线程和不终止进程

  1. 线程中 return
  2. 调用pthread_exit()终止自己原型
原型

void pthread_exit(void *value_ptr);

参数

value_ptr:value_ptr不要指向一个局部变量。

    返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)
  1. 一个线程可以通过调用pthread_cancel()来终止其他线程
原型

int pthread_cancel(pthread_t thread);

参数

thread:线程ID

返回值:成功返回0;失败返回错误码

线程等待

为什么要线程等待? 一个线程执行可能执行失败,我们想知道他失败的原因,而且,线程的资源要被释放,仍然在进程的地址空间内,而且创建的新线程不会复用刚才退出的地址空间

功能:等待线程结束

原型

int pthread_join(pthread_t thread, void **value_ptr);

参数

thread:线程ID

value_ptr:它指向一个指针,后者指向线程的返回值

返回值:成功返回0;失败返回错误码

thread线程以不同的方法终止,通过pthread_join()得到的终止状态是不一样的

  1. 如果通过return ,value_ptr中就是返回值
  2. 如果是pthread_cancel(),,value_ ptr所指向的单元里存放的是常数PTHREAD_ CANCELED
  3. 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit参数
  4. 如果对于退出结果不感兴趣,可以传入空指针

线程的分离

如果我们不关心线程的返回值,那么我们可以分离这个线程,当线程退出的时候,自动释放线程资源

int pthread_detach(pthread_t thread);
也可以自己分离自己
pthread_detach(pthread_self());

但是一个线程不可以既joinable又可以分离

线程互斥

当多个线程访问同一个资源的时候,这个资源就是临界资源,这个资源如果被多个线程同时操作,有可能引发线程安全的问题.这个就叫做线程互斥。互斥实际上就是有且只有一个执行流进入临界区。原子性:不会被任何调度机制打断的操作,要么完成,要么不做。

互斥量

我们要实现线程安全,就要把这个临界资源保护起来,这时互斥量就应运而生了,

初始化互斥量有两种方法

静态分配

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

动态分配

*int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict

attr);*

销毁互斥量

如果是静态分配的互斥量无需销毁,动态分配的互斥量要调用

*int pthread_mutex_destroy(pthread_mutex_t mutex);

互斥量加锁和解锁

int pthread_mutex_lock(pthread_mutex_t *mutex);

int pthread_mutex_unlock(pthread_mutex_t *mutex);

返回值:成功返回0,失败返回错误号
互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功

发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,

那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁

互斥量实现原理

我们使用互斥量来保护临界资源,但是这样多个线程又访问了互斥量,那么这个互斥量也是一个临界资源啊!那么互斥量是怎么实现原子性的呢?

实际上,我们的互斥量可以认为是一个值为1的数lock,当加锁的时候lock--,当lock大于0的时候才能申请锁,当释放锁的时候lock++;

互斥量用一条汇编来实现了CPU寄存器数据和内存数据的交换。

image.png

死锁

当一个线程在访问临界区资源的时候如果被切走了,那么它的上下文也被切走了,而上下文是带着锁的,这就导致了其他线程再想竞争锁就失败了,这就是死锁问题。

死锁的四个必要条件

互斥条件:一个资源每次只能被一个执行流使用

请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放

不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺

循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系

如果要破坏死锁,那么上面的条件失去一个就行了。

线程同步

条件变量

当一个线程互斥的访问其他变量的时候,它可能在发现其他线程改变状态之前,什么也做不了,例如一个线程在访问队列的时候,发现队列是空的,它只能等待,直到其他线程把一个节点添加到队列中,这种情况就要用到条件变量。

同步

同步实际上就是让线程按照一定的顺序访问临界资源

变量初始化
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict

attr);

参数:

cond:要初始化的条件变量

attr:NULL
变量销毁
int pthread_cond_destroy(pthread_cond_t *cond)
等待条件满足
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);

参数:

cond:要在这个条件变量上等待

mutex:互斥量,后面详细解释
唤醒等待
int pthread_cond_broadcast(pthread_cond_t *cond);

int pthread_cond_signal(pthread_cond_t *cond);

**为什么线程等待需要用到互斥量? **

如果只有一个线程,那么条件变量永远不会改变,所以要有额外线程来把共享数据进行改变,这个共享数据实际上就是一个临界资源,所以要互斥量进行保护。