Linux系统编程:线程总结

线程的概念

基本概念

所谓线程,通俗的说就是一个正在运行的函数。

在Linux系统中,线程是程序运行的最小单位,也被视为进程内部的控制序列。同一进程下的多个线程共享进程的所有资源,包括进程环境变量、打开的文件描述符、信号量、虚拟地址空间、代码、数据等。线程也有自己的程序计数器、栈空间和寄存器等。

更具体的,线程是进程中的一个执行流程,一个进程至少包含一个执行线程。在Linux系统中,通过进程虚拟地址空间可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流。

进程是资源分配的最小单位,而线程是程序运行的最小单位。多进程由于进程间是互不干扰的,每个进程都有自己的用户虚拟空间,可靠性高,但数据共享方面就会比较复杂,需要用到 IPC通信机制。而多线程则相对容易,因为同一进程下的多个线程通信容易,它们处于同一个虚拟空间下。

线程有很多标准,现在最常用的就算POSIX线程标准,注意这个是标准而不是实现。

还可以参考的标准比如openmp线程标准。

线程标识(也就是线程ID):pthread_t ;

线程相关的常用函数:

pthread_equal():

注意这个函数使用时编译链接要添加 -pthread 选项。

比较两个线程的 ID 是否相等。

pthread_self():

获取当前的线程标识。

线程的创建:pthread_create()

pthread_create():

pthread_create是类Unix操作系统(Unix、Linux、Mac OS X等)的创建线程的函数。它的功能是创建线程(实际上就是确定调用该线程函数的入口点),在线程创建以后,就开始运行相关的线程函数。

这个函数需要我们传入一些参数,包括:

指向线程标识符的指针:这个标识符是用来唯一标识该线程的。
用来设置线程属性的参数:这个参数可以用来设置新线程的一些属性,比如线程的堆栈大小、线程的优先级等。
线程运行函数的起始地址:这个参数是要在线程中运行的函数的地址。
运行函数的参数:这些参数会被传递给在线程中运行的函数。

当pthread_create函数成功创建一个新线程时,它会返回0;如果创建线程失败,它会返回一个错误码。

新创建的线程会从我们传给pthread_create函数的函数地址开始运行。这个函数被称为线程函数,是新线程的入口点。

来写个例子感受一下:

这里我们没有打印出创建出来的线程内容,这是因为线程的调度取决于系统的调度器策略。

这里子线程还没有来得及被调度,主线程就已经结束了(主线程除了作为线程的身份还有一个身份就是是线程所在的进程,进程都没了那子线程肯定就没了呀),所以看不到子线程打印的内容。

所以我们不能凭空猜测父子线程(或者进程)谁先开始运行嗷。

线程的终止

线程的终止有三种方式:

1、 线程从启动例程中返回,返回值为线程的退出码

2、 线程可以被同一进程中的其它线程取消

3、 线程调用pthread_exit()函数

pthread_exit():

在Linux的线程库中,pthread_exit函数用于终止调用它的线程,并返回一个指向某个值的指针。这个返回值可以被其他线程通过pthread_join函数获取。

具体来说,pthread_exit函数的作用是结束当前线程的执行,并返回一个指向某个值的指针。这个返回值可以被其他线程通过pthread_join函数获取。当线程调用pthread_exit函数时,该线程将立即停止执行,但其线程ID和一些资源(如打开的文件描述符)将保留,直到其他线程通过pthread_join函数将其终止。

需要注意的是,pthread_exit函数与普通的return语句有所不同。普通的return语句会返回一个值,但不会终止当前线程的执行。而pthread_exit函数会立即终止当前线程的执行,并返回一个指向某个值的指针。

此外,如果主线程没有正确地调用pthread_join函数来等待其他线程结束,那么这些线程将不会自动被终止。即使主线程已经执行完毕,这些未被终止的线程仍会继续执行,直到它们自然结束或被其他线程通过pthread_join函数终止。因此,在使用pthread_exit函数时,需要注意正确地调用pthread_join函数来等待其他线程结束,以避免资源泄漏和数据不一致等问题。

所以这里就必然要提到另一个函数pthread_join():

在Linux的线程库中,pthread_join函数用于等待一个线程的结束。它的作用是阻塞当前线程,直到指定的线程终止,然后返回该线程的退出状态。

具体来说,pthread_join函数需要传入两个参数:要等待的线程的标识符和一个指向pthread_exit函数的返回值的指针。当调用pthread_join函数时,当前线程将被阻塞,直到指定的线程终止。当指定的线程终止时,pthread_join函数将返回该线程的退出状态,这个返回值可以被存储在指向pthread_exit函数的返回值的指针中。

需要注意的是,pthread_join函数并不会立即终止指定的线程,而是等待它自然结束或被其他线程通过pthread_exit函数终止。因此,在使用pthread_join函数时,需要注意正确地管理线程的生命周期,确保在需要的时候调用pthread_exit函数来结束线程。

此外,如果多个线程同时调用pthread_join函数来等待同一个线程的结束,那么只有其中一个线程会得到该线程的退出状态,而其他线程将继续阻塞。因此,在使用pthread_join函数时,需要注意避免竞争条件和死锁等问题。

总的来说,pthread_join函数是用来等待一个线程结束的函数,并返回该线程的退出状态。它是多线程编程中常用的函数之一,可以帮助我们实现线程间的同步和资源管理。

一个小例子:

可以看见和我们预料到的一样,程序先执行主线程,打印Begin,然后开启了一个子线程,此时主线程因为调用了pthread_join被阻塞,在等待子线程的结束,然后子线程打印working正常退出后,主线程给子线程收完尸打印了end结束程序。

栈的清理:pthread_cleanup_push() 和 pthread_cleanup_pop()

关于栈的清理有两个函数:

pthread_cleanup_push() 和 pthread_cleanup_pop():

在Linux的线程库中,pthread_cleanup_push和pthread_cleanup_pop函数用于管理线程的清理操作。它们提供了一种机制,用于在线程结束时执行一些清理操作,以确保资源被正确释放和避免内存泄漏等问题。

