Linux_深究多线程_(线程等待,线程终止,线程分离,线程互斥,可重入,线程安全)

  • 在前面的博客【Linux_初始多线程—>Link】中已经讲了有关多线程的概念和基本知识。这章将接着上一章的内容对线程操作以及其他概念继续深究。

1. 主 / 新线程

Linux_深究多线程_(线程等待,线程终止,线程分离,线程互斥,可重入,线程安全)
  • 从上图中可得知main()函数中,函数pthread_create()往下的为主线程,而函数pthread_create()创造的线程为新线程。接下来的内容将围绕主/新线程讲解。
  • 注意线程之间谁先执行是不确定的,而跟系统调度有关。(线程是调度的基本单位,系统调度的线程标识是LWP)

1.1 主 / 新线程退出

  • 主线程或者新线程各自独立退出时会有两种情况
  1. 主线程没有等待新线程结束而提前退出,则整个进程退出
    问题:没有等待新线程退出而进程退出,新线程也是该进程中的一个执行流,会造成内存泄漏。
Linux_深究多线程_(线程等待,线程终止,线程分离,线程互斥,可重入,线程安全)
  1. 新线程退出而主线程不退出。主线程继续执行
Linux_深究多线程_(线程等待,线程终止,线程分离,线程互斥,可重入,线程安全)

2.线程等待

  • 为什么需要线程等待?
  1. 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。(如上面的情况1
  2. 创建新的线程不会复用刚才退出线程的地址空间。
int pthread_join(pthread_t thread, void **value_ptr); 

参数

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

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

调用该函数的线程将挂起等待,直到id为thread的线程终止。
Linux_深究多线程_(线程等待,线程终止,线程分离,线程互斥,可重入,线程安全)

3.线程终止

  • 如果需要只终止某个线程而不终止整个进程,可以有三种方法:
  1. 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit
  2. 线程可以调用pthread_ exit终止自己
  3. 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程
  • 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。在main()函数中return则是结束的整个函数。如果在调用的函数中return则只是结束这个函数。而exit()是不管在那个位置,都直接结束整个函数。线程也是如此。
    Linux_深究多线程_(线程等待,线程终止,线程分离,线程互斥,可重入,线程安全)

  • pthread_exit()函数

void pthread_exit(void *value_ptr) 

参数

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

返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)。

Linux_深究多线程_(线程等待,线程终止,线程分离,线程互斥,可重入,线程安全)
  • pthread_ cancel()函数
int pthread_cancel(pthread_t thread); 

参数:

  • thread:线程ID

返回值

  • 成功返回0,失败返回错误码。
    Linux_深究多线程_(线程等待,线程终止,线程分离,线程互斥,可重入,线程安全)
  • 为什么被pthread_cancel()退出以后退出码是-1呢

答:用pthreat_cancel()函数退出别的别线程以后,该线程是被异常终止掉的,value_ ptr所指向的单元里存放的是常数PTHREAD_CANCELED。该函数是一个宏定义。就是-1
Linux_深究多线程_(线程等待,线程终止,线程分离,线程互斥,可重入,线程安全)

4.部分总结

  • thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:
  1. 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
  2. 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_CANCELED
  3. 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。
  4. 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参

5.线程分离

  • 当不需要关心某个线程执行结果如何,或者执行的对与错,就可将这个线程进行分离。线程分离带来的最直观的感受就是主线程不需要关心这个线程的执行如何。
  1. 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
  2. 如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
  3. 可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:
  • 在别的线程中分离
int pthread_detach(pthread_t thread); 
  • 自己分离自己
pthread_detach(pthread_self()); 
Linux_深究多线程_(线程等待,线程终止,线程分离,线程互斥,可重入,线程安全)

6.线程分离总结

  1. 线程分离退出码是0,而不是线程返回的退出码,是因为将线程分离以后,拿不到线程的退出码,也不需要拿到它的退出码。
  2. 线程分离以后,被分离的线程会自动释放自己的资源。不需要被等待,退出码也不会写进PCB。
  3. 但是线程被分离以后,如果线程中有异常,还是会影响当前进程。(举例:分离以后,各过各的,但是我出现问题,还是会影响你)。

