LINUX线程操作

文章目录

一、 线程的定义

线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。
线程是独立调度和分派的基本单位。线程可以为操作系统内核调度的内核线程,如Win32线程;由用户进程自行调度的用户线程,如Linux平台的POSIX Thread;或者由内核与用户进程,如Windows 7的线程,进行混合调度。
同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。
一个进程可以有很多线程,每条线程并行执行不同的任务。
在多核或多CPU,或支持Hyper-threading的CPU上使用多线程程序设计的好处是显而易见,即提高了程序的执行吞吐率。在单CPU单核的计算机上,使用多线程技术,也可以把进程中负责I/O处理、人机交互而常被阻塞的部分与密集计算的部分分开来执行,编写专门的workhorse线程执行密集计算,从而提高了程序的执行效率。

以上是百度给出的线程的定义,我们可以了解到:
1)线程是进程的一个分支,是一个单一的顺序流程
2)一个进程最少拥有一个线程==>主线程即程序本身
3)同进程的多个线程共享进程的的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。
4)每个线程拥有自己的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。

二、 LINUX中的线程模型

linux 最开始使用的线程是linuxThreads, 但是linuxThreads不符合POSIX标准, 后来出现了NGPT, 性能更高, 之后又出现了NPTL, 比NGPT更快, 随着时间推移, 就只剩下NPTL了。
线程的模型分为三种:
多对一(M:1)的用户级线程模型
一对一(1:1)的内核级线程模型: 例如linuxThreads和NPTL
多对多(M:N)的两极线程模型: 例如NGPT

查看当前系统使用的进程库

getconf GNU_LIBPTHREAD_VERSION

wangju@wangju-virtual-machine:/usr/include$ getconf GNU_LIBPTHREAD_VERSION
NPTL 2.31
wangju@wangju-virtual-machine:/usr/include$ ^C

Ubuntu使用NPTL线程库,NPTL 是 LinuxThreads 的替代者,而且其符合了 POSIX 的标准,在稳定性和性能方面都有了很大的提升。和 LinuxThreads 一样,NPTL 采用了一对一的线程模型。

1、 一对一模型

一个用户线程对应一个内核线程。内核负责每个线程的调度,可以调度到其他处理器上面。
优点:
实现简单。
缺点:
对用户线程的大部分操作都会映射到内核线程上,引起用户态和内核态的频繁切换。
内核为每个线程都映射调度实体,如果系统出现大量线程,会对系统性能有影响。

2、 多对一模型

顾名思义,多对一线程模型中,多个用户线程对应到同一个内核线程上,线程的创建、调度、同步的所有细节全部由进程的用户空间线程库来处理。
优点:
用户线程的很多操作对内核来说都是透明的,不需要用户态和内核态的频繁切换。使线程的创建、调度、同步等非常快。
缺点:
由于多个用户线程对应到同一个内核线程,如果其中一个用户线程阻塞,那么该其他用户线程也无法执行。
内核并不知道用户态有哪些线程,无法像内核线程一样实现较完整的调度、优先级等。

3、 多对多模型

多对一线程模型是非常轻量的,问题在于多个用户线程对应到固定的一个内核线程。多对多线程模型解决了这一问题:m个用户线程对应到n个内核线程上,通常m>n。由IBM主导的NGPT采用了多对多的线程模型,不过现在已废弃。
优点:
兼具多对一模型的轻量
由于对应了多个内核线程,则一个用户线程阻塞时,其他用户线程仍然可以执行
由于对应了多个内核线程,则可以实现较完整的调度、优先级等
缺点:
实现复杂

三、 线程实现原理

首先明确进程与进程的基本概念:
1)进程是资源分配的基本单位
2)线程是CPU调度的基本单位
3)一个进程下可能有多个线程
4)线程共享进程的资源

linux用户态的进程、线程基本满足上述概念,但内核态不区分进程和线程。可以认为,内核中统一执行的是进程,但有些是“普通进程”(对应进程process),有些是“轻量级进程”(对应线程pthread或npthread),都使用task_struct结构体保存保存。使用fork创建进程,使用pthread_create创建线程。两个系统调用最终都都调用了do_dork,而do_dork完成了task_struct结构体的复制,并将新的进程加入内核调度。

普通进程需要深拷贝虚拟内存、文件描述符、信号处理等;而轻量级进程之所以“轻量”,是因为其只需要浅拷贝虚拟内存等大部分信息,多个轻量级进程共享一个进程的资源。

linux加入了线程组的概念,让原有“进程”对应线程,“线程组”对应进程,实现“一个进程下可能有多个线程”。
task_struct中,使用pgid标的进程组,tgid标的线程组,pid标的进程或线程。
一个进程组包含多个进程,一个进程包含一个线程组,一个线程组包含多个线程,所以tgid相同的task_struct属于同一个线程组,即属于同一个进程(pid == tgid)。
在一个线程组内 线程id pid=tgid(线程组号)的是主线程,其余线程地位相等。
因此,调用getpgid返回pgid(主进程号),调用getpid应返回tgid(主线程号=进程号),调用gettid应返回pid(线程号)。
在这里插入图片描述

进程下除主线程外的其他线程是CPU调度的基本单位,这很好理解。而所谓主线程与所属进程实际上是同一个task_struct,也能被CPU调度,因此主线程也是CPU调度的基本单位。tgid相同的所有线程组成了概念上的“进程”,只有主线程在创建时会实际分配资源,其他线程通过浅拷贝共享主线程的资源。结合前面介绍的普通线程与轻量级进程,实现“进程是资源分配的基本单位”。

进程是一个逻辑上的概念,用于管理资源,对应task_struct中的资源,每个进程至少有一个线程,用于具体的执行,对应task_struct中的任务调度信息,以task_struct中的pid区分线程,tgid区分进程,pgid区分进程组

四、线程的管理

1、 线程的状态