具体来说,pthread_cleanup_push函数用于将一个清理函数压入当前线程的清理堆栈中。这个清理函数将在线程结束时被自动调用。当线程结束时,系统会检查清理堆栈,并依次调用其中的清理函数。这些清理函数可以执行一些必要的资源释放操作,如关闭文件描述符、释放内存等。

pthread_cleanup_push函数的原型如下:

c 复制代码
void pthread_cleanup_push(void (*func)(void *arg), void *arg);

其中,func是指向清理函数的指针,arg是传递给清理函数的参数。

与pthread_cleanup_push函数相反,pthread_cleanup_pop函数用于从当前线程的清理堆栈中弹出最近的清理函数,并调用它。这个函数通常在另一个函数中调用,用于执行一些特定的清理操作。

pthread_cleanup_pop函数的原型如下:

c 复制代码
void pthread_cleanup_pop(int execute);

其中,execute是一个标志位,用于指示是否要立即执行清理函数。如果execute为0,则清理函数将被保留在堆栈中,直到线程结束时才被执行;如果execute为1,则清理函数将被立即执行。

需要注意的是,当线程结束时,系统会自动调用所有压入清理堆栈的清理函数。因此,在使用pthread_cleanup_push和pthread_cleanup_pop函数时,需要注意正确地管理线程的生命周期,确保在需要的时候调用pthread_cleanup_pop函数来执行清理操作,避免出现资源泄漏和数据不一致等问题。

来写个小例子感受一下:


执行顺序是321,这符合堆栈特性。

但实际上pthread_cleanuo_pop放在函数中的哪个位置都行,只要符合基本的语法规范,能够和pthread_cleanup_push的左大括号相匹配上即可,如上图中的程序还可以这么写:

从上图可以看到pop函数出现在pthread_exit()之后理论上这三个函数都不会再被执行了,但要注意pthread_cleanup_push和pop并非真正的函数,它们是宏定义,只会在预处理过程中被文本替换掉,所以上图的程序不会报错,三个pop函数也会被执行,只不过其参数值就看不到了,会执行默认的 1 操作,也就是执行清理堆栈的函数:

可以看见运行结果一模一样。

线程的取消选项

pthread_cancel()

涉及到的函数为pthread_cancel():

在Linux的线程库中,pthread_cancel函数用于取消指定线程的执行。它的作用是终止正在执行的线程,并释放其占用的资源。

具体来说,pthread_cancel函数需要传入一个线程标识符作为参数,以指定要取消的线程。当调用pthread_cancel函数时,当前线程将尝试取消指定线程的执行。如果成功,该线程将被终止,并且其占用的资源将被释放。然而,需要注意的是,由于线程的执行是异步的,因此有时pthread_cancel函数可能会失败,而线程仍然继续执行。

需要注意的是,pthread_cancel函数的使用需要谨慎。如果取消一个正在执行关键任务的线程,可能会导致数据不一致或其他不可预知的问题。因此,在使用pthread_cancel函数时,需要仔细考虑线程的生命周期和任务性质,确保在合适的时机使用该函数来避免潜在的风险。

此外,需要注意的是,被取消的线程的退出状态将被设置为PTHREAD_CANCELED,可以通过pthread_join函数来获取这个状态。如果其他线程尝试对已经被取消的线程进行操作(如读取其状态或等待其结束),则会导致未定义的行为。

总的来说,pthread_cancel函数是用来取消指定线程的执行的函数,可以用于实现多线程编程中的线程间控制和资源管理。但需要注意正确使用该函数以避免潜在的风险和问题。

取消有两种状态:允许和不允许。

允许取消又分为:异步cancel 和 推迟cancel(推迟cancel是默认的,即推迟到cancel点才cancel)。

cancel点的概念:POSIX定义的cancel点,都是可能引发阻塞的系统调用。

在cancel这块可以有一些函数来使用,pthread_setcanceltype和pthread_setcancelstate:

pthread_setcanceltype()

在Linux的线程库中,pthread_setcanceltype函数用于设置线程的取消类型。它的作用是允许线程在接收到取消信号时以特定的方式进行取消操作。

具体来说,pthread_setcanceltype函数需要传入两个参数:取消类型和旧取消类型。取消类型指定了线程在接收到取消信号时的行为方式,而旧取消类型则返回之前的取消类型。

以下是常见的取消类型:

PTHREAD_CANCEL_ENABLE:使能取消。当线程设置为这种类型时,它可以接收到取消信号并执行相应的取消操作。

PTHREAD_CANCEL_DISABLE:禁止取消。当线程设置为这种类型时,它将忽略取消信号并继续执行。

需要注意的是,取消类型是线程级别的属性,每个线程都有独立的取消类型设置。因此,在使用pthread_setcanceltype函数时,需要确保对每个线程进行正确的设置,以避免出现意外的问题。

此外,需要注意的是,取消信号是异步的,即线程在执行期间可能会在任何时候接收到取消信号。如果线程被取消,其退出状态将被设置为PTHREAD_CANCELED,可以通过pthread_join函数来获取这个状态。

总的来说,pthread_setcanceltype函数是用来设置线程的取消类型的函数,可以用于实现多线程编程中的线程间控制和资源管理。但需要注意正确使用该函数以避免潜在的风险和问题。

pthread_setcancelstate()

在Linux的线程库中,pthread_setcancelstate函数用于设置线程的取消状态。它的作用是允许线程在接收到取消信号时以特定的方式进行取消操作。

具体来说,pthread_setcancelstate函数需要传入两个参数:取消状态和旧取消状态。取消状态指定了线程在接收到取消信号时的行为方式,而旧取消状态则返回之前的取消状态。

以下是常见的取消状态:

PTHREAD_CANCEL_ENABLE:使能取消。当线程设置为这种状态时,它可以接收到取消信号并执行相应的取消操作。