7.线程互斥

7.1 线程间相关概念

  1. 临界资源:多线程执行流共享的资源就叫做临界资源。(共享数据)
  2. 临界区:每个线程内部,访问临界区域的代码,就叫做临界区。(共享代码)
  3. 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
  4. 原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成。
Linux_深究多线程_(线程等待,线程终止,线程分离,线程互斥,可重入,线程安全)

7.2 互斥量mutex

  1. 大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。
  2. 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。
  3. 多个线程并发的操作共享变量,会带来一些问题
    Linux_深究多线程_(线程等待,线程终止,线程分离,线程互斥,可重入,线程安全)
  • 要解决以上问题,需要做到三点
  1. 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
  2. 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
  3. 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。

注意:要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量

7.3 互斥量的接口

初始化互斥量

  • 初始化互斥量有两种方法:
  1. 静态分配:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER 
  1. 动态分配:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr); 

参数

  • mutex:要初始化的互斥量
  • attr:NULL

销毁互斥量

int pthread_mutex_destroy(pthread_mutex_t *mutex)

销毁互斥量需要注意

  1. 使用PTHREAD_ MUTEX_ INITIALIZER初始化的互斥量不需要销毁
  2. 不要销毁一个已经加锁的互斥量
  3. 已经销毁的互斥量,要确保后面不会有线程再尝试加锁

互斥量加锁和解锁

int pthread_mutex_lock(pthread_mutex_t *mutex); int pthread_mutex_unlock(pthread_mutex_t *mutex); 

返回值:

  • 成功返回0,失败返回错误号。

调用pthread_ lock 时,可能会遇到以下情况:

  1. 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功。
  2. 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。

7.4 代码示例

Linux_深究多线程_(线程等待,线程终止,线程分离,线程互斥,可重入,线程安全)

7.5 互斥量实现原理

  1. 经过上面的例子,如果不是原子的,有可能会有数据出现问题。
  2. 为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。 锁的原子性详解->link

8. 互斥量总结

  1. 对临界区进行保护,所有的执行线程都必须遵守这个规则。
  2. 基本过程:lock->访问临界区->unlock。锁的原子性详解->link
  3. 所有的线程必须先看到同一把锁,锁本身就是临界资源!锁本身得先保证自身安全!申请锁的过程,不能有中间状态,也就是两态的,lock->原子性,unlock->原子性。
  4. lock->访问临界区(花时间)->unlock,在特定线程/进程拥有锁的时候,期间有新线程过来申请锁,一定不能申请到,那么新线程进行阻塞,将进程,线程对应的PCB投入到等待队列。
  5. 加锁时候效率变低,本来是并行/并发执行流,加锁以后成了串行执行流
  6. 一次保证只有一个线程进入临界区,访问临界资源,就叫做互斥

9. 可重入 & 线程安全

9.1 概念

  • 线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,
    并且没有锁保护的情况下,会出现该问题。
  • 重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们
    称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重
    入函数,否则,是不可重入函数

9.2 常见的线程不安全的情况

  1. 不保护共享变量的函数
  2. 函数状态随着被调用,状态发生变化的函数
  3. 返回指向静态变量指针的函数
  4. 调用线程不安全函数的函数

9.3 常见的线程安全的情况

  1. 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
  2. 类或者接口对于线程来说都是原子操作
  3. 多个线程之间的切换不会导致该接口的执行结果存在二义性

9.4 常见不可重入的情况

  1. 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
  2. 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
  3. 可重入函数体内使用了静态的数据结构

9.5 常见可重入的情况

  1. 不使用全局变量或静态变量
  2. 不使用用malloc或者new开辟出的空间
  3. 不调用不可重入函数
  4. 不返回静态或全局数据,所有数据都有函数的调用者提供
  5. 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

9.6 可重入与线程安全联系

  1. 函数是可重入的,那就是线程安全的
  2. 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
  3. 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的

9.7 可重入与线程安全区别

  1. 可重入函数是线程安全函数的一种
  2. 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
  3. 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的