(1)新建状态(New)

线程被创建后,但还没有调用start()方法。

(2)就绪状态(Runnable)

调用start()方法后,线程处于就绪状态,等待CPU调度。

(3)运行状态(Running)

线程获取CPU资源执行。

(4)阻塞状态(Blocked)

线程因为某些原因放弃CPU使用权,暂时停止运行。

(5)死亡状态(Dead)

线程执行完毕或被终止后的状态

2、 创建线程

#include <pthread.h>
typedef unsigned long int pthread_t;
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

参数:
pthread_t * :成功创建时接收线程id
pthread_attr_t :设置线程属性,一般为NULL。
void *(*start_routine) (void *):返回值为void参数为void的函数,线程的执行函数。
void *arg:给线程函数传参。
返回值:
成功:返回0;
失败:返回错误号 errno,并且第一个参数不会被设置

3、 线程的属性

创建线程时通过参数pthread_attr_t 设置线程的属性

union pthread_attr_t
{
  char __size[__SIZEOF_PTHREAD_ATTR_T];
  long int __align;
};
#ifndef __have_pthread_attr_t
typedef union pthread_attr_t pthread_attr_t;
# define __have_pthread_attr_t 1

typedef struct
{
       int                       detachstate;   // 线程的分离状态
       int                       schedpolicy;   // 线程调度策略
       structsched_param         schedparam;    // 线程的调度参数
       int                       inheritsched;  // 线程的继承性
       int                       scope;         // 线程的作用域
       size_t                    guardsize;     // 线程栈末尾的警戒缓冲区大小
       int                       stackaddr_set; // 线程的栈设置
       void*                     stackaddr;     // 线程栈的位置
       size_t                    stacksize;     // 线程栈的大小
} pthread_attr_t;

Posix线程中的线程属性pthread_attr_t主要包括scope属性、detach属性、堆栈地址、堆栈大小、优先级。在pthread_create中,把第二个参数设置为NULL的话,将采用默认的属性配置。
线程具有属性,用pthread_attr_t表示,在对该结构进行处理之前必须进行初始化,在使用后需要对其去除初始化。
调用pthread_attr_init之后,pthread_t结构所包含的内容就是操作系统实现支持的线程所有属性的默认值。
如果要去除对pthread_attr_t结构的初始化,可以调用pthread_attr_destroy函数。如果pthread_attr_init实现时为属性对象分配了动态内存空间,pthread_attr_destroy还会用无效的值初始化属性对象,因此如果经pthread_attr_destroy去除初始化之后的pthread_attr_t结构被pthread_create函数调用,将会导致其返回错误。

(1) 属性相关函数

  • 1、pthread_attr_init
    功能: 对线程属性变量的初始化。
  • 2、pthread_attr_setscope
    功能: 设置线程 __scope 属性。scope属性表示线程间竞争CPU的范围,也就是说线程优先级的有效范围。POSIX的标准中定义了两个值:PTHREAD_SCOPE_SYSTEM和PTHREAD_SCOPE_PROCESS,前者表示与系统中所有线程一起竞争CPU时间,后者表示仅与同进程中的线程竞争CPU。默认为PTHREAD_SCOPE_PROCESS。目前LinuxThreads仅实现了PTHREAD_SCOPE_SYSTEM一值。
  • 3、pthread_attr_setdetachstate
    功能: 设置线程detachstate属性。该表示新线程是否与进程中其他线程脱离同步,如果设置为PTHREAD_CREATE_DETACHED则新线程不能用pthread_join()来同步,且在退出时自行释放所占用的资源。缺省为PTHREAD_CREATE_JOINABLE状态。这个属性也可以在线程创建并运行以后用pthread_detach()来设置,而一旦设置为PTHREAD_CREATE_DETACH状态(不论是创建时设置还是运行时设置)则不能再恢复到PTHREAD_CREATE_JOINABLE状态。
  • 4、pthread_attr_setschedparam
    功能: 设置线程schedparam属性,即调用的优先级。
  • 5、pthread_attr_getschedparam
    功能: 得到线程优先级。
    原文链接:https://blog.youkuaiyun.com/houzijushi/article/details/80978345

4、 线程获取自身id

#include <pthread.h>
pthread_t pthread_self(void);

返回值:返回线程号,即对应task_struct 的 pid,并且该函数总是会成功

(1) 判断两个线程是否相等

int pthread_equal(pthread_t t1, pthread_t t2);

作用:比较两个线程ID是否相等。
返回值:相等返回非0值,不等返回0值。

(2) 判断两个线程是否相等有什么用??(待补充)

5、 线程终止

线程终止有三种方式
1)return
2)pthread_exit
3) pthread_cancel
默认属性的线程执行结束后并不会立即释放占用的资源,直到整个进程执行结束,所有线程的资源以及整个进程占用的资源才会被操作系统回收。实现线程资源及时回收的常用方法有两种,一种是修改线程属性,另一种是在另一个线程中调用 pthread_join() 函数。如果主线程提前结束,会终止所有的同组线程。

(1) return 终止

由上面的分析,我们了解到线程执行的是一个指定函数的内容,所以该函数return之后函数运行结束,调用它的线程也终止。return的值也可以被pthread_join函数接收。

(2) pthread_exit

#include <pthread.h>
 void pthread_exit(void *retval);

参数:
retval 是 void* 类型的指针,可以指向任何类型的数据,它指向的数据将作为线程退出时的返回值。如果线程不需要返回任何数据,将 retval 参数置为 NULL 即可。

注意,retval 指针不能指向函数内部的局部数据(比如局部变量)。换句话说,pthread_exit() 函数不能返回一个指向局部数据的指针,否则很可能使程序运行结果出错甚至崩溃。

(3) pthread_cancel

#include <pthread.h>
int pthread_cancel(pthread_t thread);

