Linux——多线程

目录

线程概念

线程控制

线程创建

进程 vs 线程

线程异常

线程等待

线程终止

pthread_cancel

进程替换

线程分离

线程互斥

mutex

mutex接口

mutex的理解

互斥锁的实现

可重入和线程安全

死锁

什么是死锁

死锁产生的必要条件

避免死锁

线程同步

概念

条件变量

条件变量函数


线程概念

        在一个程序加载到内存中时,他就变成了一个进程,会有自己的代码和数据还有内核数据结构,也有虚拟地址到物理地址的映射关系。

        如果我想再创建一个进程,这个进程不想给他创建内核数据结构只有PCB,让这个PCB指向同样的地址空间。就是通过某些手段将当前进程的“资源”通过某些方式划分给不同的PCB。

        我们把上图中的每一个task_struct都可以叫做一个线程(thread)。线程是在进程内部执行的,或者说线程在进程的地址空间内运行,是操作系统调度的基本单位

        上面的这每一个线程都是一个执行流,线程会更轻量化,执行的粒度更细调度轻量化资源占用更少调度成本较低,只要能满足这些它都是线程,不管是在哪个操作系统下。

        进程和线程都要被调度、被创建、维护各种关系等,这两种在概念上都高度重合,那么操作系统也要维护和管理这些线程,就这些操作下来,只是多了一份和进程高度重合的代码来维护线程,所以Linux中没有在内核上去区分进程和线程都用task_struct表示,只不过进程有独立地址空间,线程和进程共享地址空间就行了,用最小的代码实现线程的效果。

        在用户视角看来,进程就是内核数据结构加上进程对应的代码和数据;在内核的视角来看,进程是承担分配系统资源的基本实体

        申请task_struct,申请地址空间,创建页表,开辟内存保存代码和数据,这些都是进程向操作系统申请的,之后的线程就不会再向操作系统申请,而是向进程申请。

        原来我们说的进程是只有一个执行流的进程,后面就可能会遇到内部有多个执行流的进程,原来的task_struct就是进程内部的一个执行流

        在CPU的视角,它不关心是进程还是线程,它只知道task_struct。在Linux下的PCB的量级会小于等于其他操作系统的PCB,如果这个PCB是多线程的一个执行流,那就要比其他OC的PCB量级小,如果只有一个线程,那就是等于。

        所以Linux下的进程统称为轻量级进程

        Linux下没有真正意义上的线程结构,因为没有对应的数据结构,它的进程是用PCB模拟实现的。所以Linux不能直接给我们提供线程的相关接口,只能提供轻量级进程的接口。但是用户有不知道什么是轻量级进程,我只想用进程的时候调用进程的接口,用线程的时候调用线程的接口,所以Linux在用户层实现了一套用户多线程方案,以库的方式提供给用户进行使用,这就是pthread——线程库,也叫他原生线程库


线程控制

线程创建

作用:创建一个新的线程

参数:

  • thread:线程id,是一个输出型参数
  • attr:设置创建线程的属性,默认为nullptr就可以了
  • start_routine:函数指针,线程执行进程代码一部分的入口函数
  • arg:传入入口函数的参数

返回值:创建成功返回0,失败错误码被设置

还有要注意的是,在使用gcc或g++编译的时候要加 -lpthread

#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <cstdio>
#include <string>using namespace std;void* threadRun(void* args)
{const string name = (char*)args;while (true){cout << "新线程: "<< name << ", pid: " << getpid() << endl;sleep(1);}
}int main()
{pthread_t tid[5]; // 直接创建5个线程char name[64];for (int i = 0; i < 5; i++){snprintf(name, sizeof(name), "thread-%d", i);pthread_create(tid + i, nullptr, threadRun, (void*)name); // 创建线程,把name传入函数sleep(1);}while (true) {cout << "主线程, pid: " << getpid() << endl;sleep(3);}return 0;
}

        使用ps -aL指令就可以查看系统中的轻量级进程,他们都属于同一个进程,LWP(Light Weight Process)就是轻量级进程,这些编号都是不一样的,有一个PID和LWP是一样的,那就是主线程,所以操作系统识别是LWP,PID不能说明唯一性,原来只有一个轻量级进程的时候LWP和PID是一样的,也没有问题。

进程 vs 线程

        进程中的多个线程是共享同一地址空间的,比如正文代码区、全局数据区和堆区都是共享的,文件描述符表、信号的处理方式、当前工作目录、用户id和所属组id也是共享的。

