首页 > 系统相关 >linux中的线程

linux中的线程

时间:2024-10-11 22:32:43浏览次数:7  
标签:调用 函数 取消 linux 互斥 线程 pthread

线程

一个进程可以包含多个线程。同一程序中的所有线程均会独立执行相同程序,且共享同一份全局内存区域,其中包括初始化数据段(initialized data)、未初始化数据段(uninitialized data),以及堆内存段(heap segment)

多线程的进程内存布局

4

文本段、数据段这些,线程共享,然后会为每个线程分配特定的栈,其中每个进程都有一个主线程,也就是main函数的执行线程

线程的数据类型

这些类型用于编写多线程程序,编译时需要加上 -lpthread
5

创建线程

进程启动时,会有一条初始线程,即主线程,所以创建线程指代的是创建除主线程之外的线程

6

需要指定该线程执行的函数

终止线程

线程的终止,如果没有 显式 的return或者exit,那么执行完代码后会正常退出

1

pthread_exit() 函数相当于return语句,可以退出线程的执行函数,即结束线程,不能直接使用exit函数,这会导致所有线程都终止

2

线程的连接

线程的连接,其实就是一个线程等待另一个线程执行完毕

若线程并未分离(detached),则必须使用 ptherad_join()来进行接。如果未能连接,那么线程终止时将产生僵尸线程,与僵尸进程(zombie process)的概念相类似。除了浪费系统资源以外,僵尸线程若累积过多,应用将再也无法创建新的线程。

7

  • 线程之间的关系是平等的,它不像进程的wait那样,只能由父进程等待子进程

  • 线程只能连接特定线程ID,即它只能连接它所知道的线程,这是为了避免,创建的线程连接库函数私自创建的线程等等带来的问题

线程的分离

线程的分离,默认情况下,线程是可连接的,当设置为分离状态后,该线程退出时,其他线程 无法使用pthread_join()来获取它的返回状态,系统会在其终止时自动清理并移除

需要注意的是任意一个线程exit或者主线程return,分离的线程也会立即终止,分离只是控制线程终止之后所发生的事情,而非何时或如何终止线程

8

补充

1

线程可以自行分离,但不能自行连接

pthread_detach(pthread_self())是合法的,pthread_join(pthread_self(), NULL)是非法的

当线程调用 pthread_join(pthread_self(), NULL) 时,实际上是在等待它自己终止,而这不可能完成,因为 当前线程无法在等待自身的同时继续运行,也就是说,调用者线程会被 阻塞,导致死锁

2

2

以上代码存在以下问题:

  • 变量作用域和生命周期

    struct someStruct buf;
    

    在 main 函数中定义的 buf 是一个局部变量,具有 main 函数的作用域。当 main 函数执行完毕时,buf 的生命周期结束,buf 的内存将不再有效

    如果 threadFunc 线程在 main 函数结束之前还未执行完毕,可能会导致对已释放内存的访问,造成未定义行为

  • 线程未正确处理

    pthread_create(&thr, NULL, threadFunc, (void *) &buf);
    

    代码中调用 pthread_create 创建线程,但是缺少 pthread_join 或适当的退出处理,主线程直接调用了 pthread_exit(NULL),这可能会导致主线程退出,threadFunc 线程可能未能正确运行完毕

3

在Linux中,当主线程(即main线程)退出后,如果其他线程仍在执行,整个进程的行为取决于主线程的退出方式和其他线程的状态。以下是几种情况的详细说明:

  • 主线程先于其他线程退出

    • 主线程正常退出

      • 使用returnexit():
        • 当主线程正常退出时,进程会被终止,所有的子线程(包括正在运行的其他线程)都会被强制终止
        • 这种情况下,线程的清理工作不会被执行,可能导致未释放的资源(如内存或文件描述符)
    • 主线程调用pthread_exit()

      • 如果主线程调用pthread_exit(),则该线程会退出,但进程不会立即终止。其他线程仍然会继续执行
      • 在这种情况下,进程会保持运行状态,直到所有线程都退出或者调用pthread_cancel()等其他方式强制终止
    • 主线程被其他线程取消

      • 如果主线程被其他线程调用pthread_cancel(),主线程会被终止。与正常退出相似,所有未完成的子线程也会被强制终止
  • 线程的状态和清理

    • 如果主线程退出,其他线程会根据其状态决定是否继续执行。若主线程以pthread_exit()退出,其他线程将继续执行,但进程最终仍会在所有线程完成后退出
    • 需要注意的是,其他线程可能会遭遇资源泄露,因为主线程的退出可能导致无法正确释放资源
  • 使用pthread_join()

    • 为了避免资源泄露,通常在主线程中调用pthread_join()等待其他线程完成。这样,主线程会在其他线程退出后再返回,确保所有线程正常结束和资源释放