参数 :thread 指定需要取消的目标线程;成功返回 0,失败将返回错误码。
通过调用 pthread_cancel()库函数向一个指定的线程发送取消请求,要求指定线程终止。可以在一个线程中取消另一个线程。
*发出取消请求之后,函数 pthread_cancel()立即返回,不会等待目标线程的退出。默认情况下,目标线程也会立刻退出,其行为表现为如同调用了参数为 PTHREAD_CANCELED(其实就是(void )-1)的pthread_exit()函数,但是,线程可以设置自己不被取消或者控制如何被取消,所以 pthread_cancel()并不会等待线程终止,仅仅只是提出请求。

(4) pthread_exit 和 return的区别

无论是采用 return 语句还是调用 pthread_exit() 函数,主线程中的 pthread_join() 函数都可以接收到线程的返回值。return 语句和 pthread_exit() 函数的含义不同,return 的含义是返回,它不仅可以用于线程执行的函数,普通函数也可以使用;pthread_exit() 函数的含义是线程退出,它专门用于结束某个线程的执行。
此外,pthread_exit() 可以自动调用线程清理程序(本质是一个由 pthread_cleanup_push() 指定的自定义函数),return 则不具备这个能力。总之在实际场景中,如果想终止某个子线程执行,强烈建议大家使用 pthread_exit() 函数。终止主线程时,return 和 pthread_exit() 函数发挥的功能不同,可以根据需要自行选择。

6、 线程的回收

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

参数:
thread:要等待回收的线程号
retval:接收return或pthread_exit返回的参数
return:成功返回0,失败返回errno号。
当指定的线程已经终止时立即返回,否则阻塞等待。
使用pthread_join时线程必须是未分离的,否则不可用
当调用 pthread_join() 时,当前线程会处于阻塞状态,直到被调用的线程结束后,当前线程才会重新开始执行。当 pthread_join() 函数返回后,被调用线程才算真正意义上的结束,它的内存空间也会被释放(如果被调用线程是非分离的)。这里有三点需要注意:

  • 被释放的内存空间仅仅是系统空间,你必须手动清除程序分配的空间,比如 malloc() 分配的空间。
  • 一个线程只能被一个线程所连接(阻塞等待)。
  • 被连接的线程必须是非分离的,否则连接会出错。
    所以可以看出pthread_join()有两种作用:
    1)用于等待其他线程结束:当调用 pthread_join() 时,当前线程会处于阻塞状态,直到被调用的线程结束后,当前线程才会重新开始执行。
    2)对线程的资源进行回收:如果一个线程是非分离的(默认情况下创建的线程都是非分离)并且没有对该线程使用 pthread_join() 的话,该线程结束后并不会释放其内存空间,这会导致该线程变成了“僵尸线程”。
    原文链接:https://blog.youkuaiyun.com/yzy1103203312/article/details/80849831

7、 线程的分离

int pthread_detach(pthread_t thread);

将指定线程与指控线程分离,线程结束后(不会产生僵尸线程),其退出状态不由其他线程获取,而直接自己自动释放(自己清理掉PCB的残留资源)。
一般情况下,线程终止后,其终止状态一直保留到其它线程调用pthread_join获取它的状态为止(或者进程终止被回收了)。但是线程也可以被置为detach状态,这样的线程一旦终止就立刻回收它占用的所有资源,而不保留终止状态。不能对一个已经处于detach状态的线程调用pthread_join,这样的调用将返回EINVAL错误。

8、 线程的取消

(1)什么是取消点

1)pthread_cancel调用并不等待线程终止,它只提出请求。线程在取消请求(pthread_cancel)发出后会继续运行,直到到达某个取消点(CancellationPoint)。取消点是线程检查是否被取消并按照请求进行动作的一个位置,即取消点是线程判断是否可被取消的位置,程序产生了取消点,则可以被取消。

2)线程取消的方法是向目标线程发Cancel信号,但如何处理Cancel信号则由目标线程自己决定,或者忽略、或者立即终止、或者继续运行至Cancelation-point(取消点),由不同的Cancelation状态决定。

3)线程的取消点,也称为“中断点”或“cancel point”,是在程序设计中设定的一个特殊位置,其目的是允许外部请求暂停或者停止正在运行的线程。当线程到达这个点时,它会检查是否有被中断的情况发生,如果有,就会执行相应的中断处理逻辑。
作用主要有三个:
①响应中断:线程可能会在一个操作完成后检查是否需要中断,如网络请求、长时间计算等,以便及时响应中断信号,避免资源浪费。
②控制流程:通过设置取消点,程序员可以更好地管理复杂的异步任务,例如,在等待某个条件满足时,如果需要提前结束,可以在取消点上让线程退出。
③异常处理:作为一种优雅的方式,线程的取消点可以帮助捕获并处理非预期的终止请求,比如用户关闭应用程序时,主线程可以设置一个取消点来清理资源或记录日志。

取消点总结:取消点是线程stat=PTHREAD_CANCEL_ENABLE,type=PTHREAD_CANCEL_DEFFERED时,线程内部相应Cancel信号的位置。stat=PTHREAD_CANCEL_ENABLE,type=PTHREAD_CANCEL_ASYCHRONOUS时,立即响应Cancel信号,与取消点无关。
简单来说,取消点就是线程延迟响应cnacel信号的位置。

(2) 取消点的实现原理

下面我们看 Linux 是如何实现取消点的。(其实这个准确点儿应该说是 GNU 取消点实现,因为 pthread 库是实现在 glibc 中的。) 我们现在在 Linux 下使用的 pthread 库其实被替换成了 NPTL,被包含在 glibc 库中。以 pthread_cond_wait 为例,glibc-2.6/nptl/pthread_cond_wait.c 中:

/* Enable asynchronous cancellation. Required by the standard. */
cbuffer.oldtype = __pthread_enable_asynccancel ();
 