int g_val = 0;void* threadRoutine(void* args)
{while (true){cout << (char*)args << " : " << g_val << " &: " << &g_val << endl;sleep(1);g_val++;}
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, (void*)"new thread");while (true){cout << "main thread: " << g_val << " &: " << &g_val << endl;sleep(1);}return 0;
}

        如果使用__thread来修饰全局变量,可以让每一个线程各自拥有一个全局变量 -- 线程的局部存储。

__thread int g_val = 0;

        进程是资源分配的基本单位,而线程是调度的基本单位,也有一部分数据是每一个线程独有的,比如线程id;线程也是要被调度的,寄存器中要存有这个线程的上下文,所以寄存器也是独有的,要调度肯定还有优先级也是独有的;每个线程要调用不同的函数完成各种功能,也定要入栈和出栈,临时变量要保存在栈中,如果栈也共享的,所有的执行流都要访问,另一个执行流可能就会覆盖你的数据,所以栈也是独有的寄存器和栈可以体现出线程的动态属性

线程的优点:

  • 线程之间切换需要操作系统做的工作更少,成本比较低。
  • 线程占用的资源要比进程少,它的资源也是从进程来的。

线程的缺点(对标多进程):

  • 线程间切换并不是没有成本,如果是单核单CPU的情况下创建一个线程是最好的,不会有切换的成本,所以进程不是创建的越多越好。
  • 健壮性会降低,多个线程都是用了全局变量,一个线程修改了就会影响别人。
  • 缺乏访问控制,后续也要有访问控制的方案。
  • 编程难度变高。

当tast_struct要切换的时候,为什么线程要比进程切换的成本低呢?

  1. 如果是同一个进程地址空间和页表不需要切换。
  2. 如果要调度的是另一个进程,就要把上下文、临时数据、页表、地址空间全都要切换
  3. CPU内部是有硬件级别的缓存的(cache),如果一条一条的从内存中读指令,那就会拉低效率,CPU会根据局部性原理预读一些指令。如果进程切换了cache就失效了,新进程来了,只能重新缓存。

线程异常

 还是演示除0错误。

#include <iostream>
#include <unistd.h>
#include <pthread.h>using namespace std;void* threadRoutine(void* args)
{while (true){cout << (char*)args << " running ..." << endl;sleep(1);int a = 10;a /= 0;}
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, (void*)"new thread");while (true){cout << "main thread running ..." << endl;sleep(1);}return 0;
}

        虽然这里看到主线程先运行,新线程后运行,其实线程的运行顺序是由调度器决定的。一个线程异常了就可能导致整个进程终止。

线程等待

        通过前几章说过的进程等待,线程也是需要等待的,如果主线程不等待,也会引发类似的僵尸问题,导致内存泄漏。

        线程等待用的就是这个接口。

参数:

  • thread:线程id,与创建不同的是不需要取地址
  • retval:线程退出的退出码。

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

 

void* threadRoutine(void* args)
{int i = 5;while (true){cout << (char*)args << " running ..., exit after " << i << " seconds" << endl;sleep(1);if (--i == 0) break;}cout << "new thread quit" << endl;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, (void*)"new thread");pthread_join(tid, nullptr); // 默认会阻塞等待cout << "main thread wait success ... main quit" << endl;return 0;
}

        我们已经看到了主线程在等新线程退出,pthead_join还有一个参数是一个void**,新线程执行的函数的返回值是一个void*,所以这第二个参数就是接受函数返回值的。

void* threadRoutine(void* args)
{int i = 5;while (true){cout << (char*)args << " running ..., exit after " << i << " seconds" << endl;sleep(1);if (--i == 0) break;}cout << "new thread quit" << endl;return (void*)1;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, (void*)"new thread");void* ret = nullptr;pthread_join(tid, &ret); // 默认会阻塞等待// 因为这个机器下默认就是64位的,所以不能强转成intcout << "main thread wait success ... main quit, new thread quit code: " << (long long)ret << endl;return 0;
}

        所以通过这个参数,我们不止可以返回一个数,也可以返回从堆上申请的空间,因为堆是共享的。

void* threadRoutine(void* args)
{int i = 5;int* data = new int[5];while (true){cout << (char*)args << " running ..., exit after " << i << " seconds" << endl;sleep(1);data[i-1] = i;if (--i == 0) break;}cout << "new thread quit" << endl;return (void*)data;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, (void*)"new thread");int* ret = nullptr;pthread_join(tid, (void**)&ret); // 默认会阻塞等待cout << "main thread wait success ... main quit" << endl;for (int i = 0; i < 5; i++){cout << ret[i] << endl;}return 0;
}

线程终止

原来我们使用的exit是终止进程的,那我们现在想要一个线程终止该怎么做呢?

作用:终止线程

参数:retval返回值