PTHREAD_CANCEL_DISABLE:禁止取消。当线程设置为这种状态时,它将忽略取消信号并继续执行。

需要注意的是,取消状态是线程级别的属性,每个线程都有独立的取消状态设置。因此,在使用pthread_setcancelstate函数时,需要确保对每个线程进行正确的设置,以避免出现意外的问题。

此外,需要注意的是,取消信号是异步的,即线程在执行期间可能会在任何时候接收到取消信号。如果线程被取消,其退出状态将被设置为PTHREAD_CANCELED,可以通过pthread_join函数来获取这个状态。

总的来说,pthread_setcancelstate函数是用来设置线程的取消状态的函数,可以用于实现多线程编程中的线程间控制和资源管理。但需要注意正确使用该函数以避免潜在的风险和问题。

pthread_testcancel()

在Linux的线程库中,pthread_testcancel函数用于测试当前线程是否已被取消。它的作用是在线程执行过程中检查是否已经收到了取消信号,并在收到取消信号时执行相应的取消操作。

具体来说,pthread_testcancel函数会检查当前线程的取消状态和取消类型。如果取消状态被设置为PTHREAD_CANCEL_ENABLE,并且取消类型不是PTHREAD_CANCEL_DISABLE,那么函数将返回一个非零值,表示线程已经被取消。否则,函数将返回零,表示线程尚未被取消。

需要注意的是,pthread_testcancel函数并不会阻塞当前线程,而是在执行期间检查是否已被取消。因此,可以在线程的执行过程中多次调用该函数,以便及时处理取消操作。

此外,需要注意的是,如果线程已经被取消,但其退出状态尚未被获取(通过pthread_join函数),则调用pthread_testcancel函数将导致未定义的行为。因此,在使用pthread_testcancel函数时,需要确保已经正确处理了线程的退出状态。

总的来说,pthread_testcancel函数是用来测试当前线程是否已被取消的函数,可以用于实现多线程编程中的线程间控制和资源管理。但需要注意正确使用该函数以避免潜在的风险和问题。

注意,这个函数本身就是一个cancel点,所以通俗的说这个函数本身什么也不做,就是用来设置一个取消点。

线程分离函数:pthread_detach()

在Linux的线程库中,pthread_detach函数用于将指定的线程转换为分离状态。处于分离状态的线程在结束时会自动释放其占用的资源,而不会等待其他线程对其进行清理操作。

具体来说,pthread_detach函数需要传入一个线程标识符作为参数,以指定要转换为分离状态的线程。当调用pthread_detach函数时,当前线程将尝试将指定线程转换为分离状态。如果成功,该线程在结束时将自动释放其占用的资源,而不会影响其他线程的执行。

需要注意的是,一旦线程被转换为分离状态,它将无法被重新设置为可join状态。因此,在使用pthread_detach函数时需要谨慎考虑,确保不会导致无法挽回的问题。

此外,需要注意的是,被分离的线程的退出状态将被设置为PTHREAD_EXIT_SUCCESS,可以通过pthread_join函数来获取这个状态。但是,被分离的线程的资源(如内存和文件描述符)将立即被释放,而不会等待其他线程对其进行清理操作。因此,在使用pthread_detach函数时需要确保正确处理线程的资源释放和清理操作。

总的来说,pthread_detach函数是用来将指定线程转换为分离状态的函数,可以用于实现多线程编程中的资源管理和线程间控制。但需要注意正确使用该函数以避免潜在的风险和问题。

线程竞争和故障

这里我们写个小例子来看一下竞争与故障的问题:

我们程序的功能是初始状态下,文件/tmp/out中有个1,接下来我们创建一堆线程去读写(覆盖写)这个文件,看看竞争与冲突,我们先往这个文件中写入1:

编写程序:

运行结果:

为什么为2?

这是因为我们的 20 个线程七手八脚的读取这个文件,读取完了之后都被沉睡了1秒(加sleep的原因是因为机器是单核的,无法展示出并发问题,所以进行一个模拟),等睡醒之后往文件里面进行覆盖写时相当于覆盖写了20次2,所以文件中的最后结果为2而不是21,这就是线程竞争问题或者叫线程故障问题。

解决线程竞争问题,就需要使用到进程同步的内容。

线程同步

线程同步主要使用的就是互斥量,也叫互斥锁。

这里涉及到的一个类型是:pthread_mutex_t 互斥锁类型;

pthread_mutex_init() 和 pthread_mutex_destroy()

初始化互斥量和销毁互斥量:pthread_mutex_init和pthread_mutex_destroy。

上图中的pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; 这种初始化方式称为静态初始化,与之相对的上面两种函数声明互斥锁的被称为动态初始化。

在Linux下查看man手册时,发现终端输出No manual entry for pthread_mutex_init,没有此模块。因为线程模块是posix标准的,man手册没有安装。

只需在终端安装即可:

在终端输入以下命令:

c 复制代码
sudo apt-get install manpages-posix manpages-posix-dev

在Linux的线程库中,pthread_mutex_init和pthread_mutex_destroy是用来管理互斥锁(mutex)的两个函数。互斥锁是一种同步机制,用于防止多个线程同时访问共享资源,避免产生竞态条件。

pthread_mutex_init函数用于初始化一个互斥锁。它需要传入一个指向互斥锁结构的指针以及一个标志位,用于指定互斥锁的类型和属性。以下是常见的互斥锁类型:

PTHREAD_MUTEX_NORMAL:非递归互斥锁。这种类型的互斥锁不支持递归锁定,即同一个线程不能重复获取同一个互斥锁。如果尝试重复获取已经锁定的互斥锁,将会导致死锁。

PTHREAD_MUTEX_RECURSIVE:递归互斥锁。这种类型的互斥锁支持同一个线程重复获取同一个互斥锁。在释放互斥锁时,需要与获取锁的次数相同才能完全释放。

PTHREAD_MUTEX_ERRORCHECK:错误检查互斥锁。这种类型的互斥锁在尝试获取已经被锁定的互斥锁时,会进行检查并返回错误。