/* Wait until woken by signal or broadcast. */
lll_futex_wait (&cond->__data.__futex, futex_val);
 
/* Disable asynchronous cancellation. */
__pthread_disable_asynccancel (cbuffer.oldtype);

我们可以看到,在线程进入等待之前,pthread_cond_wait 先将线程取消类型设置为异步取消(__pthread_enable_asynccancel),当线程被唤醒时,线程取消类型被修改回延迟取消 __pthread_disable_asynccancel 。

这就意味着,所有在 __pthread_enable_asynccancel 之前接收到的取消请求都会等待 __pthread_enable_asynccancel 执行之后进行处理,所有在 __pthread_disable_asynccancel 之前接收到的请求都会在 __pthread_disable_asynccancel 之前被处理,所以真正的 Cancellation Point 是在这两点之间的一段时间。

原文链接 https://blog.youkuaiyun.com/m0_46535940/article/details/124908464

(3)取消点相关函数

①int pthread_cancel(pthread_t thread)

发送终止信号给thread线程,如果成功则返回0,否则为非0值。发送成功并不意味着thread会终止。

②int pthread_setcancelstate(int state, int *oldstate)

设置本线程对Cancel信号的反应**(动作),state有两种值:PTHREAD_CANCEL_ENABLE(缺省)和PTHREAD_CANCEL_DISABLE,分别表示收到信号后设为CANCLED状态忽略CANCEL信号**继续运行;old_state如果不为NULL则存入原来的Cancel状态以便恢复。

③int pthread_setcanceltype(int type, int *oldtype)

设置本线程取消动作的执行时机**(类型),type由两种取值:PTHREAD_CANCEL_DEFFERED和PTHREAD_CANCEL_ASYCHRONOUS,仅当Cancel状态为Enable时有效,分别表示收到信号后继续运行至下一个取消点再退出立即执行取消动作(退出)**;oldtype如果不为NULL则存入运来的取消动作类型值。

④void pthread_testcancel(void)

是说pthread_testcancel在不包含取消点,但是又需要取消点的地方创建一个取消点,以便在一个没有包含取消点的执行代码线程中响应取消请求.线程取消功能处于启用状态且取消状态设置为延迟状态时(表示 stat=PTHREAD_CANCEL_ENABLE type=PTHREAD_CANCEL_DEFFERED),pthread_testcancel()函数有效。如果在取消功能处处于禁用状态下调用pthread_testcancel(),则该函数不起作用。请务必仅在线程取消线程操作安全的序列中插入pthread_testcancel()。除通pthread_testcancel()调用以编程方式建立的取消点意外,pthread标准还指定了几个取消点。测试退出点,就是测试cancel信号。

(4)pthreads标准指定的取消点

(1)通过pthread_testcancel调用以编程方式建立线程取消点。
(2)线程等待pthread_cond_wait或pthread_cond_timewait()中的特定条件。
(3)被sigwait(2)阻塞的函数
(4)一些标准的库调用。通常,这些调用包括线程可基于阻塞的函数。

(5)取消线程带来的问题

在线程中使用同步机制,如果释放导致同步资源未释放,会导致等待资源的线程进入死等,产生死锁现象。
所以应该使用push、和pop进行资源清理。

(6)线程的资源清理

主线程可以通道 pthread_cancel 主动终止子线程,但是子线程中可能还有未被释放的资源,比如malloc开辟的空间。如果不清理,很有可能会造成内存泄漏。因此,为了避免这种情况,于是就有了一对线程清理函数 pthread_cleanup_push 和 pthread_cleanup_pop 。两者必须是成对存在的,否则无法编译通过。
pthread_cleanup_push 和 pthread_cleanup_pop都是宏定义

#  define pthread_cleanup_push(routine, arg) \
  do {                                        \
    __pthread_cleanup_class __clframe (routine, arg)

/* Remove a cleanup handler installed by the matching pthread_cleanup_push.
   If EXECUTE is non-zero, the handler function is called. */
#  define pthread_cleanup_pop(execute) \
    __clframe.__setdoit (execute);                        \
  } while (0)

可以看到只有成对出现时代码才是完整的push { , pop }。

①pthread_cleanup_push
void pthread_cleanup_push(void (*routine)(void *), void *arg);

第一个参数 routine:回调清理函数。当上面三种情况的任意一种存在时,回调函数就会被调用
第二个参数 args:要传递个回调函数的参数
pthread_cleanup_push 的作用是创建栈帧,设置回调函数,该过程相当于入栈。回调函数的执行与否有以下三种情况:
线程被取消的时候(pthread_cancel)
线程主动退出的时候(pthread_exit)
pthread_cleanup_pop的参数为非0值(pthread_cleanup_pop)

②pthread_cleanup_pop
void pthread_cleanup_pop(int execute);

当 execute = 0 时, 处在栈顶的栈帧会被销毁,pthread_cleanup_push的回调函数不会被执行
当 execute != 0 时,pthread_cleanup_push 的回调函数会被执行。
pthread_cleanup_pop 函数的作用是执行回调函数 或者 销毁栈帧,该过程相当于出栈。根据传入参数的不同执行的结果也会不同。

(7)线程资源清理时机

①在pthread_exit时清理

这里 pthread_cleanup_pop 函数的放置位置和参数需要注意:
必须放在 pthread_exit 后面,否则pthread_cleanup_pop会先清除栈帧,pthread_exit就无法调用清理函数了。pthread_cleanup_pop的参数是 0,因为pthread_cleanup_pop的参数为非0值时也会调用回调清理函数

void* pthread_cleanup(void* args){
	printf("线程清理函数被调用了\n");
}
 
void* pthread_run(void* args)
{
	pthread_cleanup_push(pthread_cleanup, NULL);
 
	pthread_exit((void*)1);     // 子线程主动退出
	pthread_cleanup_pop(0);     // 这里的参数要为0,否则回调函数会被重复调用
}
 