void* threadRoutine(void* args)
{int i = 5;while (true){cout << (char*)args << " running ..., exit after " << i << " seconds" << endl;sleep(1);if (--i == 0) break;}cout << "new thread quit" << endl;pthread_exit((void*)1);}

pthread_cancel

还有一种终止进程的方法就是取消进程。 

参数:要取消的线程的id。

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

void* threadRoutine(void* args)
{while (true){cout << (char*)args << " running ..." << endl;sleep(1);}cout << "new thread quit" << endl;pthread_exit((void*)1);}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, (void*)"new thread");int i = 0;while (true){cout << "main thread running ..." << endl;sleep(1);i++;if (i == 5) break;}pthread_cancel(tid);cout << "pthread_cancel: " << tid << endl;void* ret = nullptr;pthread_join(tid, (void**)&ret); // 默认会阻塞等待cout << "main thread wait success ... main quit, new thread quit code: " << (long long)ret << endl;return 0;
}

        线程被取消,join的退出码会被设置为-1。

        使用pthread_cancel时,取消的一定是一个已经跑起来的线程,我觉得不需要了,才取消的。

        一般都是使用主线程去取消新线程,如果主线程被新线程取消了,那么谁来等待这个新线程退出呢,所以一般不这样做。

        我们也看到了pthread_cancel取消的线程id是很长的一串,并不是我们使用ps aL看到的LWP,它本质上是一个地址,因为我们用的不是Linux自带的接口,而是pthread库提供的接口。        

        一个线程也要有自己的属性,它也要被管理起来,操作系统是对轻量级进程的调度,还有内核数据结构的管理,库也要给用户提供线程相应的属性,Linux下使用进程模拟的线程,用户想要知道一个线程的退出结果,线程的参数、栈结构等,这些内核是不管的,所以就要库在用户层来管理这个线程,就是在库中管理相应的结构。

为了更好的让线程找到自己的用户层属性,就把每个线程结构的起始地址设置为tid

        那么把每个线程的栈结构都放在了共享区,那么地址空间中的栈结构还用不用呢?那肯定是用的,主线程用的就是内核级的栈结构新线程用的就是共享区提供的栈结构,这也不会和单进程一个执行流冲突,一个线程那用的就是内核级的栈区。

 

作用:获取线程的tid

// ...cout << (char*)args << " running ..., new tid: " << pthread_self() << endl;
// ...cout << "main thread running ..., main tid: " << pthread_self() << endl;
// ...

进程替换

        如果我们使用execl进程替换函数,那么整个进程都要被替换,包括每一个线程,所以不管在哪一个线程下调用execl系列的进程进程替换函数,整个进程都会被替换,所以它才被叫做进程替换。

线程分离

        默认情况下,每一个新建的线程都是要被等待的,如果线程退出不使用pthread_join就无法释放资源,造成内存泄漏。但是pthread_join默认是阻塞等待的,没有非阻塞等待的设置,如果我不关心线程返回的是什么,就可以使用线程分离

参数:线程的tid

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

pthread_detach和pthead_join是不能一起使用的,你都已经分离了,我就不能再等了。

void* threadRoutine(void* args)
{pthread_detach(pthread_self());while (true){cout << (char*)args << " running ..., new tid: " << pthread_self() << endl;sleep(1);break;}pthread_exit((void*)1);
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, (void*)"new thread");while (true){cout << "main thread running ..., main tid: " << pthread_self() << endl;sleep(1);break;}int n = pthread_join(tid, nullptr);cout << "n: " << n << ", " << strerror(n) << endl; return 0;
}

没有线程分离就正常等待。


线程互斥

下面的这些概念在原来的时候也说过,我们现在再来完善一下:

  • 临界资源:多个线程执行流看到的同一份资源叫做临界资源
  • 临界区:每个线程内部访问临界资源的代码就叫做临界区
  • 互斥:为了保护临界区,多执行流任何时刻只能有一个进程进入临界区,这就叫做互斥
  • 原子性:不会被任何调度机制打断,对于一件事要么做要么不做,没有中间状态就成为原子性

下面就来看一些实例,比如我们模拟一个卖票的程序。