pthread_mutex_init函数的原型如下:

c 复制代码
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr, int type);

其中,mutex是指向互斥锁结构的指针,attr是指向互斥锁属性的指针(通常可以传入NULL表示使用默认属性),type是互斥锁的类型。

pthread_mutex_destroy函数用于销毁一个已经初始化的互斥锁。它需要传入一个指向互斥锁结构的指针,以释放该互斥锁所占用的资源。以下是pthread_mutex_destroy函数的原型:

c 复制代码
int pthread_mutex_destroy(pthread_mutex_t *mutex);

其中,mutex是指向互斥锁结构的指针。

需要注意的是,在销毁互斥锁之前,确保没有线程正在等待该互斥锁的释放。否则,会导致未定义的行为。通常可以在所有线程都已经释放了该互斥锁之后,再调用pthread_mutex_destroy函数来销毁它。

pthread_mutex_lock()和pthread_mutex_trylock()和pthread_mutex_unlock()

在Linux的线程库中,pthread_mutex_lock、pthread_mutex_trylock和pthread_mutex_unlock是用于管理互斥锁(mutex)的三个函数。它们分别用于锁定互斥锁、尝试锁定互斥锁和解锁互斥锁。

pthread_mutex_lock函数:

c 复制代码
int pthread_mutex_lock(pthread_mutex_t *mutex);

pthread_mutex_lock函数用于锁定指定的互斥锁。如果该互斥锁已经被锁定,则当前线程将阻塞并等待,直到该互斥锁变为未锁定状态。当成功获取到互斥锁时,该函数将返回0;否则,将返回一个错误码。

pthread_mutex_trylock函数:

c 复制代码
int pthread_mutex_trylock(pthread_mutex_t *mutex);

pthread_mutex_trylock函数用于尝试锁定指定的互斥锁,而不会阻塞当前线程。如果该互斥锁已经被锁定,则该函数将立即返回错误码(通常是EBUSY),而不会等待互斥锁变为未锁定状态。如果成功获取到互斥锁,则该函数将返回0;否则,将返回一个错误码。

pthread_mutex_unlock函数:

c 复制代码
int pthread_mutex_unlock(pthread_mutex_t *mutex);

pthread_mutex_unlock函数用于解锁指定的互斥锁。只有成功获取到该互斥锁的线程才能解锁它。当成功释放互斥锁时,该函数将返回0;否则,将返回一个错误码。需要注意的是,在解锁互斥锁之前,确保已经成功获取到该互斥锁,否则可能会导致未定义的行为。

这些函数的使用可以保证在多线程环境中对共享资源的互斥访问,避免竞态条件和数据不一致性的问题。在使用这些函数时,需要注意正确地初始化互斥锁对象,并在使用完之后及时销毁它,以释放资源。

使用互斥锁重写刚刚的线程竞争程序

使用互斥锁的静态初始化:

我们重新将该文件的内容写为1:

再执行程序:

可以看见成功啦!

另一个例子程序,强化对互斥锁的理解

这个程序的作用是有四个线程,这四个线程拼命的往终端上输出abcd,一个线程输出a,一个线程输出b,一个线程输出c还有一个输出d:

运行结果如下图:

但是现在我们想实现的效果是工工整整的按abcd的效果打印出来而不是上图这样的乱七八糟的,改动程序:

编译运行:

可以看见abcd依次输出。

条件变量

条件变量涉及一个类型:pthread_cond_t;

在Linux下,多线程并发通常使用pthread库来实现。条件变量是pthread库中的一种同步机制,用于实现线程间的协调和通信。

条件变量是一种同步原语,它允许一个或多个线程等待某个条件成立,而其他线程可以通知它们条件已经成立或不再成立。条件变量常常用于解决生产者-消费者问题,其中一个生产者线程产生数据,并将其放入缓冲区,而多个消费者线程从缓冲区消费数据。

在Linux下,使用条件变量可以实现以下操作:

1、初始化条件变量:使用pthread_cond_init函数初始化条件变量。

2、等待条件成立:使用pthread_cond_wait函数等待条件成立。线程会进入阻塞状态,直到其他线程调用pthread_cond_signal或pthread_cond_broadcast函数通知条件成立或不再成立。

3、通知条件成立:使用pthread_cond_signal函数通知等待在条件变量上的一个线程条件已经成立。如果多个线程正在等待,则其中一个线程会被唤醒并继续执行。

4、通知所有条件成立:使用pthread_cond_broadcast函数通知所有等待在条件变量上的线程条件已经成立。所有等待的线程都会被唤醒并继续执行。

5、销毁条件变量:使用pthread_cond_destroy函数销毁条件变量。

在使用条件变量时,需要注意以下几点:

1、必须在互斥锁的保护下使用条件变量,以避免竞态条件。

2、等待和通知操作必须作为一对出现,即如果有一个线程正在等待某个条件成立,必须有一个或多个线程来通知它条件已经成立。

3、等待操作会阻塞当前线程,直到被通知操作唤醒。因此,在实现生产者-消费者问题时,需要注意确保生产者线程在消费者线程之前先运行,以避免死锁。

4、条件变量的使用应该结合具体的业务场景进行设计,以避免出现死锁或竞争的情况。

总之,条件变量是Linux下多线程并发中常用的一种同步机制,它允许线程间进行协调和通信,以实现更高效的多线程并发处理。

接下来详细介绍一下每个函数的作用和使用方法:

pthread_cond_init:这个函数用于初始化一个条件变量。它需要传入两个参数:一个是指向pthread_cond_t类型变量的指针,另一个是指向pthread_condattr_t类型变量的指针,用于设置条件变量的属性。如果条件变量已经初始化,则此函数会返回错误。

c 复制代码
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *cond_attr);

pthread_cond_signal:这个函数用于通知等待在条件变量上的一个线程条件已经成立。它需要传入一个pthread_cond_t类型的变量。如果多个线程正在等待,则其中一个线程会被唤醒并继续执行。需要注意的是,这个函数只通知一个线程,如果有多个线程在等待,其他线程仍然需要等待。