线程同步

临界区

临界区就是指访问共享资源的代码段,以下是几个关键概念:

  • 共享资源

    共享资源可以是任何被多个线程访问的数据或状态,例如全局变量、静态变量、动态分配的内存等

  • 互斥访问

    为了保护临界区,必须确保 对临界区的访问是互斥的,即在任一时刻只能有一个线程可以进入临界区

    互斥的实现通常使用锁机制,例如互斥锁(mutex)或读写锁

  • 临界区的必要性

    当多个线程同时读写同一共享资源时,如果不加以控制,就会导致数据竞争(Race Condition),使得程序的行为不可预测,甚至可能崩溃

互斥量

互斥量可以帮助线程同步对共享资源的使用,以防如下情况发生:线程某甲试图访问一共享变量时,线程某乙正在对其进行修改。

互斥量的初始化

  • 静态分配
  // 全局变量
  static pthread_mutex_t mutex = PTHREAT_MUTEX_INITIALIZER;
 静态初始化的互斥量,存储在全局区,无需显示的销毁互斥量,程序结束后会自动回收,简单方便
  • 动态初始化

11

对于以上的三种情况要使用动态初始化,即使用以下函数初始化,并且要显示的销毁

12

互斥量的销毁

12

只有动态初始化的互斥量需要使用该函数进行显式销毁,静态分配的互斥量会自动回收

互斥量的行为(加锁、解锁)

每个线程访问同一资源的时候,都应该遵循以下步骤:

  • 针对共享资源锁定互斥量
  // 加锁
  int pthread_mutex_lock(pthread_mutex_t *mutex);
  • 访问共享资源
  /* 访问共享资源的代码 */
  • 对互斥量解锁
  // 解锁
  int pthread_mutex_unlock(pthread_mutex_t *mutex);

对互斥量的加锁解锁也会带来一定的开销,所以不要多次的加锁解锁,若是一次加锁解锁就能满足线程的访问需求更好,比如说如果是循环方式的话,可以考虑把加锁解锁放到循环之外

需要注意某些对于互斥量的非法行为:

10

流程图如下,互斥量同一时间只能由一个线程持有,其他线程试图持有会被阻塞
9

互斥量死锁

13

其中每个线程都成功地锁住一个互斥量,接着试图对已为另一线程锁定的互斥量加锁。两个线程将无限期地等待下去

条件变量

条件变量允许线程相互通知共享变量(或其他共享资源)的状态发生了变化

他的作用是,当一个线程不断的循环检测某一个共享资源的状态,这会造成cpu的资源浪费,可以使用条件变量,允许一个线程休眠,直到接收到另一线程的通知才取执行某些操作

条件变量的初始化

  • 静态分配
  // 全局变量
  static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

无需显式销毁

  • 动态初始化

    条件和互斥量动态初始化类似,需要显式销毁
    13

条件变量的销毁

14

对某个条件变量而言,仅当没有任何线程在等待它时,将其销毁才是安全的

条件变量的行为(通知、等待)

  • 通知
  int pthread_cond_signal(pthread_cond_t *cond); // 唤醒一个等待的线程
  int pthread_cond_broadcast(pthread_cond_t *cond); // 唤醒所有等待的线程
  • 等待
  int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex); // 阻塞线程(调用者)直到接到通知

条件变量与互斥变量的联系

15

函数pthread_cond_wait()会自动执行最后两步中对互斥量的解锁和加锁动作,即这里的3和4属于原子操作

换句话说:

  • 当线程陷入休眠时,pthread_cond_wait会自动解锁持有的互斥量
  • 当线程被唤醒时,pthread_cond_wait会自动加锁需要的互斥量