int main(){
	pthread_t tid;
	pthread_create(&tid, NULL, pthread_run, NULL);
	sleep(1);
 
	pthread_join(tid, NULL);
	return 0;
}
②pthread_cancel时清理

这里 pthread_cleanup_pop 函数的放置位置和参数需要注意,放置在取消点后,pop参数为0

void* pthread_cleanup(void* args){
	printf("线程清理函数被调用了\n");
}
 
void* pthread_run(void* args)
{
	pthread_cleanup_push(pthread_cleanup, NULL);
 
	pthread_testcancel();       // 设置取消点
	pthread_cleanup_pop(0);     // 这里的参数要为0,否则回调函数会被重复调用
}
 
int main(){
	pthread_t tid;
	pthread_create(&tid, NULL, pthread_run, NULL);
	pthread_cancel(tid);        // 取消线程
	sleep(1);
 
	pthread_join(tid, NULL);
	return 0;
}
③pthread_cleanup_pop的参数为非0值
void* pthread_cleanup(void* args){
	printf("线程清理函数被调用了\n");
}
 
void* pthread_run(void* args)
{
	pthread_cleanup_push(pthread_cleanup, NULL);
 
	pthread_cleanup_pop(1);     // 这里的参数为非0值
}
 
int main(){
	pthread_t tid;
	pthread_create(&tid, NULL, pthread_run, NULL);
	sleep(1);
 
	pthread_join(tid, NULL);
	return 0;
}

(8)不显式清理时,线程在什么时候清理资源

在C语言中,使用pthread库进行并发处理时,pthread_cleanup_push和pthread_cleanup_pop函数是用来管理清理上下文(cleanup context)的,它们主要用于指定在退出某个作用域时需要执行的清理操作。如果你没有调用pthread_cleanup_pop来弹出并执行之前设置的清理操作,那么这些清理任务通常会在以下几个情况下自动发生:

线程结束:当线程因为正常退出(如调用pthread_exit或其终止信号导致),系统会自动执行清理上下文中列出的任务。

异常退出:如果线程由于未捕获的错误或异常而被迫终止,清理操作将在内核层执行,尽管这可能会因平台而异。

栈溢出:如果线程的堆栈空间不足以完成当前的操作,可能导致栈溢出,这时清理可能不会被执行,取决于系统的行为。

但是,如果没有显式地通过pthread_cleanup_pop来控制清理顺序,不保证所有注册的清理操作一定会按预期执行,尤其是当线程提前中断时。因此,推荐在合适的位置调用pthread_cleanup_pop来管理和控制清理行为。如果不希望在特定条件下执行清理,可以手动清除清理队列,例如使用pthread_cleanup_destroy。

五、线程的同步

1、 什么是互斥与同步

数学上互斥指:
事件A和B的交集为空,A与B就是互斥事件,也叫互不相容事件。也可叙述为:不可能同时发生的事件。如A∩B为不可能事件(A∩B=Φ),那么称事件A与事件B互斥,其含义是:事件A与事件B在任何一次试验中不会同时发生。
同步指:
同步(英语:Synchronization),指对在一个系统中所发生的事件(event)之间进行协调,在时间上出现一致性与统一化的现象。在系统中进行同步,也被称为及时(in time)、同步化的(synchronous、in sync)。
在计算机中,
【同步】:

是指散步在不同任务之间的若干程序片断,它们的运行必须严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。最基本的场景就是:两个或两个以上的进程或线程在运行过程中协同步调,按预定的先后次序运行。比如 A 任务的运行依赖于 B 任务产生的数据。

【互斥】:

是指散步在不同任务之间的若干程序片断,当某个任务运行其中一个程序片段时,其它任务就不能运行它们之中的任一程序片段,只能等到该任务运行完这个程序片段后才可以运行。最基本的场景就是:一个公共资源同一时刻只能被一个进程或线程使用,多个进程或线程不能同时使用公共资源。

2、 什么是PV操作

PV两个字母是荷兰文 P asseren(通过),V rijgeven(释放)的简称。

PV操作与信号灯的处理相关,P表示通过的意思,V表示释放的意思。所谓信号灯,实际上就是用来控制进程状态的一个代表某一资源的存储单元。例如,P1和P2是分别将数据送入缓冲B和从缓冲B读出数据的两个进程,为了防止这两个进程并发时产生错误,狄克斯特拉设计了一种同步机制叫“PV操作”,P操作和V操作是执行时不被打断的两个操作系统原语。执行P操作P(S)时信号量S的值减1,若结果不为负则P(S)执行完毕,否则执行P操作的进程暂停以等待释放。执行V 操作V(S)时,S的值加1,若结果不大于0则释放一个因执行P(S)而等待的进程。对P1和P2可定义两个信号量S1和S2,初值分别为1和0。进程 P1在向缓冲B送入数据前执行P操作P(S1),在送入数据后执行V操作V(S2)。进程P2在从缓冲B读取数据前先执行P操作P(S2),在读出数据后执行V操作V(S1)。当P1往缓冲B送入一数据后信号量S1之值变为0,在该数据读出后S1之值才又变为1,因此在前一数未读出前后一数不会送入,从而保证了P1和P2之间的同步。PV操作属于进程的低级通信。

具体定义如下:

P(S):①将信号量S的值减1,即S=S-1;

       ②如果S30,则该进程继续执行;否则该进程置为等待状态,排入等待队列。

V(S):①将信号量S的值加1,即S=S+1;

       ②如果S>0,则该进程继续执行;否则释放队列中第一个等待信号量的进程。

3、 互斥锁实现线程同步

(1) 互斥锁的类型

互斥锁本质就是一个特殊的全局变量,拥有lock和unlock两种状态,unlock的互斥锁可以由某个线程获得,当互斥锁由某个线程持有后,这个互斥锁会锁上变成lock状态,此后只有该线程有权力打开该锁,其他想要获得该互斥锁的线程都会阻塞,直到互斥锁被解锁。