c 复制代码
int pthread_cond_signal(pthread_cond_t *cond);

pthread_cond_broadcast:这个函数用于通知所有等待在条件变量上的线程条件已经成立。它需要传入一个pthread_cond_t类型的变量。所有等待的线程都会被唤醒并继续执行。

c 复制代码
int pthread_cond_broadcast(pthread_cond_t *cond);

pthread_cond_destroy:这个函数用于销毁一个条件变量。它需要传入一个pthread_cond_t类型的变量。在销毁条件变量之前,需要确保没有线程正在等待在条件变量上。

c 复制代码
int pthread_cond_destroy(pthread_cond_t *cond);

pthread_cond_wait:该函数是用于实现线程间的条件变量等待。这个函数允许一个线程等待一个条件变量,直到条件变量被满足或被其他线程唤醒。

函数原型如下:

c 复制代码
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

参数说明:

cond:指向要等待的条件变量的指针。

mutex:指向保护条件变量的互斥锁的指针。在调用pthread_cond_wait之前,线程需要获取这个互斥锁。

函数返回值:

如果函数成功执行,返回0;

如果函数执行失败,返回一个错误码。

注意事项:

在调用pthread_cond_wait之前,线程需要先获取互斥锁,以避免竞态条件。

pthread_cond_wait函数会释放互斥锁,并在等待期间进入阻塞状态。当条件变量被满足或被其他线程唤醒时,线程将被唤醒并重新获取互斥锁。

如果线程在等待期间被打断(如收到信号),则函数将返回错误码EINTR。此时,可能需要重新调用pthread_cond_wait函数或其他操作来处理异常情况。

在使用条件变量时,需要注意死锁问题。如果一个线程在等待条件变量时一直无法获取互斥锁或条件变量不被满足,则可能导致死锁。因此,需要合理设计程序以确保线程间的同步和避免死锁情况。

pthread_cond_timewait:该函数用于实现线程间的条件变量等待。这个函数允许一个线程等待一个条件变量,并在指定的时间内保持等待状态。如果条件变量在等待时间内被满足,线程将被唤醒并继续执行;否则,线程将保持等待状态,直到时间到达或被其他线程唤醒。

函数原型如下:

c 复制代码
int pthread_cond_timewait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);

参数说明:

cond:指向要等待的条件变量的指针。

mutex:指向保护条件变量的互斥锁的指针。在调用pthread_cond_timewait之前,线程需要获取这个互斥锁。

abstime:指向一个timespec结构体的指针,用于指定等待的绝对时间。该时间是从1970年1月1日(UTC)开始的秒数和纳秒数。如果该参数为NULL,则函数会一直等待,直到条件变量被唤醒。

函数返回值:

如果函数成功执行,返回0;

如果函数执行失败,返回一个错误码。

注意事项:

在调用pthread_cond_timewait之前,线程需要先获取互斥锁,以避免竞态条件。

当函数返回时,互斥锁已经被释放。如果线程需要在条件满足后继续执行,需要重新获取互斥锁。

如果条件在等待时间内被满足,线程将被唤醒并继续执行;否则,线程将保持等待状态,直到时间到达或被其他线程唤醒。

如果线程在等待期间被打断(如收到信号),则函数将返回错误码EINTR。

以上就是关于Linux下多线程并发时条件变量的详细解释和函数介绍。在使用条件变量时,一定要注意保护好条件变量,避免出现竞态条件和死锁等问题。同时,结合具体的业务场景进行设计也是非常重要的。

我们使用条件变量来改写之前的疯狂输出abcd的例子,我们原先是使用了锁链机制来解决的线程竞争问题,这里我们使用条件变量来使用通知法解决该问题:

这里有一点对于thr_func函数中的注释补充:

编译运行:

可以看见完全正常。

信号量

在Linux中,多线程并发中的信号量是一个整型变量,用于控制对共享资源的访问。它是一种同步机制,用于解决并发程序中的同步和互斥问题。

信号量本质上是一个计数器,可以执行两种操作:P操作和V操作。P操作(也称为wait操作)会使信号量的值减一,如果此时信号量的值小于0,则进程或线程将被阻塞。V操作(也称为signal操作)会使信号量的值加一,并唤醒可能因为等待信号量而被阻塞的进程或线程。

信号量用于协调多个进程(包括父子进程)对共享数据对象的读/写。它不以传送数据为目的,主要是用来保护共享资源(信号量、消息队列、socket连接等),保证共享资源在一个时刻只有一个进程独享。

在并发编程中,信号量被广泛用于避免竞争条件和死锁问题。通过使用信号量,程序员可以确保在任何时候只有一个进程或线程访问特定的共享资源,从而避免数据不一致和其他并发问题。

需要注意的是,在使用信号量时,程序员需要谨慎地设计程序以避免死锁和其他并发问题。同时,也需要对信号量的使用进行充分的测试和验证,以确保程序的正确性和可靠性。

读写锁

读写锁(也称为共享-独占锁)是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者。读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。这种锁相对于自旋锁而言,能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的逻辑CPU数。写者是排他性的,一个读写锁同时只能有一个写者或多个读者(与CPU数相关),但不能同时既有读者又有写者。

读写锁适合于对数据结构的读次数比写次数多很多的场合。它与互斥锁相比,能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源。读写锁的状态有三种:读模式下加锁(共享)、写模式下加锁(独占)以及不加锁。一次只有一个线程可以占有写模式下的读写锁;但是多个线程可以同时占有读模式下的读写锁。读写锁在写加锁状态时,其他试图以写模式加锁的线程都会被阻塞。读写锁在读加锁状态时,如果有线程希望以写模式加锁时,必须阻塞,直到所有线程释放锁

简单的理解就是:读锁->共享锁,写锁->互斥锁;

线程属性

注意,这块内容其实了解即可,因为工作当中百分之八十的内容线程属性都是默认为NULL即可。