测试条件变量的判断条件

每个条件变量都有与之相关的判断条件,涉及一个或多个共享变量

从pthread_cond_wait返回时,即线程被唤醒,并不能确定判断条件的状态,需要立即重新检查,原因如下:

  • 其他进程率先醒来,修改了共享变量,和预期不符合
  • 虚假唤醒

所以要采用循环检测的方法,而不是用if,如下:

  // 不要用if
  if (var == 0) 
    pthread_cond_wait(&cond, &mtx);

  // 使用while
  while (var == 0)
    pthread_cond_wait(&cond, &mtx);

线程安全

若函数可同时供多个线程安全调用,则称之为线程安全函数;反之,如果函数不是线程安全的,则不能并发调用。

实现线程安全的方式

  • 将函数与互斥量关联使用

    在调用函数时将其锁定,在函数返回时解锁,这意味着一次只能有一个线程执行该函数,即 串行执行,虽然简单,但导致了多线程并行能力的损失

  • 将共享变量与互斥量关联起来

    在执行到临界区时,才获取或释放互斥量,较为复杂,但保留了线程的并行能力

  • 可重入

    可重入函数则无需使用互斥量即可实现线程安全。其要诀在于避免对全局和静态变量的使用。需要返回给调用者的任何信息,亦或是需要在对函数的历次调用间加以维护的信息,都存储于由调用者分配的缓冲区内。

一次性初始化

一次性初始化就是,只会初始化一次,无论调用几次,由以下函数实现:

15

利用参数 once_control 的状态,函数 pthread_once()可以确保无论有多少线程对pthread_once()调用了多少次,也只会执行一次由init指向的调用者定义函数。

// 全局变量
static pthread_once_t once_control = PTHREAD_ONCE_INIT;
  • once_control 是一个控制变量,用于跟踪初始化是否已经完成
  • 初始化函数 init: 这个函数负责执行只需一次的初始化逻辑,例如设置共享资源的值

每线程存储

每线程存储,就是每个线程到单独存储要访问的数据,避免了资源竞争,也就不需要用互斥量了,实现了可重入,也就是线程安全

线程特有数据

16

线程特有数据使函数得以为每个调用线程分别维护一份变量的副本(copy)。线程特有数据是长期存在的。在同一线程对相同函数的历次调用间,每个线程的变量会持续存在,函数可以向每个调用线程返回各自的结果缓冲区(如果需要的话)。

线程局部存储

类似于线程特有数据,线程局部存储提供了持久的每线程存储。

线程局部存储的主要优点在于,比线程特有数据的使用要简单。要创建线程局部变量,只需简单地在全局或静态变量的声明中包含__thread说明符即可。

但凡带有这种说明符的变量,每个线程都拥有一份对变量的拷贝。线程局部存储中的变量将一直存在,直至线程终止,届时会自动释放这一存储。

// 示例
// 全局变量
static __thread int var1;
extern __thread int var2;
__thread int var3;

使用线程局部变量需要注意以下几个:

17

线程取消

发送取消请求

17

函数pthread_cancel()向由 thread 指定的线程发送一个取消请求。发出取消请求后,函数pthread_cancel()当即返回,不会等待目标线程的退出。

取消状态及类型

取消状态和取消类型,决定了目标线程的响应

取消状态

int pthread_setcancelstate(int state, int *oldstate);

函数 pthread_setcancelstate()会将调用线程的取消性状态置为参数 state 所给定的值。

  • state参数
    • PTHREAD_CANCEL_DISABLE 不可取消状态

      线程不可取消。如果此类线程收到取消请求,则会将请求挂起,直至将线程的取消状态置为启用。

    • PTHREAD_CANCEL_ENABLE 取消状态

      线程可以取消。这是新建线程取消性状态的默认值。
      线程的前一取消性状态将返回至参数oldstate所指向的位置。

取消类型

只有当线程的取消状态设置为启用时,那么对目标线程对取消请求的处理取决于取消类型

int pthread_setcanceltype(int type, int *oldtype);
  • type参数
    • PTHREAD_CANCEL_ASYNCHRONOUS 异步取消

      可能会在任何时点(也许是立即取消,但不一定)取消线程

    • PTHREAD_CANCEL_DEFERRED 延迟取消

      取消请求保持挂起状态,直至到达取消点,也是默认类型