普通锁(PTHREAD_MUTEX_NORMAL)

互斥锁默认类型。当一个线程对一个普通锁加锁以后,其余请求该锁的线程将形成一个 等待队列,并在该锁解锁后按照优先级获得它,这种锁类型保证了资源分配的公平性。一个 线程如果对一个已经加锁的普通锁再次加锁,将引发死锁;对一个已经被其他线程加锁的普 通锁解锁,或者对一个已经解锁的普通锁再次解锁,将导致不可预期的后果。

检错锁(PTHREAD_MUTEX_ERRORCHECK)

一个线程如果对一个已经加锁的检错锁再次加锁,则加锁操作返回EDEADLK;对一个已 经被其他线程加锁的检错锁解锁或者对一个已经解锁的检错锁再次解锁,则解锁操作返回 EPERM。

嵌套锁(PTHREAD_MUTEX_RECURSIVE)

该锁允许一个线程在释放锁之前多次对它加锁而不发生死锁;其他线程要获得这个锁,则当前锁的拥有者必须执行多次解锁操作;对一个已经被其他线程加锁的嵌套锁解锁,或者对一个已经解锁的嵌套锁再次解锁,则解锁操作返回EPERM。

默认锁(PTHREAD_MUTEX_ DEFAULT)

一个线程如果对一个已经加锁的默认锁再次加锁,或者虽一个已经被其他线程加锁的默 认锁解锁,或者对一个解锁的默认锁解锁,将导致不可预期的后果;这种锁实现的时候可能 被映射成上述三种锁之一。

(2) 创建不同类型的锁

void *thread_func(void *arg) {
    pthread_mutexattr_t mutex_attr;
    int result;

    // 初始化互斥锁属性,并设置为递归模式
    pthread_mutexattr_init(&mutex_attr);
    pthread_mutexattr_settype(&mutex_attr, PTHREAD_MUTEX_RECURSIVE);

    pthread_mutex_init(&my_recursive_lock, &mutex_attr); // 创建递归锁

    while (condition) { // 模拟需要递归锁定的循环
        result = pthread_recursive_lock(&my_recursive_lock);
        if (result != 0) {
            // 错误处理
        }
        // 执行任务...
        pthread_recursive_unlock(&my_recursive_lock); // 当离开循环时释放锁
    }

    // 销毁锁
    pthread_mutex_destroy(&my_recursive_lock);

(3) 线程互斥锁的使用


// 静态方式创建互斥锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; 

// 动态方式创建互斥锁,其中参数mutexattr用于指定互斥锁的类型,具体类型见上面四种,如果为NULL,就是普通锁。
int pthread_mutex_init (pthread_mutex_t* mutex,const pthread_mutexattr_t* mutexattr);

int pthread_mutex_lock(pthread_mutex_t *mutex); // 加锁,阻塞
int pthread_mutex_trylock(pthread_mutex_t *mutex); // 尝试加锁,非阻塞
int pthread_mutex_unlock(pthread_mutex_t *mutex); // 解锁

4、 自旋锁实现线程同步

(1) 自旋锁

自旋锁顾名思义就是一个死循环,不停的轮询,当一个线程未获得自旋锁时,不会像互斥锁一样进入阻塞休眠状态,而是不停的轮询获取锁,如果自旋锁能够很快被释放,那么性能就会很高,如果自旋锁长时间不能够被释放,甚至里面还有大量的IO阻塞,就会导致其他获取锁的线程一直空轮询,导致CPU使用率达到100%,特别CPU时间。

(2) 自旋锁的使用

int pthread_spin_init(pthread_spinlock_t *lock, int pshared); // 创建自旋锁

int pthread_spin_lock(pthread_spinlock_t *lock); // 加锁,阻塞
int pthread_spin_trylock(pthread_spinlock_t *lock); // 尝试加锁,非阻塞
int pthread_spin_unlock(pthread_spinlock_t *lock); // 解锁

5、 条件变量 + 互斥锁 实现同步

Linux 条件变量:实现线程同步(什么是条件变量、为什么需要条件变量,怎么使用条件变量(接口)、例子,代码演示(生产者消费者模型))
条件变量:标志事件是否发生
使用方法
在这里插入图片描述

(1)信号量实现同步(互斥和同步都能实现)

使用系统的信号量实现同步。
linux线程同步方式3——信号量(semaphore)Posix
System V 信号量

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
//sem_init(sem_t *__sem, int __pshared, unsigned int __value);

sem_t sem;
int loop=0;

void * thread1(void * arg)
{
    pthread_setcancelstate(PTHREAD_CANCEL_ENABLE,NULL);
    pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL);
    while(1)
    {
        loop++;
        printf("thread1\n");
        sem_post(&sem);
        sleep(1);
    }

}
void * thread2(void * arg)
{
    pthread_t * th1 = (pthread_t *)arg;
    while(1)
    {
        sem_wait(&sem);
        printf("thread2 loop = %d \n",loop);
        if(loop == 10)
        {
            pthread_cancel(*th1);
            break;
        }
        sleep(1);
    }
    pthread_exit(NULL);
}


int main()
{
    int err;
    pthread_t th[2];
    err = sem_init(&sem,0,0);
    if(0 != err)
    {
        printf("err sem_init %s \n",strerror(err));
        exit(-1);
    }
    err = pthread_create(&th[0],NULL, thread1,NULL);
    if(0 != err)
    {
        printf("err pthread_create %s \n",strerror(err));
        exit(-1);
    }
    err = pthread_create(&th[1],NULL, thread2,(void *)&th[0]);
    if(0 != err)
    {
        printf("err pthread_create %s \n",strerror(err));
        exit(-1);
    }
    pthread_join(th[0],NULL);
    printf("recalling thread1\n");
    pthread_join(th[1],NULL);
    printf("recalling thread2\n");
    sem_destroy(&sem);
}