int tickets = 1000; // 定义了1000张票,这个数字不重要void* getTickets(void* args) // 多个执行流可能同时进入这个函数,这个函数就被重入了
{(void)args;while (true){if (tickets > 0){usleep(1000);cout << pthread_self() << " : " << tickets << endl;tickets--;}else{break;}}
}int main()
{pthread_t t1, t2, t3;pthread_create(&t1, nullptr, getTickets, nullptr);pthread_create(&t2, nullptr, getTickets, nullptr);pthread_create(&t3, nullptr, getTickets, nullptr);pthread_join(t1, nullptr);pthread_join(t2, nullptr);pthread_join(t3, nullptr);return 0;
}

        我们看到的是票数出现了-1,这就代表着多卖了一张,这可是一个很严重的错误。我们执行的操作tickets--其实做了三步。

        第一步把内存中的数据读到CPU中,第二步tickets--,第三步放回到内存中。线程1在这个操作执行的过程中因为某些原因发生了线程切换,可能线程2已经把tickets减到0了,这时候你这个线程1被换回来,你才减了一次,又把这个数放到了内存中。

        又或者此时的tickets已经到1了,这时候你这个线程1又被切换了,线程2已经把tickets减到0了,这时候线程1回来了,因为刚才判断时tickets大于0,现在又把tickets--,这就变成了-1。

        所以tickets这个全局变量在并发访问的时候导致了数据不一致的问题。

mutex

        为了解决上面的这种问题就要有一个新的概念就是互斥锁。这是原生线程库提供的一个数据类型。