取消状态及类型对进程的影响

当某线程调用 fork()时,子进程会继承调用线程的取消性类型及状态。而当某线程调用exec()时,会将新程序主线程的取消性类型及状态分别重置为PTHREAD_CANCEL_ ENABLE和PTHREAD_CANCEL_DEFERRED。

取消点

若将线程的取消性状态和类型分别置为启用和延迟,仅当线程抵达某个取消点(cancellation point)时,取消请求才会起作用。

常见取消点:

  • 阻塞函数,如pthread_join()、sleep()等

  • 手动设置取消点,可以在需要的地方插入取消检查,例如通过调用pthread_testcancel(),这会产生一个取消点,和那些阻塞函数的取消点的作用是一样的,线程执行到该位置,若有取消请求,则线程会直接终止

    19

清理函数

一旦有处于挂起状态的取消请求,线程在执行到取消点时如果只是草草收场,这会将共享变量以及Pthreads 对象(例如互斥量)置于一种不一致状态,可能导致进程中其他线程产生错误结果、死锁,甚至造成程序崩溃。

线程可以设置一个或多个清理函数,当线程 取消 时会自动执行这些执行函数,进行资源的释放等等

每个线程都可以拥有一个清理函数栈。当线程遭取消时,会沿该栈自顶向下依次执行清理函数,首先会执行最近设置的函数,接着是次新的函数,以此类推。当执行完所有清理函数后,线程终止。

20

通常,只有当线程被取消时才会执行设置的清理函数,正常结束是不需要执行这些清理动作的

线程补充

线程栈

创建线程时,每个线程都有一个属于自己的线程栈,且大小固定。

和进程布局中的栈是差不多的意思,有时候也可以通过也写函数改变线程栈的大小

线程与信号

概述

UNIX 信号模型早于 Pthreads 设计,信号与线程模型之间存在冲突。主要挑战 在于保持传统信号语义,同时支持多线程环境的新信号模型。在多线程程序中处理信号时要特别小心,以避免复杂性

UNIX 信号模型与线程的映射

  1. 信号动作

    • 信号动作是进程层面的。若任一线程收到未经处理的信号(如 SIGSTOP 或 SIGTERM),则所有线程将被停止或终止。
    • 进程中的所有线程共享信号处理设置。使用 sigaction() 设置的信号处理程序会在收到信号时被所有线程调用。
  2. 信号发送

    • 信号可以发送给整个进程或特定线程。以下三种信号属于线程特定:
      • 由硬件异常引起的信号(如 SIGBUS、SIGFPE)。
      • 由于对断开管道的写操作引发的 SIGPIPE。
      • 通过 pthread_kill()pthread_sigqueue() 发送的信号。
    • 其他机制(如用户输入、定时器到期等)产生的信号则是进程级的。
  3. 信号处理

    • 当多线程程序收到信号且存在处理程序时,内核会 随机选择一条线程 来处理该信号。
  4. 信号掩码

    • 信号掩码是线程特有的,使用 pthread_sigmask() 可以独立阻止或允许信号。
    • 每个线程初始时的挂起信号集合为空,可以发送信号给特定线程,如果被阻塞,则会保持挂起状态。
  5. 信号处理函数

    • 如果信号处理程序中断了 pthread_mutex_lock(),该调用会自动重新开始。
    • pthread_cond_wait() 的中断可能导致自动重新开始或返回 0(假唤醒),应用程序需要重新检查条件。
  6. 备选信号栈

    • 每个线程都有独立的备选信号栈,且新线程不继承创建者的信号栈。

信号动作与信号处理的区别:

  • 信号动作是系统对信号的预设响应行为(如忽略、终止或执行处理函数),而信号处理则是具体的代码实现,即对信号的响应逻辑
  • 信号动作是进程共享的,而信号处理程序可以在各个线程中独立执行(即信号动作是进程级的,信号处理是线程级的)
  • 粗略的理解就是,信号动作是 要做什么, 信号处理是 具体怎么做

线程和进程控制

以下是关于多线程程序中exec()fork()exit()的关键点总结:

线程与 exec()

  • 行为:当任一线程调用exec()系列函数时,当前程序将被完全替换。
  • 影响:调用exec()的线程以外的所有线程将消失,且不会执行线程特有数据的解构函数或清理函数。
  • 结果:该进程的互斥量和条件变量将被丢弃,调用线程的线程ID变为不确定。

线程与 fork()

  • 行为:当多线程进程调用fork()时,仅复制调用该函数的线程到子进程中。

  • 影响

    • 其他线程在子进程中消失,且不调用清理函数或解构函数。
    • 全局变量和Pthreads对象在子进程中保留,会导致潜在的内存泄漏和不一致状态。
    • 如果父进程中有线程锁定互斥量,子进程中的线程无法解锁该互斥量。
  • 建议:在多线程程序中调用fork()的唯一推荐情况是紧接着调用exec()

  • 处理机制:使用pthread_atfork()注册fork处理函数,确保在调用fork()前后进行必要的准备和清理。

线程与 exit()

  • 行为:如果任一线程调用exit()或主线程执行return,所有线程都将终止。
  • 影响:不会执行线程特有数据的解构函数或清理函数。

总结

  • exec()会导致线程和相关资源消失。
  • fork()可能导致子进程中的状态不一致,推荐后接exec()
  • exit()将终止所有线程,且不执行清理操作。

线程实现模型

实现模型的差异主要集中在线程如何与内核调度实体(KSE,Kernel Scheduling Entity)相映射。KSE是内核分配CPU以及其他系统资源的(对象)单位。

以下是关于线程实现模型的简要总结:

线程实现模型

1. 多对一 (M:1) 实现(用户级线程)

  • 概述:所有线程管理(创建、调度、同步)由用户空间的线程库处理,内核对线程不可见。
  • 优点
    • 快速的线程操作(创建、终止、上下文切换)因为无需切换到内核模式。
    • 移植性好,无需内核支持。
  • 缺点
    • 一旦某线程进行阻塞系统调用(如read()),所有线程都将被阻塞。
    • 内核无法在多处理器平台上调度这些线程。

2. 一对一 (1:1) 实现(内核级线程)

  • 概述:每个线程映射一个独立的KSE,内核对每个线程进行调度。
  • 优点
    • 阻塞的系统调用不会影响其他线程,内核可在多处理器上调度多个线程。
  • 缺点
    • 线程创建和上下文切换速度较慢,需要切换到内核模式。
    • 大量线程会增加内核调度器的负担,可能影响系统性能。
  • 应用:LinuxThreads和NPTL采用此模型。

3. 多对多 (M:N) 实现(两级模型)

  • 概述:多个线程可以映射到多个KSE,结合了M:1和1:1的优点。
  • 优点
    • 允许内核将同一应用的线程调度到不同CPU上,避免性能问题。
  • 缺点
    • 复杂性高,线程调度由内核和用户线程库共同管理,需要协作和信息交换。
    • 管理信号的复杂度增加。

Linux中POSIX线程的实现

1. LinuxThreads

  • 概述:LinuxThreads 是早期的POSIX线程实现,最初由Xavier Leroy开发,旨在为Linux提供对POSIX线程标准的支持。
  • 实现特点
    • 采用 1:1 模型,即每个用户级线程都有一个对应的内核级线程。
    • 提供基本的线程管理功能,如创建、终止、同步和调度。
  • 缺点
    • 性能相对较低,尤其在大量线程的情况下,调度和管理开销较大。
    • 存在一些稳定性问题,尤其是在与信号处理相关的功能上。

2. NPTL (Native POSIX Thread Library)

  • 概述:NPTL 是对 LinuxThreads 的重写,目的是提供更高效、更可靠的线程实现。它是自Linux 2.6内核开始的默认线程库。
  • 实现特点
    • 采用 1:1 模型,内核能够更好地支持线程调度,允许每个线程独立进行系统调用。
    • 提供了更好的性能和可扩展性,特别是在多处理器环境下。
    • 改进了信号处理机制,确保了更高的稳定性。
  • 优点
    • 能够有效地管理成千上万的线程,减少了调度开销。
    • 支持POSIX线程标准的完整实现,兼容性更好。