命名信号量:可用于进程间同步,是一个文件

sem_t *sem_open(const char *name, int oflag);
sem_t *sem_open(const char *name, int oflag,
                mode_t mode, unsigned int value);
int sem_unlink(const char *name);

未命名信号量:线程间同步,不是一个文件

int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_destroy(sem_t *sem);

7、 读写锁

linux线程同步方式5——读写锁(rwlock)

六、 线程的信号处理

Linux下多线程中的信号处理
总结:线程的信号处理动作在同一个线程组的线程都是相同的,但是不同线程有自己独立的信号掩码,一个信号只会被一个线程处理,优先在主线程处理,主线程不处理则寻找一个可以处理的线程,否则忽略。
1)如果是异常产生的信号(比如程序错误,像SIGPIPE、SIGEGV这些),则只有产生异常的线程收到并处理。

2)如果是用pthread_kill产生的内部信号,则只有pthread_kill参数中指定的目标线程收到并处理。

3)如果是外部使用kill命令产生的信号,通常是SIGINT、SIGHUP等job control信号,则会遍历所有线程,直到找到一个不阻塞该信号的线程,然后调用它来处理。(一般从主线程找起),注意只有一个线程能收到。

4)每个线程都有自己独立的signal mask,但所有线程共享进程的signal action。这意味着,你可以在线程中调用pthread_sigmask(不是sigmask)来决定本线程阻塞哪些信号。但你不能调用sigaction来指定单个线程的信号处理方式。如果在某个线程中调用了sigaction处理某个信号,那么这个进程中的未阻塞这个信号的所有线程在收到这个信号都会按同一种方式处理这个信号。另外,注意子线程的mask是会从主线程继承而来的。

1、相关函数

(1) pthread_sigmask

(2) pthread_kill

(3) int pthread_sigqueue

问题记录

1、(补充)进程组与会话组

Linux之进程的基本概念(进程,进程组,会话关系)

2、(补充)用户线程如何映射到内核进程

各个教科书都解释过,用户线程:内核线程:内核进程有n:0:1和n:n:1和m:n:r(m>n>r>1)的关系。
用户线程必须与内核线程相关联的原因是:
**用户线程本身只是一堆数据用户程序。内核线程是系统中的真正线程,因此对于用户线程来说,用户程序必须让它的调度器采用用户线程,然后在内核线程上运行它。用户线程和内核线程之间的映射不必是一对一(1:1)映射;你可以有多个用户线程共享相同的内核线程(每次只运行其中一个用户线程),并且你可以有一个单独的用户线程在不同的内核线程(1:n)映射之间循环。
结论:
如果线程管理调度工作在用户空间完成,则内核线程比用户线程更少甚至没有(只有一个内核进程)。只有一个进程是极端情况,m:n:r是中间情况。
如果在内核空间调度管理,则必须一一映射到内核空间。
内核线程和用户线程
** 补充 2025.01.11

linux内核中创建进程和线程做了什么工作?
用户态线程和内核态线程是怎么进行绑定的,具体代码是怎么实现的?

3、具体使用pthread_cond_wait的问题

pthread_cond_wait 的调用应该在pthread_cond_signal 之前,否则若是pthread_cond_signal只是调用一次,pthread_cond_wait 将会接收不到信号导致卡死。
示例:

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int a,b;
pthread_cond_t * cond = NULL;
pthread_mutex_t * mu = NULL;
void  clean(void * arg)
{
    printf("\033[33mclean function:unlock mu\n\033[0m");
    pthread_mutex_t * mutex = (pthread_mutex_t *)arg;
    pthread_mutex_unlock(mutex);
}






void *  thread1(void * arg)
{
    pthread_mutex_t * mutex = (pthread_mutex_t *)arg;
    while(1)
    {
        pthread_mutex_lock(mutex);
        if(a != b)
        {
            a++;
            b--;
            printf("thread1 a++ = %d , b-- = %d \n",a,b);
        }
        else
        {

            break;
        }
        pthread_mutex_unlock(mutex);
    }
    pthread_cleanup_push(clean, arg);
    pthread_exit(NULL);
    pthread_cleanup_pop(0);
}




void * thread2(void * arg)
{
    pthread_mutex_t * mutex =(pthread_mutex_t *)arg;
    while(1)
    {
        pthread_mutex_lock(mutex);
        if(a == b)
        {
            printf("thread2: a==b relase cond\n");
            if(0 != pthread_cond_signal(cond))
            {
                printf("err pthread_cond_signal \n");
            }
            break;
        } 
        pthread_mutex_unlock(mutex);
    }
    pthread_cleanup_push(clean, arg);
    pthread_exit(NULL);
    pthread_cleanup_pop(0);
}



void * thread3(void * arg)
{
    pthread_mutex_t * mutex = (pthread_mutex_t *)arg;
    pthread_mutex_lock(mutex);
    pthread_cond_wait(cond,mutex);
    printf("\033[32mthread3: a==b \n\033[0m");
    pthread_mutex_unlock(mutex);
    pthread_cleanup_push(clean, arg);
    pthread_exit(NULL);
    pthread_cleanup_pop(0);
}