int tickets = 1000;pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER; // 这是一个全局的锁void* getTickets(void* args) // 多个执行流可能同时进入这个函数,这个函数就被重入了
{(void)args;while (true){pthread_mutex_lock(&mtx); // 加锁的同时也要注意解锁,这是一个全局变量// 临界区if (tickets > 0){cout << pthread_self() << " : " << tickets << endl;tickets--;pthread_mutex_unlock(&mtx);}else{pthread_mutex_unlock(&mtx);break;}}
}int main()
{pthread_t t1, t2, t3;pthread_create(&t1, nullptr, getTickets, nullptr);pthread_create(&t2, nullptr, getTickets, nullptr);pthread_create(&t3, nullptr, getTickets, nullptr);pthread_join(t1, nullptr);pthread_join(t2, nullptr);pthread_join(t3, nullptr);return 0;
}

        这样我们就看到了tickets变为了1,还可以看到一个现象就是所有的pthread_self打印的tid都是一样的,这是因为选择哪个线程完全是操作系统说了算的,但是我们可以模拟一下买完票的后续操作。

void* getTickets(void* args) // 多个执行流可能同时进入这个函数,这个函数就被重入了
{(void)args;while (true){pthread_mutex_lock(&mtx); // 加锁的同时也要注意解锁,这是一个全局变量// 临界区if (tickets > 0){cout << pthread_self() << " : " << tickets << endl;tickets--;pthread_mutex_unlock(&mtx);}else{pthread_mutex_unlock(&mtx);break;}// 模拟后续动作usleep(1000);}
}

【注意】:加锁的粒度越小越好,尽量不在加锁和解锁中间放一些无关的代码。

mutex接口

我们再来看一下这些函数。

作用:初始化互斥锁

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);

参数:

  • mutex:需要初始化的互斥锁。
  • attr:初始化互斥锁的属性,一般设置为nullptr

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

上面的代码我们使用的:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

调用pthread_mutex_init函数初始化互斥锁叫做动态分配,除此之外,我们还可以用下面这种方式初始化互斥锁,该方式叫做静态分配,这个互斥锁不需要销毁。

 

作用:销毁互斥锁

int pthread_mutex_destroy(pthread_mutex_t *mutex);

参数:要销毁的互斥锁

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

 

作用:给临界资源加锁

int pthread_mutex_lock(pthread_mutex_t *mutex);

参数:要使用的互斥锁

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

 

作用:给临界资源解锁

int pthread_mutex_unlock(pthread_mutex_t *mutex);

参数:要使用的互斥锁

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

下面我们就写一个完整的代码:

int tickets = 1000;#define THREAD_NUM 3class ThreadData
{
public:ThreadData(const string& s, pthread_mutex_t* pm):tname(s),pmtx(pm){}string tname;pthread_mutex_t* pmtx;
};void* getTickets(void* args) // 多个执行流可能同时进入这个函数,这个函数就被重入了
{ThreadData* td = (ThreadData*)args;(void)args;while (true){pthread_mutex_lock(td->pmtx);// 临界区if (tickets > 0){cout << td->tname << " : " << tickets << endl;tickets--;pthread_mutex_unlock(td->pmtx);}else{pthread_mutex_unlock(td->pmtx);break;}// 模拟后续动作usleep(1000);delete td;}
}int main()
{pthread_mutex_t mtx;pthread_mutex_init(&mtx, nullptr);pthread_t t[THREAD_NUM];for (int i = 0; i < THREAD_NUM; i++){string name = "thread ";name += to_string(i + 1);ThreadData* td = new ThreadData(name, &mtx);pthread_create(t + i, nullptr, getTickets, (void*)td);}for (int i = 0; i < THREAD_NUM; i++){pthread_join(t[i], nullptr);}pthread_mutex_destroy(&mtx);return 0;
}

mutex的理解

        如果给临界区加锁后,那么线程在临街中是否会切换。那是一定会切换的,因为切不切换是操作系统说了算的。既然它还要切换,会不会出现上述的问题呢?

        虽然被切换了,在访问tickets的时候,这个线程是持有锁的。其他线程想要访问,那就得先申请锁申请锁也不会成功,所以就被阻塞了,这就保证了临界区数据一致性,所以访问临界资源先申请锁,用完后释放锁,这才是正确的编码方式。

        所以对这个持有锁的线程,其他线程就认为这个线程的操作是原子的

        加锁之后就可以保证临界区的代码一定是串行的

        换言之,每个进程都要申请同一个锁,这个锁不也就是共享资源吗,所以也要保证锁的安全,那申请锁和释放锁也必须是原子的。

互斥锁的实现

        从汇编的角度来说,要是只有一条汇编语句,我们就认为该汇编语句执行时是原子的。在汇编语言中有一条swap或exchange指令,用这一条语句将CPU内的寄存器和内存数据进行交换。

        多执行流中,在CPU中所有的寄存器存放的是当前执行流的上下文,这些数据是该执行流私有的,但寄存器的空间是被所有执行流共享的。


可重入和线程安全

  • 现在就可以理解什么是重入了,抢票的函数就是一个可重入的函数
  • 线程安全:多个线程并发访问同一段代码时不会出现不同的结果。

常见的线程不安全情况:

  • 不保护临界资源的函数。

  • 函数状态随着调用发生了变化的函数。

  • 返回指向静态变量指针的函数。

  • 调用线程不安全的函数。

常见的线程安全的情况:

  • 每个线程对全局变量或者静态变量只有读取的权限。
  • 类或者接口对于线程来说都是原子操作,就像只用一条语句完成交换。

常见不可重入的情况:

  • 调用了malloc/free,因为malloc函数是用全局链表来管理堆的。
  • 调用标准IO库函数,函数大多都是不可重入的方式使用全局数据结构。
  • 可重入函数内使用了静态的数据结构。

常见可重入的情况:

  • 不使用全局变量或静态变量。
  • 不使用malloc或者new开辟出的空间。
  • 不调用不可重入函数。
  • 不返回静态或全局数据,所有数据都由函数的调用者提供。
  • 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据,一开始定义一个临时变量保存数据,函数执行完后再用临时数据恢复。

可重入和线程安全的联系:

  • 函数可重入,那么线程一定是安全的。
  • 函数不可重入,那么就有可能引发线程安全问题。

可重入函数和线程安全的区别:

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

死锁

什么是死锁

        死锁是指在一组执行流中的各个执行流均占有不会释放的资源,但因互相申请被其他进程所占用不会释放的资源而处于的一种永久等待状态

        别说多个了,一个执行流如果重复申请锁,也会产生死锁,你已经申请了一个锁了,在申请一看al是0,那直接就被阻塞了,但是你还拿着锁呢,所以写代码的时候也要注意。

        多执行流就像下图,锁执行线程表示分配给了他,线程指向锁代表要申请锁,此时线程a持有锁1,线程b持有锁2,线程a还想要锁2,线程b还想要锁1,那这两个线程就都阻塞了,这就叫做死锁。

死锁产生的必要条件

  • 互斥条件:一个资源每次只能被一个执行流使用,使用了互斥锁
  • 请求与保持条件: 一个执行流因请求资源而阻塞时,对已获得的资源保持不放,我已经有了一个锁了,还想再申请另一个锁。
  • 不可抢占条件: 一个执行流已获得的资源,在未使用完之前,不能强行夺取。
  • 循环等待条件: 若干执行流之间形成一种头尾相接的循环等待资源的关系,最后形成一个环。

避免死锁

  • 破坏死锁的四个必要条件
  • 加锁顺序一致
  • 避免锁未释放的场景
  • 资源一次性分配

线程同步

概念

  • 同步: 在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,这就叫做同步。
  • 竞态条件: 因为时序问题,而导致程序异常,称之为竞态条件。

        加锁也是会存在某些问题的,如果个别线程的竞争力特别强,每次都能够申请到锁,但申请到锁之后啥也不做,所以这个线程就一直在申请锁和释放锁,这就可能导致其他线程长时间竞争不到锁,引起饥饿问题,他可以这样做,就是不合理。
        加锁没有错,它能够保证在同一时间段只有一个线程进入临界区,但没有高效的使用这份临界资源。
        现在就规定,当一个线程释放锁后,这个线程不能立马再次申请锁,该线程必须排到这个锁的资源等待队列的最后。

        所以多执行流按照一定的顺序进行对临街资源的访问就叫做线程同步,引入线程同步就是为了解决访问临界资源不合理的问题。

条件变量

        当我们申请临界资源的时候,先要对临界资源是否存在做检测,要检测也是要访问临界资源。所以对临街资源的检测也要在加锁和解锁之间,这就导致了频繁的检测就绪条件,也就要频繁的申请释放锁,为了解决这个问题提出来一下建议:

  1. 不要让线程频繁的检测,要让他等待
  2. 当条件就绪的时候,通知对应的线程,让他进行资源申请和访问。

这就引出了一个概念:条件变量

条件变量函数

这些函数返回值类型都是int,成功返回0,失败返回错误码。

作用:动态分配初始化条件变量。

int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);

参数:

  • cond:需要初始化的条件变量。
  • attr:初始化条件变量的属性,一般设置为nullptr。

作用:静态分配初始化条件变量,不需要销毁。

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

作用:销毁条件变量。

int pthread_cond_destroy(pthread_cond_t *cond);

参数:cond:需要销毁的条件变量。

作用:等待条件变量满足

int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);