总结

  • LinuxThreads 是较早的线程实现,提供基本的线程支持,但存在性能和稳定性的问题。
  • NPTL 则是现代Linux系统中使用的高效线程库,解决了前者的许多缺陷,支持更复杂的多线程应用。

标签:调用,函数,取消,linux,互斥,线程,pthread
From: https://www.cnblogs.com/dylaris/p/18447866

相关文章

  • NOI Linux使用指南
    快捷键Ctrl+Alt+T:打开终端命令ls:列出当前文件夹下所有文件cd[文件夹名称]:进入某个文件夹mkdir[文件夹名称]:新建文件夹touch[文件名称]:新建文件g++x.cpp-oy[编译选项]:生成x.cpp的已编译文件y。编译选项(可叠加):-O2:开启O2-std=c++11:使用C++11编译-......
  • [linux] 使用Screen后台运行命令
    概述Screen需要下载,常用来后台运行程序。比如后台运行一个nodejs项目、mc服务器等。下载在centos中,yuminstallscreen;在ubuntu中,aptinstallscreen。使用screen-h查看帮助文档查看所有会话screen-lsdaohe@neko:~/MC/Server$screen-lsTherearescreenson:......
  • Linux文件和文件夹操作
    一、文件操作(一)文件创建命令行作用vi/opt/learn/1.txt在目录/opt/learn下创建1.txt并进入vi界面touch/opt/learn/test在目录/opt/learn下创建空白文件testcat>/opt/learn/catfile创建文件catfile并在屏幕上输入内容,最后按Crtl+D退出(二)文件查看命令行作用vi/etc/pa......
  • 在Linux中搭建WordPress并实现Windows主机远程访问
      WordPreWordPress是一个基于PHP开发的开源平台,适用于在支持PHP与MySQL数据库的服务器上搭建个性化博客或网站。同时,它也能够作为功能强大的内容管理系统(CMS)被广泛应用。虚拟机:VirtualBox虚拟机安装......
  • gdb多线程多进程调试命令
    多线程infothreads查看当前所有运行线程的列表thread线程编号 切换到特定线程进行调试setscheduler-lockingon只运行当前线程,停止其他线程进行调试多进程infoinferions显示所有正在调试的进程inferion进程编号 切换到特定进程运行,同时挂起其他进程detach-on-fo......
  • Linux下以编译源码的方式安装Qt5与Qt6及其使用
    文章目录概要资源下载依赖安装编译Qt5Qt6遇到的问题qtchooser使用概要自Qt5.15开始,不再提供opensourceofflineinstallers,也就是原来的.run的安装文件,只能通过源码编译来安装了参考文章资源下载源码网址,链接为Qt的资源,根据自己选择下载例如#下载源码......
  • Java并发编程-线程池
    ThreadLocal应用场景:两个线程争执一个资源。解决问题:实现每个线程绑定自己的专属本地变量,可以将ThreadLocal类理解成存放数据的盒子,盒子中存放每个线程的私有数据。线程池的用途选择快速响应用户请求:比如说用户查询商品详情页,会涉及查询商品关联的一系列信息如价格、优......
  • WSL(Windows Subsystem for Linux)——简单的双系统开发
    文章目录WSLWSL的作用WSL的使用WSL的安装挂载磁盘的作用安装linux发行版wsl下载mysql,mongodb,redisWSL前言:本人由于在开发中需要linux环境,同时还想要直接在Windows下开发,来提升开发效率,随即简单学习WSL。WSL(WindowsSubsystemforLinux)是微软开发的一项技术,允许用......
  • Vector线程安全问题
    背景在韩顺平的Java课程中,有一个坦克大战练习项目,其中有这样一个功能需求:敌人坦克自动发射多个子弹,检测子弹是否击中我方坦克。视频中使用的是Vector存储这个子弹队列。代码实现对于这一部分,我的实现代码是://MyPanel.java的run()方法while(true){try{Thr......
  • C# 线程---Thread1
     1.thread不带参数(Main和Thread都在同步处理)(注意usingstatic和System.Console的使用)usingstaticSystem.Console;namespaceRecipe1{classProgram{staticvoidMain(string[]args){Threadt=newThread(PrintNumber);......