int main()
{
    //init
    int err;
    pthread_mutex_t lock;
    mu = &lock;
    pthread_cond_t condd;
    cond = &condd;
    pthread_mutexattr_t mu_attr;
    pthread_mutexattr_settype(&mu_attr, PTHREAD_MUTEX_ERRORCHECK);
    a=3;
    b=9;
    if(0 != pthread_cond_init(cond,NULL))
    {
        printf("main:err init cond\n");
        exit(-1);
    }
    if(0 != pthread_mutex_init(mu,&mu_attr))
    {
        printf("main:err init mutex\n");
        exit(-1); 
    }
    pthread_t th[3];
    err = pthread_create(&th[0],NULL, thread1,(void *)mu);
    if(0 != err)
    {
        printf("main:err create  thread1\n");
        exit(-1); 
    }
    err = pthread_create(&th[1],NULL, thread2,(void *)mu);
    if(0 != err)
    {
        printf("main:err create  thread2\n");
        exit(-1); 
    }
    err = pthread_create(&th[2],NULL, thread3,(void *)mu);
    if(0 != err)
    {
        printf("main:err create  thread3\n");
        exit(-1); 
    }
    pthread_join(th[0],NULL);
    printf("recycling thread1\n");
    pthread_join(th[1],NULL);
    printf("recycling thread2\n");
    pthread_join(th[2],NULL);
    printf("recycling thread3\n");
    pthread_mutex_destroy(mu);
    pthread_cond_destroy(cond);
}

在上面的例子中,线程1在 a != b 时 a++,b–。线程2在 a== b 时pthread_cond_signal 条件变量cond,线程3 等待条件变量。
由于线程的执行是无序的,若是线程2的pthread_cond_signal 操作在线程3的pthread_cond_wait之前,则pthread_cond_wait 将等不到条件pthread_cond_signal ,导致死等。

wangju@wangju-virtual-machine:~/learn/thread/lock/work1$ ./a.out 
thread1 a++ = 4 , b-- = 8 
thread1 a++ = 5 , b-- = 7 
thread1 a++ = 6 , b-- = 6 
clean function:unlock mu
recycling thread1
thread2: a==b relase cond
clean function:unlock mu
thread3: a==b 
clean function:unlock mu
recycling thread2
recycling thread3
wangju@wangju-virtual-machine:~/learn/thread/lock/work1$ ./a.out 
thread1 a++ = 4 , b-- = 8 
thread1 a++ = 5 , b-- = 7 
thread1 a++ = 6 , b-- = 6 
clean function:unlock mu
thread2: a==b relase cond
clean function:unlock mu
recycling thread1
recycling thread2


可以看到,这个示例程序有时候可以顺利执行,有时候不行,因为线程2 的调用在线程3之前,线程2的pthread_cond_signal 操作在线程3的pthread_cond_wait之前。为了保证线程3在线程2之前,可以在线程2刚进入时sleep一段时间,或者先调用线程三,之后再main中sleep一段时间。
注意:
1、pthread_cond_wait先解当前线程持有的锁,所以使用前先获取锁,以确保可以解锁。
2、pthread_cond_wait在等待到信号解除阻塞后,会再次获取锁,并检查条件是否成立,条件成立才会返回(不解锁),不成立则解锁,再次等待。所以成功后要解锁。

4、信号量超时等待 sem_timedwait

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <semaphore.h>
#include <time.h>
#include <unistd.h>
//sem_init(sem_t *__sem, int __pshared, unsigned int __value);

#define SEM_NUM 5
#define PHILOSOPHER_NUM 5
sem_t sem[SEM_NUM];
int num[5]={1,2,3,4,5};


void * eat(void * arg)
{
    int philosopher = *((int *)arg);
    struct timespec tmp_tim;
    while(1)
    {   //get left chopsticks
        sem_wait(&sem[(philosopher)%SEM_NUM]);
        //get right chopsticks
        clock_gettime(CLOCK_REALTIME,&tmp_tim);
        tmp_tim.tv_sec += 1;
        if(0 != sem_timedwait(&sem[philosopher-1],&tmp_tim))
        {
            //not get right chopsticks
            sem_post(&sem[(philosopher)%SEM_NUM]);
            continue;
        }
        else
        {
            printf("philosopher %d eating\n",philosopher);
            sem_post(&sem[(philosopher)%SEM_NUM]);
            sem_post(&sem[philosopher-1]);
            sleep(1);
 
        }
    }
}


int main()
{
    int err,loop;
    pthread_t th[5];
    for(loop=0;loop<SEM_NUM;loop++)
    {
         
         err = sem_init(&sem[loop],0,1);
         if(0 != err)
         {
            printf("err sem_init %s \n",strerror(err));
            exit(-1);
         }
         printf("init sem %d \n",loop);

    }
    for(loop=1;loop<=PHILOSOPHER_NUM;loop++)
    {
        err = pthread_create(&th[loop-1],NULL,eat,(void *)&num[loop-1]);
        if(0 != err)
        {
            printf("err pthread_create %s \n",strerror(err));
            exit(-1);
        }
        printf("init thread %d \n",loop);
    }
    
    for(loop=0;loop<PHILOSOPHER_NUM;loop++)
    {
        pthread_join(th[loop],NULL);
        printf("recalling thread%d \n",loop+1);
        sem_destroy(&sem[loop]);
    }
}

5、循环中变量以指针作为参数的传递,引发的问题

将一个不固定值的量,以指针形式传入函数,那么函数访问该指针时得到的可能不是想要的值,应为在其他地方可能会改变值,值传递直接访问内存,没有副本,访问的值会被改变,这可能也是指针参数加const的原因

6、线程退出导致主线程退出

在 C 语言中,当一个线程异常退出时,可能会导致整个进程(包括主线程)退出。这主要是因为线程的异常行为可能会引发整个进程的异常终止机制。当一个线程收到某些信号(如SIGSEGV - 段错误信号)并且没有进行信号处理时,默认的信号处理行为可能会导致整个进程终止。例如,如果一个线程试图访问非法内存地址,会触发SIGSEGV信号。
想要在子线程异常退出时不影响主线程,可以屏蔽某些信号或者自定义信号处理函数。

7、是否有对应的内核线程对应用户进程

理解:进程是一个单线程,用户线程和内核线程 1V1,所以进程标识线程可使用的资源,线程(单线程或多线程)作为被调度的单位。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值