还记得之前学过的pthread_create那个函数吧,我们在第二个参数设置线程属性时一直填的NULL,哪怕就是实际工作当中其实百分之八十以上的时间都是填NULL,不过这里还是要对其进行一个解释。

对于线程属性我们有一些函数需要学习:

线程属性是指一组描述线程行为的属性,可以通过设置这些属性来控制线程的行为。以下是与线程属性相关的函数:

pthread_attr_init:初始化一个线程属性对象,并返回0表示成功,否则返回错误码。

c 复制代码
#include <pthread.h>  
int pthread_attr_init(pthread_attr_t *attr);

pthread_attr_setstacksize:设置线程的栈大小。

c 复制代码
#include <pthread.h>  
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);

pthread_attr_getstacksize:获取线程的栈大小。

c 复制代码
#include <pthread.h>  
int pthread_attr_getstacksize(pthread_attr_t *attr, size_t *stacksize);

pthread_attr_setdetachstate:设置线程的分离状态。分离状态是指当线程结束时是否保留资源,如果设置为PTHREAD_CREATE_JOINABLE,则线程结束时不会立即释放资源,其他线程可以通过pthread_join来获取线程的退出状态;如果设置为PTHREAD_CREATE_DETACHED,则线程结束时会立即释放资源。

c 复制代码
#include <pthread.h>  
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);

pthread_attr_getdetachstate:获取线程的分离状态。

c 复制代码
#include <pthread.h>  
int pthread_attr_getdetachstate(pthread_attr_t *attr, int *detachstate);

pthread_attr_setschedparam:设置线程的调度参数,包括优先级、CPU亲和性等。

c 复制代码
#include <pthread.h>  
int pthread_attr_setschedparam(pthread_attr_t *attr, const struct sched_param *schedparam);

pthread_attr_getschedparam:获取线程的调度参数。

c 复制代码
#include <pthread.h>  
int pthread_attr_getschedparam(pthread_attr_t *attr, struct sched_param *schedparam);

pthread_attr_setscope:设置线程的竞争范围,PTHREAD_SCOPE_SYSTEM表示系统级竞争范围,PTHREAD_SCOPE_PROCESS表示进程级竞争范围。

c 复制代码
#include <pthread.h>  
int pthread_attr_setscope(pthread_attr_t *attr, int scope);

写一个小例子来展示系统最多能创建有多少线程:

运行结果:

在这个程序的基础上,然后我们使用线程属性来进行操作:

运行结果一样,可能是因为本身栈大小就是1M吧...俺目前也不知道为什么。

线程同步的属性

互斥量属性

在多线程编程中,有时我们希望对互斥量的行为进行一些定制。例如,我们可能希望设置互斥量的类型(例如,进程间互斥或适应型互斥),或者设置一些与互斥量相关的属性(例如,是否可以嵌套)。这就是pthread_mutexattr_init函数的作用。

pthread_mutexattr_init函数允许我们创建一个互斥量属性对象,然后我们可以使用这个对象来设置互斥量的各种属性。

以下是pthread_mutexattr_init函数的原型:

c 复制代码
#include <pthread.h>  
  
int pthread_mutexattr_init(pthread_mutexattr_t *attr);

这个函数接收一个指向pthread_mutexattr_t类型变量的指针,该变量用于存储互斥量属性。函数返回0表示成功,返回错误代码表示失败。

一旦我们使用pthread_mutexattr_init初始化了互斥量属性对象,我们可以使用其他函数(如pthread_mutexattr_settype或pthread_mutexattr_setnested)来设置特定的属性。当我们不再需要互斥量属性对象时,我们应该使用pthread_mutexattr_destroy函数来清理它。

需要注意的是,虽然使用互斥量属性可以提供更多的灵活性,但它们也会使代码变得更复杂,因此只在必要时使用。例如,对于大多数应用程序,使用默认的互斥量类型和属性就足够了

以下是这些函数的详细描述和函数原型:

pthread_mutexattr_init:此函数用于初始化一个互斥量属性对象,以便后续使用。函数原型为:

c 复制代码
int pthread_mutexattr_init(pthread_mutexattr_t *attr);

参数:attr是一个指向pthread_mutexattr_t类型变量的指针,用于存储互斥量属性。

pthread_mutexattr_settype:此函数用于设置互斥量的类型。类型可以是PTHREAD_MUTEX_NORMAL、PTHREAD_MUTEX_RECURSIVE、PTHREAD_MUTEX_ERRORCHECK等。函数原型为:

c 复制代码
int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);

参数:attr是一个指向pthread_mutexattr_t类型变量的指针,存储了互斥量属性。type是要设置的类型。

pthread_mutexattr_setnested:此函数用于设置互斥量的嵌套属性。如果设置为PTHREAD_MUTEX_NESTED,则允许多个线程嵌套地获取同一个互斥量。函数原型为:

c 复制代码
int pthread_mutexattr_setnested(pthread_mutexattr_t *attr, int nested);

参数:attr是一个指向pthread_mutexattr_t类型变量的指针,存储了互斥量属性。nested是要设置的嵌套属性。

pthread_mutexattr_destroy:此函数用于销毁一个互斥量属性对象。函数原型为:

c 复制代码
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);

参数:attr是一个指向pthread_mutexattr_t类型变量的指针,存储了互斥量属性。

pthread_mutexattr_getpshared函数用于获取互斥量的共享属性。它的原型如下:

c 复制代码
int pthread_mutexattr_getpshared(const pthread_mutexattr_t *attr, int *pshared);

参数说明:

attr:指向互斥量属性对象的指针。

pshared:指向共享属性变量的指针,用于存储获取到的共享属性值。

共享属性表示互斥量是否可以由多个进程共享。如果共享属性为PTHREAD_MUTEX_PROCESS_SHARED,则表示互斥量可以被多个进程共享;如果共享属性为PTHREAD_MUTEX_PROCESS_PRIVATE,则表示互斥量只能被同一个进程内的线程共享。