参数:

  • cond:需要等待的条件变量。
  • mutex:当前临界区对应的互斥锁

        再来说一下这个第二个参数为什么要传入锁,我们在访问临界资源的时候一定是要先检测,如果条件不满足再使用pthread_cond_wait,检测本身也是在访问临界资源,所以都是先加锁,再检测,如果要是检测出条件不满足就要阻塞这个线程,这时是带着锁阻塞的,那么其他线程想要申请锁就不会申请成功,所以这个函数的第二个参数要传入锁,调用的时候就会释放这个锁,别的线程就可以申请锁了。

        之后当线程被唤醒的时候也会自动帮我们获取锁

作用:唤醒等待

int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
  • pthread_cond_signal函数用于唤醒等待队列中首个线程。
  • pthread_cond_broadcast函数用于唤醒等待队列中的全部线程。

参数:唤醒在cond条件变量下等待的线程。

        下面就简单的演示一下条件变量是怎么用的,但是没有对应的场景还是无法更好的理解,而且使用条件变量的时候是有问题的,后面会有场景的。

#include <iostream>
#include <unistd.h>
#include <pthread.h>using namespace std;#define TNUM 4typedef void(*func_t)(const string& name, pthread_mutex_t* pmtx, pthread_cond_t* pcond);volatile bool quit = false; // 退出条件class ThreadData
{
public: ThreadData(const string& name, func_t func, pthread_mutex_t* pmtx, pthread_cond_t* pcond):_name(name),_func(func),_pmtx(pmtx),_pcond(pcond){}
public:string _name;           // 线程名func_t _func;           // 线程要执行的函数pthread_mutex_t* _pmtx; // 互斥锁pthread_cond_t* _pcond; // 条件变量
};void func1(const string& name, pthread_mutex_t* pmtx, pthread_cond_t* pcond)
{while (!quit){   // 运行到wait的时候,当前线程会立即被阻塞,每个线程都要这样做pthread_mutex_lock(pmtx);// 这里的等待就是在检测资源是否就绪,它应该在加锁与解锁之间的pthread_cond_wait(pcond, pmtx);cout << name << " running..." << endl;pthread_mutex_unlock(pmtx);}
}void func2(const string& name, pthread_mutex_t* pmtx, pthread_cond_t* pcond)
{while (!quit){pthread_mutex_lock(pmtx);pthread_cond_wait(pcond, pmtx);cout << name << " running..." << endl;pthread_mutex_unlock(pmtx);}
}void func3(const string& name, pthread_mutex_t* pmtx, pthread_cond_t* pcond)
{while (!quit){pthread_mutex_lock(pmtx);pthread_cond_wait(pcond, pmtx);cout << name << " running..." << endl;pthread_mutex_unlock(pmtx);}
}void func4(const string& name, pthread_mutex_t* pmtx, pthread_cond_t* pcond)
{while (!quit){pthread_mutex_lock(pmtx);pthread_cond_wait(pcond, pmtx);cout << name << " running..." << endl;pthread_mutex_unlock(pmtx);}
}void* Entry(void* args)
{ThreadData* td = (ThreadData*)args; // td在每一个线程私有的栈结构中保存td->_func(td->_name, td->_pmtx, td->_pcond);delete td; // 当函数执行完后也要释放td,因为它也是new出来的
}int main()
{pthread_mutex_t mtx; // 互斥锁pthread_cond_t cond; // 条件变量// 初始化pthread_mutex_init(&mtx, nullptr);pthread_cond_init(&cond, nullptr);pthread_t tids[TNUM]; // 创建多线程func_t funcs[TNUM] = {func1, func2, func3, func4}; // 每个线程独有的方法for (int i = 0; i < TNUM; i++){string name = "Thread ";name += to_string(i + 1);ThreadData* td = new ThreadData(name, funcs[i], &mtx, &cond);pthread_create(tids + i, nullptr, Entry, (void*)td);}// 让主线程区一个一个唤醒int cnt = 10;while (cnt--){sleep(1);cout << "resume thread run code: " << endl;pthread_cond_signal(&cond);// pthread_cond_broadcast(&cond); // 唤醒所有等待的线程}cout << "ctrl done" << endl;quit = true;pthread_cond_broadcast(&cond); // 再唤醒所有线程,检测退出条件// 等待线程退出for (int i = 0; i < TNUM; i++){pthread_join(tids[i], nullptr);cout << "thread: " << tids[i] << " quit" << endl;}// 销毁pthread_mutex_destroy(&mtx);pthread_cond_destroy(&cond);return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/743792.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

一个悄然崛起的AI开源项目!

众所周知&#xff0c;最近这半年AI相关的话题实在是火到出圈。尤其是生成式AI的流行&#xff0c;让我们普通人也可以近距离地接触和应用AI。这其中最典型的就是ChatGPT。 那除了ChatGPT&#xff0c;还有一个非常实用的领域&#xff0c;也是我们今天要讨论的话题&#xff0c;那…

Day33:安全开发-JavaEE应用SQL预编译Filter过滤器Listener监听器访问控制

目录 JavaEE-预编译-SQL JavaEE-过滤器-Filter JavaEE-监听器-Listen 思维导图 Java知识点 功能&#xff1a;数据库操作&#xff0c;文件操作&#xff0c;序列化数据&#xff0c;身份验证&#xff0c;框架开发&#xff0c;第三方库使用等. 框架库&#xff1a;MyBatis&#…

学习SSM的记录(八)-- SSM整合项目《任务列表案例》

前端程序搭建和运行 项目预览 接口分析 1.学习计划分页查询 需求&#xff1a;查询对应数据页数据 uri&#xff1a;schedule/{pageSize}/{currentPage} 请求方式&#xff1a;get 响应数据&#xff1a;json {"code":200,"flag":true,"data"…

基于springboot实现数据资产管理系统项目【项目源码+论文说明】计算机毕业设计

基于springboot实现数据资产管理系统演示 摘要 固定资产管理系统主要是完成对系统用户管理、资产信息管理、资产变更管理、资产用途管理、资产类别管理和资产增减管理。因为利用本系统管理员可以直接录入信息&#xff0c;修改信息&#xff0c;删除信息&#xff0c;并且若在录入…

JAVA初阶数据结构链表(2)双向链表( +专栏数据结构练习是完整版)

1.双向链表的结构&#xff08;双向不带头不循环链表&#xff09; 需要注意的一点就是&#xff0c;在jdk中的链表就是双向链表 一个节点有三个域 val&#xff08;数值域&#xff09; next&#xff08;地址域&#xff09; prev&#xff08;前驱记录前一个节点的地址&#xff09…

力扣思路题:重复的子字符串

注意比较j与j-i是否相同 bool repeatedSubstringPattern(char* s) {int i;int nstrlen(s);bool flag;for(int i1;i<n/2;i){if(n%i0){flagtrue;}for(int ji;j<n;j){if(s[j]!s[j-i]){flagfalse;break;}}if(flagtrue){return true;}}return false; }

腾讯云服务器地域有啥区别?选哪个比较好?

腾讯云服务器地域怎么选择&#xff1f;不同地域之间有什么区别&#xff1f;腾讯云哪个地域好&#xff1f;地域选择遵循就近原则&#xff0c;访客距离地域越近网络延迟越低&#xff0c;速度越快。腾讯云百科txybk.com告诉大家关于地域的选择还有很多因素&#xff0c;地域节点选择…

sql server 恢复数据库、恢复单表数据的方法

如果不小心把某个表的数据删了&#xff0c;可以用之前的备份文件对单表进行数据恢复。 1、新建一个数据库&#xff08;全新的数据库&#xff09;&#xff0c;记得路径&#xff0c;恢复的时候要用到&#xff0c;新建完不要对数据库做什么操作。 2、用需要恢复表的数据库的备份文…

基于FPGA的OV7725摄像头的HDMI显示(含源码)

1、概述 本文FPGA通过SCCB接口初始化OV7725摄像头寄存器&#xff0c;然后采集OV7725的摄像头数据&#xff0c;使用DDR3对数据进行暂存&#xff0c;最后将数据输出到HDMI显示器上进行显示。 该工程对应系统框图如下所示&#xff0c;主要包含OV7725驱动及数据处理模块、DDR3读写控…

CSS概念及入门

CSS概念及入门 简介 CSS 的全称为&#xff1a;层叠样式表 ( Cascading Style Sheets ) 。 CSS 也是一种标记语言&#xff0c;用于给 HTML 结构设置样式&#xff0c;例如&#xff1a;文字大小、颜色、元素宽高等等。 主流的布局方式:divcss。 组成 选择器 用于选择页面中的…

防御安全(IPSec实验)

目录 需求&#xff1a; pc1 ping通 pc2 ,使用IPSec VPN 拓扑图&#xff1a; ​编辑实验配置&#xff1a; 注意&#xff1a; 直接在路由器r1和r2分别配置即可&#xff0c;路由器r1和r2要写一条缺省指向ISP 实验配置截图如下&#xff1a; 2. r1​编辑 3. r3​编辑 3.r…

Go——数组

Golang Array和以往认知的数组有很大的。 数组是同一种数据类型的固定长度的序列。数组定义&#xff1a;var a[len] int&#xff0c;比如&#xff1a;var a [5]int&#xff0c;数组长度必须是常量&#xff0c;且类型的组成部分。一旦定义&#xff0c;长度不能变。长度是数组类…

docker镜像ssh服务

基于commit命令实现 首先我们是基于Ubuntu:18.04版本做ssh服务&#xff0c;拉取镜像 [rootmaster ~]# docker pull ubuntu:18.04 [rootmaster ~]# docker images REPOSITORY TAG IMAGE ID CREATED SIZE ubuntu 18.04 f9a80a5…

vue3 el-form中嵌套el-tabale 对输入动态校验

简单案例 <el-form :model"Form" :rules"rules" ref"FormRef" class"formDiv"><el-table :data"Form.copyWriters" style"width: 100%"><el-table-column label"文案链接"><temp…

rancher是什么

Rancher Labs是制作Rancher的公司。Rancher Labs成立于2014年&#xff0c;是一家专注于企业级容器管理软件的公司。它的产品设计旨在简化在分布式环境中部署和管理容器的过程&#xff0c;帮助企业轻松地采用容器技术和Kubernetes。Rancher Labs提供的Rancher平台支持Docker容器…

H5 简单四按钮个人主页源码

源码名称&#xff1a;简单四按钮个人主页源码 源码介绍&#xff1a;一款简单的带4个按钮选项的个人主页源码&#xff0c;可自行修改内容作为自己的个人主页。 需求环境&#xff1a;H5 下载地址&#xff1a; https://www.changyouzuhao.cn/11458.html

ubuntu 23.04 安装 中文输入法

1、安装 fcitx sudo apt install fcitxfcitx 安装好后&#xff0c;可以使用 fcitx-configtool 命令进行配置&#xff0c;其界面如下所示。在这里可以配置不同输入法的切换快捷键&#xff0c;默认输入法等。刚安装系统后&#xff0c;这里只有一个输入法&#xff0c;所以接下来要…

元宇宙崛起:区块链与金融科技共绘数字新世界

文章目录 一、引言二、元宇宙与区块链的深度融合三、区块链在元宇宙金融中的应用四、金融科技在元宇宙中的创新应用五、面临的挑战与机遇《区块链与金融科技》亮点内容简介获取方式 一、引言 随着科技的飞速发展&#xff0c;元宇宙概念逐渐走进人们的视野&#xff0c;成为数字…

先进电气技术 —— 片上宽禁带器件的集成与应用

一、背景 功率转换器设计的一个关键目标是降低功率损耗以提高转换效率&#xff0c;这对可再生能源等应用产生影响。通过降低功率损耗&#xff0c;可以减小变换器元件的尺寸&#xff0c;从而使整个变换器的尺寸更小。因此&#xff0c;转换器的大小和成本完全取决于设计要求和应…

Linux:kubernetes(k8s)prestop事件的使用(11)

他的作用是在结束pod容器之后进行的操作 apiVersion: v1 # api文档版本 kind: Pod # 资源对象类型 metadata: # pod相关的元数据&#xff0c;用于描述pod的数据name: nginx-po # pod名称labels: # pod的标签type: app #这个是随便写的 自定义的标签version: 1.0.0 #这个…