pthread_mutexattr_setpshared函数用于设置互斥量的共享属性。它的原型如下:

c 复制代码
int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared);

参数说明:

attr:指向互斥量属性对象的指针。

pshared:要设置的共享属性值,可以是PTHREAD_MUTEX_PROCESS_SHARED或PTHREAD_MUTEX_PROCESS_PRIVATE。

设置共享属性的目的是确定互斥量是进程间共享还是仅在同一个进程内的线程之间共享。根据具体的应用场景和需求,可以选择适当的共享属性。

这些函数的使用可以让我们在创建和管理互斥量时有更多的灵活性和控制力,以满足特定的多线程编程需求。请注意,这些函数的返回值通常为0表示成功,否则表示错误。

为什么需要这两个函数,是因为有时候我们会需要进行跨进程的操作,比如使用了clone系统调用时。

补充:clone系统调用

在Linux中,clone系统调用是一种用于创建新进程的机制,它允许在创建新进程时指定共享部分和独立部分。

clone系统调用接受三个参数:新进程的返回值,一个__clone_args结构体指针,以及一个进程创建时的标志位。

__clone_args结构体包含了创建新进程时需要用到的各种参数,包括:

new_stack:指定新进程的栈地址。如果设置为NULL,则使用当前线程的栈地址。

ptid:指向一个pthread_t结构的指针,用于存储新进程的线程ID。

tls:指向新进程的线程本地存储(TLS)的指针。如果设置为NULL,则使用当前线程的TLS。

exit_thread:一个函数指针,指向新进程退出时的处理函数。如果设置为NULL,则使用默认的处理函数。

arg:一个指向传递给新进程的参数的指针。

进程创建时的标志位用于指定新进程的类型和行为。常用的标志位包括:

CLONE_VM:共享虚拟内存空间。

CLONE_FS:共享文件系统描述符。

CLONE_FILES:共享文件描述符。

CLONE_SIGHAND:共享信号处理函数。

CLONE_PID:进程ID相同。

通过组合这些标志位,可以使用clone系统调用创建出具有不同特性的新进程。

下面是一个使用clone系统调用的示例代码:

c 复制代码
#include <stdio.h>  
#include <stdlib.h>  
#include <pthread.h>  
#include <unistd.h>  
  
void *child_func(void *arg) {  
    printf("This is the child process.\n");  
    exit(0);  
}  
  
int main() {  
    pthread_t tid;  
    __clone_args args = {NULL, &tid, NULL, NULL, child_func, NULL};  
    int clone_flags = CLONE_VM | CLONE_FS | CLONE_FILES;  
    pid_t pid = clone(&args, NULL, clone_flags);  
    if (pid == -1) {  
        perror("clone");  
        exit(EXIT_FAILURE);  
    } else if (pid == 0) {  // child process  
        printf("This is the child process.\n");  
        exit(0);  
    } else {  // parent process  
        printf("This is the parent process.\n");  
        waitpid(pid, NULL, 0);  
        exit(EXIT_SUCCESS);  
    }  
}

在上面的示例中,我们使用clone系统调用创建了一个新进程,并指定了共享虚拟内存空间、文件系统描述符和文件描述符的标志位。新进程的返回值存储在tid变量中,并通过调用pthread_join等待新进程的结束。在父进程中,我们使用waitpid等待子进程的结束,并使用返回值退出父进程。在子进程中,我们执行自定义的函数child_func并退出子进程。

条件变量属性

和互斥量属性的内容大差不差,不再赘述。

基本上用默认的属性就能够完成百分之八十以上的任务了。

可重入概念

多线程中的IO

这里提到的可重入,主要是指多线程中的IO机制。

我们的多线程中的 IO 函数都是默认已经实现了多线程的安全并发,所以我们不会比如说下面有1,2,3个线程都在调用puts函数来打印东西:

其打印结果最多就算aaaaabbbbbccccc的排列组合,而不会打印abcccacbb等这种乱序(如果是这种情况那就说明是可重入的),就是因为这里的puts底层实现是有锁机制的,对应的POSIX标准也提供了没有线程安全版本的IO函数:

如果使用的是这种版本,那么就会产生我刚刚说的现象。

线程与信号

在之前说过信号的响应过程中,每个进程有一个mask位图和一个pending位图,但其实在引入了线程机制的系统中,对于一个进程而言只有一个进程级别的pending位图(以进程为单位是没有mask位图的),然后其内部的每个线程都会有一个mask位图和一个线程级别的pending位图,进程级别之间发的信号通过进程级别的pending位图来接收,而线程级别之间发送的信号由线程级别的pending位图来接收。

对于线程响应信号时,其mask位图会先去与进程级别的pending位图进行按位与(从内核态返回用户态时),然后再来与线程级别的pending位图进行按位与,这才得到最终的信号响应情况,这就是线程与信号之间的关系:

除了这一点与之前说过的不同,其它的响应过程和之前说的都是一样的。

对于这一块我们有几个函数可以使用:

pthread_sigmask()

pthread_sigmask是一个在Linux下的Pthreads库中的函数,用于阻塞或取消阻塞一组信号。这是一个多线程编程中常用的函数,尤其是在处理可能会中断线程执行的信号时。

函数的原型如下:

c 复制代码
int pthread_sigmask(int how, const sigset_t *newmask, sigset_t *oldmask);

参数说明:

1、how:这是一个指定如何更改信号屏蔽字的整数。可能的值包括:

SIG_BLOCK:将newmask指定的信号添加到当前的信号屏蔽字中。

SIG_UNBLOCK:从当前的信号屏蔽字中移除newmask指定的信号。

SIG_SETMASK:将当前的信号屏蔽字设置为newmask。

2、newmask:这是一个指向信号集合的指针,该集合包含了我们希望添加或移除的信号。如果此参数为NULL,那么将不会更改信号屏蔽字。

3、oldmask:这是一个指向信号集合的指针,用于存储调用pthread_sigmask之前的信号屏蔽字。如果此参数为NULL,那么将不会返回旧的信号屏蔽字。

返回值:如果成功,函数返回0;如果失败,返回错误码。

使用这个函数可以帮助我们在多线程环境中更好地控制信号的处理。例如,你可能希望在某些关键的代码段中阻止某些信号,以防止这些信号中断你的线程。在代码段执行完毕后,你可以恢复原来的信号屏蔽字,以允许这些信号再次中断你的线程。

需要注意的是,虽然这个函数的名字中包含"pthread",但它实际上操作的是调用线程的整个进程的信号屏蔽字,而不仅仅是线程自己的信号屏蔽字。这意味着,对信号屏蔽字的更改将影响到进程中的所有线程。因此,在使用这个函数时需要谨慎,以避免引入难以调试的并发问题。

sigwait()

在Linux下的多线程编程中,sigwait函数是一个用于处理信号的函数。它的作用是等待指定的信号发生,并在接收到信号后执行相应的操作。

sigwait函数的原型如下:

c 复制代码
int sigwait(const sigset_t *set, int *sig);

参数说明:

set:一个指向信号集的指针,该信号集包含我们希望等待的信号。

sig:一个指向整数的指针,用于存储接收到的信号的值。

sigwait函数会一直等待,直到信号集set中的某个信号发生。当接收到信号时,函数将返回0,并将接收到的信号值存储在sig所指向的整数中。如果函数返回-1,则表示出现错误。

使用sigwait函数可以让我们在多线程中以一种阻塞的方式等待特定的信号发生。这对于需要协同多个线程工作的程序非常有用,例如,当一个线程需要等待另一个线程发送的特定信号时,可以使用sigwait函数来实现这种同步。

需要注意的是,sigwait函数并不改变信号掩码的阻塞与非阻塞状态。它只是等待指定的信号发生,并将其从进程的未决队列中取走。如果要确保sigwait线程接收到信号,其他所有线程(包括主线程和sigwait线程)必须将该信号阻塞,以确保该信号处于未决状态。

综上所述,sigwait函数是Linux下多线程编程中用于等待特定信号发生的一种机制,可以用于实现线程间的同步和协调操作。

pthread_kill()

pthread_kill函数是在Linux下多线程编程中使用的一个函数,用于向指定的线程发送信号。它的原型如下:

c 复制代码
int pthread_kill(pthread_t thread, int sig);

参数说明:

thread:一个指向要发送信号的线程的pthread_t类型的指针。

sig:要发送的信号的值。

pthread_kill函数将信号sig发送给线程thread。如果该线程是可接收信号的,则会执行与该信号关联的操作。如果函数成功,则返回0;否则,返回一个错误码。

使用pthread_kill函数可以在多线程程序中实现线程间的通信和协调。例如,当一个线程需要通知另一个线程执行特定任务时,可以使用pthread_kill函数向该线程发送一个特定的信号,以触发相应的操作。

需要注意的是,要确保发送信号的线程和接收信号的线程之间的同步和协调,以避免出现竞争条件和未定义的行为。此外,还需要根据具体的程序需求和设计来决定何时使用pthread_kill函数以及如何处理接收到的信号。

综上所述,pthread_kill函数是Linux下多线程编程中用于向指定线程发送信号的函数,可以用于实现线程间的通信和协调。在使用时需要注意同步和设计问题,以确保程序的正确性和稳定性。

线程与fork

在Linux下多线程编程中,fork()函数的使用和多线程之间存在一些特定的关系和注意事项。

首先,fork()函数在多线程编程中的使用可能会导致一些问题。当在多线程中调用fork()函数时,子进程会继承父进程的所有资源,包括线程。这意味着在子进程中会有和父进程相同数量的线程,包括调用fork()函数的线程。然而,在子进程中除调用线程外的其它线程全都终止执行并消失。这意味着如果在父进程中有多个线程,而在其中一个线程调用了fork()函数,那么在子进程中除了调用线程外,其他线程将会消失。

这种行为可能会导致死锁和内存泄露的情况。因为当多线程程序在fork()之后进行时,如果其中一个线程在其他线程之前结束,那么它可能会试图访问已经被其他线程占用的资源,从而引起死锁。同时,如果子进程继承了父进程的内存空间,而子进程比父进程先结束,那么这部分内存空间将不会被释放,从而引起内存泄露。

因此,在进行多线程编程时,尽量避免在多线程环境中使用fork()函数。如果必须使用fork()函数,那么应该在调用fork()函数之前避免创建线程,因为这会影响到全局对象的安全初始化。同时,也需要注意在父子进程之间进行同步和协调,以确保它们的正确性和稳定性。

综上所述,fork()函数的使用和多线程之间存在一些特定的关系和注意事项。在进行多线程编程时,应该尽量避免在多线程环境中使用fork()函数,并注意父子进程之间的同步和协调,以确保程序的正确性和稳定性。

相关推荐
爱吃生蚝的于勒1 小时前
C语言内存函数
c语言·开发语言·数据结构·c++·学习·算法
小白学大数据3 小时前
Python爬虫开发中的分析与方案制定
开发语言·c++·爬虫·python
舞动CPU4 小时前
linux c/c++最高效的计时方法
linux·运维·服务器
versatile_zpc6 小时前
C++初阶:类和对象(上)
开发语言·c++
小鱼仙官6 小时前
MFC IDC_STATIC控件嵌入一个DIALOG界面
c++·mfc
神仙别闹6 小时前
基本MFC类框架的俄罗斯方块游戏
c++·游戏·mfc
秦jh_6 小时前
【Linux】多线程(概念,控制)
linux·运维·前端
娅娅梨6 小时前
C++ 错题本--not found for architecture x86_64 问题
开发语言·c++
兵哥工控7 小时前
MFC工控项目实例二十九主对话框调用子对话框设定参数值
c++·mfc
我爱工作&工作love我7 小时前
1435:【例题3】曲线 一本通 代替三分
c++·算法