【Linux】-- 多线程安全
目录
进程互斥
计算 -> 时序问题
加锁保护
pthread_mutex_lock
pthread_mutex_unlock
使用init与destory
pthread_mutex_init
phtread_mutex_destory
锁的实现原理
图
可重入VS线程安全
死锁
Linux线程同步
条件变量
系统调用
进程互斥
进程线程间的互斥相关背景概念:
- 临界资源:多线程执行流共享的资源,在任何时刻只能被一个执行流访问的资源,就叫做临界资源。(一个资源被多执行流共享的情况下,通过一定的方法让任何时刻,只能被一个执行流访问的资源)
- 临界区:每个线程内部,访问临界资源的代码,就叫做临界区。(没有访问临界资源的代码,叫做正常代码)
- 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。
- 原子性:在任意时刻不会因为某些原因,事物只做一半,要么不做,要么就做全。即:该操作只有两态,要么完成,要么未完成。
需要互斥的场景:
大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互 —— 此时,因为其中的一些特性(数据不一致)就会使用到互斥。
#include <iostream>
#include <cstdio>
#include <pthread.h>
#include <unistd.h>int tickets = 1000;void *getTickets(void *args)
{(void)args; // 防止release下因为未使用而报错while(true){if(tickets > 0){usleep(1000);printf("%p: %d\n", pthread_self(), tickets);tickets--;}elsebreak;}return nullptr;
}int main()
{pthread_t t1,t2,t3;// 多线程抢票的逻辑 - t1、t2、t3对getTickets函数重入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);
}
(不)可重入函数
main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的 时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后向链表中插入两个节点,而最后只有一个节点真正插入链表中了。于是便出现了经典的内存泄漏问题。此问题存在,且非常不容易排查。
排查代码的时候,可以发现,我们的代码写的没有任何问题。对应的main函数、单链表头插insert、信号捕捉sighandler、函数调用都没有问题 —— 这个问题的产生严格来说并不是代码问题,而是因为操作系统调度导致的进程时序的变化 —— 时序问题。
像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入。insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为不可重入函数,反之, 如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。
可重入函数 VS 不可重入函数
是函数的一种特征,目前我们用的90%函数,都是不可重入的。
- 不可重入函数:好编写。
- 可重入函数:不好编写,书写成本高。
如果一个函数符合以下条件之一则是不可重入的:
- 调用了new、malloc或free,因为new、malloc也是用全局链表来管理堆的。
- 99%的STL容器,都是不可重入的。
- 函数里面带static的。
- 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。
可重入函数:需要保证函数其是独立的,没有访问任何的全局数据。
此时,我们会发现一个问题,票居然减到负数了。所以 —— 在多线程的抢票下,对于共享资源如果不加以保护,就可能在一定的时序下,引起问题。
计算 -> 时序问题
代码经过编译,变成二进制可执行程序。加载之后运行,其一定在内存里,代码和数据都在内存里。而计算需要CUP进行计算,所以计算的第一种事情就是:将内存当中的数据,加载入CPU当中 —— 线程读取内存中的数据,读到CPU的寄存器里 —— 把数据读到当前执行流的上下文当中。
判断的本质也是一种计算
计算分类:
- 数值计算
- 逻辑计算
从切换讲:
当一个线程刚刚将这个判断加载到寄存器里,就被切换了。其他线程也过来将这个判断加载到寄存器里。此时如果判断就是true,就会都是读到true。于是多个执行流进入循环进行抢票,多个线程减票数,于是导致票成负数。
从并行讲:
多个线程并行运行,可能多个线程同时判断true,进入循环进行抢票。于是多个执行流进入循环进行抢票,多个线程减票数,于是导致票成负数。
Note:
因为由于并发的问题,进而导致对应的数据变成负数。
同样的道理,对全局数据的 -- 操作也会有问题。
因为 ticket-- 不是原子性的:
400511: 8b 05 15 0b 20 00 mov 0x200b15(%rip),%eax # 60102c <ticket>
400517: 83 e8 01 sub $0x1,%eax
40051a: 89 05 0c 0b 20 00 mov %eax,0x200b0c(%rip) # 60102c <ticket>
引发了数据紊乱的问题,造成了数据不一致的问题。这就是因为线程不断切换,而对一个不加保护的全局变量,做切换,可能会引发的问题。
此处问题tickets的票数为负数的问题就是二者共同导致的,于是我们要避免这样问题的产生 —— 多执行流,对这类问题的数据进行保护 —— 加锁保护。
加锁保护
写代码的过程中,使用的最高频的保护策略:互斥锁
锁的使用原理:
- 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
- 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
- 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
首先,需要我们自己定义一把锁:
随后,需要我们将锁进行初始化:
1. 对于局部的锁初始化利用pthread_mutex_init。(也可以对全局的使用)
(较为难,后面讲解)
2. 对这把锁,只要是我们定义的是静态或全局,我们可以直接使用PTHREAD_MUTEX_INITIALIZER(宏),进行初始化。
有了定义的这一把锁,并初始化之后。就可以直接使用这一把锁,来进行对特定的区域进行保护。
于是这把锁用来保护全局变量tickets,称作为:对临界资源进行访问时,对其进行保护。即,对其中访问临界资源的临界区进行加锁保护。对临界区加锁 —— pthread_mutex_lock。
pthread_mutex_lock
#include <pthread.h> // 加锁 int pthread_mutex_lock(pthread_mutex_t *mutex);
参数:
mutex:传入我们定义并初始化的一把锁。
返回值:
- 成功,返回0。
- 失败,返回错误码。
pthread_mutex_lock—— 申请锁的时候,别的线程也在申请,如果申请锁不成功,会直接阻塞。
pthread_mutex_trylock —— 申请锁的时候,别的线程也在申请,如果申请锁不成功,会直接返回。
(意义凸显于死锁,在死锁部分讲解)
使用锁对特定的区域进行保护:
所有的线程进入循环抢票时,都需要先执行语句: pthread_mutex_lock(&mtx); 。于是由于这个锁的特点:任何一个时刻,只允许一个成功的线程,成功的获取这把锁,然后,继续向后执行。其他的线程只能默认的阻塞等待,直到拿到锁的线程最终将锁释放掉,才能有机会进入执行 —— 这就是互斥概念。
可以理解为:
加锁 ——> 互斥 ——> 串行执行。
有加锁操作,就有解锁操作 —— pthread_mutex_unlock。
pthread_mutex_unlock
#include <pthread.h> // 解锁 int pthread_mutex_unlock(pthread_mutex_t *mutex);
参数:
mutex:传入我们定义并初始化的一把锁。
返回值:
- 成功,返回0。
- 失败,返回错误码。
注意一定要解锁,因为锁是全局的,就算具有锁的进程退出了(终止了)。但是,全局的锁还是处于被修改的状态,也就是处于加锁状态,其他线程就无法继续向后执行。
此处容易出现一个错误:
- 加锁(pthread_mutex_lock)与解锁(pthread_mutex_unlock)之间的代码就称作临界区。
- 被串行化访问的共享的全局的tickets就叫做临界资源。
以前是多线程并发的执行,也就是彼此之间的数据层面会影响,但是执行之间是各自跑各自的不会互相干扰。但是当加锁之后,就会引起执行流之间互相影响,只能一个执行流跑。这一定的也就降低了效率,并且会导致一个新的,几乎无法解决的问题。
#include <iostream>
#include <cstdio>
#include <pthread.h>
#include <unistd.h>int tickets = 1000;// 加锁保护
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER; // pthread_mutex_t 就是原生数据库提供的一个数据类型void *getTickets(void *args)
{(void)args; // 防止release下因为未使用而报错while (true){pthread_mutex_lock(&mtx);if (tickets > 0) // 1.判断的本质也是计算的一种{usleep(1000);printf("%p: %d\n", pthread_self(), tickets);tickets--; // 2.也可能出现问题pthread_mutex_unlock(&mtx);}else{pthread_mutex_unlock(&mtx);break;}}return nullptr;
}int main()
{pthread_t t1, t2, t3;// 多线程抢票的逻辑 - t1、t2、t3对getTickets函数重入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);
}
补充:
此处不用当一回事,只是一个理解见识,真正的知识属于线程同步的概念(后面讲解)
为什么打印线程,发现一段时间内都是一个线程?
因为在抢票的时候,因为每个线程都有自己的时间片。一个线程在执行抢票期间,其他的线程在进行等待,执行的线程抢完票就解锁了。可是有可能此线程优先级很高,进而又得到锁继续抢票。
主要原因:所有的新线程执行的内容一样,导致调度器内部出现此问题。可以让每一个线程执行不同的任务改变。
#include <iostream> #include <cstdio> #include <cstdlib> #include <ctime> #include <pthread.h> #include <unistd.h>int tickets = 1000;// 加锁保护 pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER; // pthread_mutex_t 就是原生数据库提供的一个数据类型void *getTickets(void *args) {(void)args; // 防止release下因为未使用而报错while (true){pthread_mutex_lock(&mtx);if (tickets > 0) // 1.判断的本质也是计算的一种{usleep(1000);printf("%s: %d\n", (void*)args, tickets);tickets--; // 2.也可能出现问题pthread_mutex_unlock(&mtx);}else{pthread_mutex_unlock(&mtx);break;}usleep(rand()%10000); // 模拟每一个线程的任务不同}return nullptr; }int main() {srand((unsigned long)time(nullptr));pthread_t t1, t2, t3;// 多线程抢票的逻辑 - t1、t2、t3对getTickets函数重入pthread_create(&t1, nullptr, getTickets, (void*)"thread one");pthread_create(&t2, nullptr, getTickets, (void*)"thread two");pthread_create(&t3, nullptr, getTickets, (void*)"thread three");// 线程等待pthread_join(t1, nullptr);pthread_join(t2, nullptr);pthread_join(t3, nullptr); }
Note:
加锁保护:加锁的时候,一定要保证加锁的粒度,越小越好。不要将无关竟要的代码也放入。
使用init与destory
pthread_mutex_init
#include <pthread.h> //对我们定义的锁进行初始化。 int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
参数:
mutex:传入我们定义的一把锁。
attr:锁的属性 —— 可以不管,直接设为nullptr。
返回值:
- 成功,返回0。
- 失败,返回错误码。
使用pthread_mutex_init初始化局部锁。 并且最后不要锁了,就必须要用phtread_mutex_destory,对锁进行释放。
phtread_mutex_destory
#include <pthread.h> //销毁锁 int pthread_mutex_destory(pthread_mutex_t *mutex);
参数:
mutex:传入我们定义并初始化的一把锁。
返回值:
- 成功,返回0。
- 失败,返回错误码。
销毁互斥量需要注意:
使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁。 不要销毁一个已经加锁的互斥量。 已经销毁的互斥量,要确保后面不会有线程再尝试加锁。
需要注意的是,我们在主线程内部创建的局部锁,线程是看不到的,所以我们需要将锁传递给每一个线程,此处也是对pthread_create的第四个参数的升级利用,此处用可以采取使用类的方式,封装锁等,传递进入。
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <ctime>
#include <pthread.h>
#include <unistd.h>int tickets = 1000;class ThreadData
{
public:ThreadData(const std::string &n, pthread_mutex_t *pm):tname(n), pmtex(pm){}std::string tname;pthread_mutex_t *pmtex;
};void *getTickets(void *args)
{ThreadData *td = (ThreadData*)args;while (true){pthread_mutex_lock(td->pmtex);if (tickets > 0) // 1.判断的本质也是计算的一种{usleep(1000);printf("%s: %d\n", td->tname.c_str(), tickets);tickets--; // 2.也可能出现问题pthread_mutex_unlock(td->pmtex);}else{pthread_mutex_unlock(td->pmtex);break;}usleep(rand()%10000); // 模拟每一个线程的任务不同}delete td;return nullptr;
}#define THREAD_NUM 3int main()
{pthread_mutex_t mtx;pthread_mutex_init(&mtx, nullptr);srand((unsigned long)time(nullptr));pthread_t tid[THREAD_NUM];for(int i = 0; i < THREAD_NUM; ++i){std::string name = "thread ";name += std::to_string(i + 1);ThreadData *td = new ThreadData(name, &mtx);pthread_create(tid + i, nullptr, getTickets, (void*)td);}// 线程等待for(int i = 0; i < THREAD_NUM; ++i){pthread_join(tid[i], nullptr);}pthread_mutex_destroy(&mtx);return 0;
}
#问:加锁了之后,线程在临界区中,是否会切换?
会切换,因为是多条语句,操作系统有切换的权利,但是不会有问题。
使用层面上的理解:
当,持有锁的线程进入临界区的时候,是随时随地可能被切换,但是这与平时的不同。因为临界区中且换,该线程一定抢到了锁,并且没有释放锁。所以,即便是该线程不再执行了,但是其它线程想执行,也必须具有锁,但锁已经被切换的线程拿走了。
Note:
一个线程,不申请锁,单纯的访问临界资源 -- 错误的编码方式(编码规则)
于是对于线程2来说,对其有意义的事:
- 线程1没有持有锁(什么都没做),此时我可以申请锁。
- 线程1释放锁(做完),此时我可以申请锁。
体现出:线程1持有锁期间,对于其他线程来说线程1的操作(要么有锁、要么没锁)就是原子性的。
#问:加了锁就是串行吗?
是的,执行临界区代码一定事串行的。不然如何保证的原子性?
#问:要访问临界资源,每一个线程都必须现申请锁,每一个线程都必须先看到同一把锁,并访问它,锁本身是不是就是一种共享资源?那锁由谁来保护?
所以,为了保证锁的安全,申请和释放锁,必须是原子性的。重点:锁是如何实现的?
锁的实现原理
在汇编的视角:
只有一条汇编语句,就认为该汇编语句的执行是原子的。于是,为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,以一条汇编的方式,将内存和CPU内寄存区数据进行交换。
在执行流的视角:
CPU内部的寄存器的本质:当前执行流的上下文,寄存器的空间是被所有的执行流共享的,但是寄存器的内容(上下文),是被每一个执行流私有的。
核心:是交换并非拷贝 ——> 交换 - 永远只有一个1。

前面的学习使得我们知道,之所以出现数据不一致问题是因为,一个线程将一个共享数据拿走了,但是没来的及改变共享数据的数值,就被切换成了新的线程来使用这个共享数据。 而在此,是交换并非拷贝,于是可以理解为,有锁即为:1,没有锁即为:0。于是,利用交换的原理,将共享数据的数值与线程的自身数值进行切换,(CUP中寄存器的数据,本质上就是属于该线程的上下文数据)。
lock:只有执行了核心,才会将锁(1),从共有数据交换为加锁线程的私有程序。私有数据没锁(0),交换为公共数据,并且是只有一行的。所以一定是交换了 / 没交换两种状态。于是可以保证加锁的线程,可以将锁带走,因为加锁后,锁就是该线程的上下文数据。
unlock:与lock同理,movb行执行就交换线程上下文数据与共享数据。所以保证了只要换,锁一定释放,没换一定还在改线程手上。
融汇贯通的理解:所以,因为锁是原子性的,利用一行汇编(交换),使得锁永远只有一个。并且对于拥有锁的线程来说,锁是该线程的上下文,是线程的私有资源,除非unlock时交换回内存(公共资源)。不然永远为一个线程私有。也就进一步的说明了线程在临界区中,被会切换,是没有任何影响。
可重入VS线程安全
- 可重入:针对于函数的说法,一个函数被多个执行流重复进入的现象,并且重复进入期间不会出现问题 —— 描述函数。
- 线程安全:在线程执行过程当中,可能因为一个或若干个线程,在执行的过程当中访问了某些不该访问的资源(全局变量或者静态变量、共享的资源),进而导致其他线程数据不一致问题、其他线程崩溃、进程终止 —— 描述线程。
常见的线程不安全的情况:
- 不保护共享变量的函数。
- 函数状态随着被调用,状态发生变化的函数。
- 返回指向静态变量指针的函数
- 调用线程不安全函数的函数。
常见的线程安全的情况:
- 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的。
- 类或者接口对于线程来说都是原子操作。
- 通过加锁的方式,局部上保持原子性。
- 多个线程之间的切换不会导致该接口的执行结果存在二义性。(函数不会有状态的变化)
常见不可重入的情况:
- 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的。
- 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构。
- 可重入函数体内使用了静态的数据结构。
常见可重入的情况:
- 不使用全局变量或静态变量。
- 不使用用malloc或者new开辟出的空间。
- 不调用不可重入函数。
- 不返回静态或全局数据,所有数据都有函数的调用者提供。
- 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据。
void func()
{int myerrno = errno;// ……errno = myerron;
}
保证调用的时候,调用之前与调用之后,全局数据的errno的数据没有变化。因为在一个线程执行的时候,另一个线程也执行,可能会将errno的值进行修改。
可重入与线程安全联系:
(两个有交集,但是是不同的概念)
- 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题。
- 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
- 可重入函数是线程安全函数的一种。
- 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
- 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生。死锁,因此是不可重入的。
Note:
线程安全:有对错之分,一定要写出线程安全的函数。(不)可重入:没有对错之分,是特点。
死锁
如:在实际用锁的时候,不一定只用了一把锁,有可能用了2把锁以上。就可能出现以下问题:
线程申请一把锁是原子的,但是,当申请完一把锁再去申请另一把锁的时候,就可能会出现问题。
当两个线程同时执行,线程A拿了锁1,线程B拿了锁2。二者就要继续向后申请,向后拿到各自的最后一把锁。
这个时候就出现了,两个线程申请互相锁的状态 —— 死锁。
一把锁也可能出现死锁:
#include <iostream> #include <cstdio> #include <cstdlib> #include <pthread.h> #include <unistd.h>// 加锁保护 pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;void *getTickets(void *args) {(void)args;pthread_mutex_lock(&mtx);// ……//pthread_mutex_lock(&mtx); 写错了,将解锁写成了加锁pthread_mutex_lock(&mtx);return nullptr; }int main() {srand((unsigned long)time(nullptr));pthread_t t1;pthread_create(&t1, nullptr, getTickets, (void *)"thread one");// 线程等待pthread_join(t1, nullptr);return 0; }
持有锁还将自己挂起,永远不可能被唤醒。
- 互斥条件:一个资源每次只能被一个执行流使用。
- 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺。
- 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系。
以故事的视角理解:
锁 == 情侣关系
死锁 == 冷战
- 互斥条件,即只能由一个女孩的存在于情侣关系中。
- 请求与保持条件,冷战,女孩不认错想男孩认错。
- 不剥夺条件,冷战,女孩不强制男孩认错。
- 循环等待条件,冷战,男孩同时不认错想女孩认错。
避免死锁:
- 破坏死锁的四个必要条件
- 加锁顺序一致
- 避免锁未释放的场景
- 资源一次性分配
互斥条件:
是一切锁的最根本的功能,而死锁是肯定使用了锁而导致的,所以是不怎么考虑的解决方式。因为有情侣关系才会有冷战,不可能说为了合好,从而分手吧?但也不得说避免冷战的出现,就不谈恋爱,是最有效的方式。
直观来说,不适用锁,也就是从根源解决死锁。
请求与保持条件:
pthread_mutex_lock—— 申请锁的时候,别的线程也在申请,如果申请锁不成功,会直接阻塞。
pthread_mutex_trylock —— 申请锁的时候,别的线程也在申请,如果申请锁不成功,会直接返回。
中的tryloc作用就是凸显于此,因为使用其申请锁的线程,发现锁被其他线程占用,并不会阻塞,而是立即返回:
- 在成功完成之后会返回零。
- 其他任何返回值都表示出现了错误。(EBUSY:由于 mutex 所指向的互斥锁已锁定,因此无法获取该互斥锁。)
将返回值EBUSY的次数进行连续记录,如果到到达一定数目,该线程将自身所有的锁解掉。
不剥夺条件:
线程尝试着去抢占(竞争)锁,依据一定的条件:我的优先级更高。强制的要求释放锁,我来获取锁。
循环等待条件:
通过编码的方式解决:堆每一把锁进行编号,要求每个线程申请锁时,如果是申请多把锁,每个线程都必须按照一定的顺序来申请。只能按照锁1,锁2,锁3……的申请,不允许交叉申请。
尽量将资源一次性分配,避免频繁加锁分配资源,导致锁太多而锁被申请数量多,导致易出现死锁的问题。
Linux线程同步
就如同我们前面所讲的抢票,多线程抢票的时候,可能会出现一段时间一直是一个线程抢票(因数很多),该线程因为优先级高,疯狂的申请锁、释放锁。其也没有任何问题,但是其是不合理:
- 频繁申请到资源 —— 导致别的线程申请不到。
- 太过于浪费进程自己的资源和临界区的资源。
频繁申请到资源:
此没有错,但是不合理的行为,导致:别的进程长时间的得不到资源,而导致别的进程饥饿的问题。
就如同食堂排队。有一个人总是由于某些合理的原因,一直处于队首,并且打完饭又立马回到队首继续打饭,甚至直到将饭打完。
所以:
如果只有互斥,就会导致所有的竞争情况为极强的随机性(看调度器),就有可能导致其他线程长时间得不到资源的情况。
太过于浪费进程自己的资源和临界区的资源:
一个进程申请到资源,首先需要做的是,不是访问资源,而是在申请锁之后,先检测资源是否就绪,如果就绪才会有后续操作。
就如同电影院买票,票经过线程抢购售卖,而前提是提供了票。于是忽,票抢完之后需要电影院方再提供票售卖,而此期间线程会持续的申请锁并检测(防止有新票上线),没有票的时候,所有线程还在申请锁、检测、释放锁,做无用功。
此期间线程在进行,临界区提供检测,从而浪费支援。
所以,我们需要引入同步:不是严格意义上的保证临界资源安全的方法,主要为了解决,访问临界资源合理性问题。
再以食堂打饭为例,但是这一次极端一些:
只有一个餐盘,并且只有这一个餐盘才允许打饭(多余要求没有)。
于是,有一个人早饭一过就开始等,下午饭时间一到,立马将餐盘一抢,开始打饭,但是这个人很奇怪,每次打几粒米,然后吃完,将餐盘拿到厨房。然后,厨房将餐盘刚刚洗完放回,这人又饿了,于是立马又拿起来继续跑去打饭(按照规矩这个人也没错,他离餐盘最近,他肯定先抢的到),甚至循环往复将饭打完了,于是后面的人就吃不到饭菜。
于是便有了线程同步,也就是管理员发现不对,就加了一个规矩:吃完的人不能立即,再拿餐盘。后面等的人不能乱等,必须按照前来时间排队。然后如果吃完放回餐盘,想再打饭,必须排在队尾。
意义:让访问线程访问临界资源,具有一定的顺序性。
条件变量
当我们申请临界资源前 -> 先要对临界资源进行是否存在的检测 -> 要做检测的本质:也是访问临界资源。因为:该线程不清楚资源是否被其他线程拿完,大部分资源都是我们申请到锁,但并不代表是有资源的。所以需要使用临界资源,也就代表了需要要先对临界资源进行检测。
结论:对临界资源的检测,也一定是需要在加锁和解锁之间。
常规方式要检测条件就绪,就注定了线程必须频繁的申请和释放锁,使得线程处于一直在申请和释放锁,这样会使得线程一直处于很忙的状态,还不会创造什么价值。
#问:有没有什么办法让线程检测资源处于不就绪的时候?
1. 不要让线程在频繁检测 —— 让其处于等待。
2. 当条件就绪的时候,通知对应的线程,让他来进行资源的申请和访问。
- 当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
例如一个线程访问队列时,发现队列为空,它只能等待,直到其它线程将一个节点添加到队列中,这种情况就需要用到条件变量。
- 同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步。
- 竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。
系统调用
初始化与destory
1. 对于局部的条件变量初始化利用pthread_cond_init。(也可以用于全局)
#include <pthread.h> // 局部条件变量初始化 int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
参数:cond:要初始化的条件变量attr:局部变量的属性 —— 可以不管,直接设为nullptr。返回值:
- 成功,返回0。
- 失败,返回错误码。
与锁的初始化相同,需要:pthread_cond_destroy。
#include <pthread.h> // 局部条件变量销毁 int pthread_cond_destroy(pthread_cond_t *cond);
参数:
mutex:传入我们定义并初始化的条件变量。
返回值:
- 成功,返回0。
- 失败,返回错误码。
2. 对条件变量,只要是我们定义的是静态或全局,我们可以直接使用PTHREAD_COND_INITIALIZER(宏),进行初始化。
等待条件满足
现在我们需要的不是加锁和解锁,我们需要的是在对应临界区中,资源不就绪的时候,不要频繁的检测,等待即可:pthread_cond_wait。
pthread_cond_wait
#include <pthread.h> // 等待临界资源是否就绪 int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
参数:
cond:要在这个条件变量上等待。
mutex:互斥量(锁)
- mutex就是对应着为了保护数据安全的一把锁,也就是说除了要保证能够同步,还要保持能够互斥。所以参数mutex与参数cond可以说是有渊源的 —— 使用条件变量,必须有一把锁。(为什么?后面具体代码详细解释)
返回值:
pthread_cond_timedwait
(用的少,此文不细致讲解)
#include <pthread.h> // 等待临界资源是否就绪 int pthread_cond_timedwait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
参数:
前面两个参数与pthread_cond_wait一样。restrict abstime:传入的时间
- 也就是说等的时候,是只能一直的等?不是的,我们可以使用pthread_cond_timedwait,设置一个时间restrict abstime,时间到了线程会自动醒来。
返回值:
唤醒等待
条件满足,对应的线程就要收到通知,也就是唤醒它。所以条件满足的时候,需要向等待的线程发通知:pthread_cond_broadcast、pthread_cond_signal。
pthread_cond_broadcast
#include <pthread.h> // 可能不止一个线程在等资源就绪 —— 将对应等待的线程全部唤醒。 int pthread_cond_broadcast(pthread_cond_t *cond);
参数:
cond:当前在指定条件变量cond上阻塞所有线程。
返回值:
- 成功,返回0。
- 失败,返回错误码。
pthread_cond_signal
#include <pthread.h> // 唤醒指定的一个线程。 int pthread_cond_signal(pthread_cond_t *cond);
参数:
cond:当前在指定条件变量cond上阻塞中的一个线程。
返回值:
- 成功,返回0。
- 失败,返回错误码。
代码使用
(下列代码从基础向上增加代码,以做讲解)
#:先写一个简易的多线程程序
(由于局部的锁与条件变量更难写,所以此处演示局部的)
#include <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>#define NUMS 3
typedef void (*func_t)(const std::string &);class ThreadDate
{
public:ThreadDate(const std::string name, func_t func) : name_(name), func_(func){}public:std::string name_;func_t func_;
};void func1(const std::string &name)
{while (true){std::cout << name << " running -- 下载" << std::endl;usleep(10000);}
}void func2(const std::string &name)
{while (true){std::cout << name << " running -- 播放" << std::endl;usleep(20000);}
}void func3(const std::string &name)
{while (true){std::cout << name << " running -- 扫码用户信息" << std::endl;usleep(30000);}
}void *Entry(void *args)
{ThreadDate *td = (ThreadDate *)args; // td在每一个线程自己私有的栈空间中保存td->func_(td->name_);delete td;return nullptr;
}int main()
{pthread_t tids[NUMS];func_t funcs[NUMS] = {func1, func2, func3};for (int i = 0; i < NUMS; ++i){std::string name = "Thread ";name += std::to_string(i + 1);ThreadDate *td = new ThreadDate(name, funcs[i]);pthread_create(tids + i, nullptr, Entry, (void *)td);}for (int i = 0; i < NUMS; ++i){pthread_join(tids[i], nullptr);std::cout << "Thread " << i + 1 << "-quit-" << std::endl;}return 0;
}
#:利用互斥锁和条件变量控制线程,并采用wait等待和signal唤醒
现在想控制线程,想让不同的线程去运行,想唤醒哪个就唤醒哪个:
控制线程:
- 定义一把互斥锁。
- 定义一个条件变量。
随后使用wait与signal:
#include <pthread.h>
// 等待临界资源是否就绪
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
#include <pthread.h>
// 唤醒指定的一个线程。
int pthread_cond_signal(pthread_cond_t *cond);
唤醒的时候,为什么参数传递的是一个条件变量?因为所有程序都在对应的条件变量下等,并且我们根本不关心是哪一个线程,当对应的新线程在对应的条件变量下等,就认为其进行了排队等待。只是当条件变量由 "无效" 变为 "有效" 的时候,从其中挑一个线程,让其去执行特定的任务。(这也就是条件变量的意义)
#include <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>#define NUMS 3
typedef void (*func_t)(const std::string& name, pthread_mutex_t* pmtx, pthread_cond_t* cond);class ThreadDate
{
public:ThreadDate(const std::string name, func_t func, pthread_mutex_t* pmtx, pthread_cond_t* pcond): name_(name), func_(func), pmtx_(pmtx), pcond_(pcond){}public:std::string name_;func_t func_;pthread_mutex_t* pmtx_;pthread_cond_t* pcond_;
};void func1(const std::string &name, pthread_mutex_t* pmtx, pthread_cond_t* pcond)
{while (true){pthread_cond_wait(pcond, pmtx);std::cout << name << " running -- 下载" << std::endl;}
}void func2(const std::string &name, pthread_mutex_t* pmtx, pthread_cond_t* pcond)
{while (true){pthread_cond_wait(pcond, pmtx);std::cout << name << " running -- 播放" << std::endl;}
}void func3(const std::string &name, pthread_mutex_t* pmtx, pthread_cond_t* pcond)
{while (true){pthread_cond_wait(pcond, pmtx);std::cout << name << " running -- 扫码用户信息" << std::endl;}
}void *Entry(void *args)
{ThreadDate *td = (ThreadDate *)args; // td在每一个线程自己私有的栈空间中保存td->func_(td->name_, td->pmtx_, td->pcond_);delete td;return nullptr;
}int main()
{pthread_mutex_t mtx;pthread_cond_t cond;pthread_mutex_init(&mtx, nullptr);pthread_cond_init(&cond, nullptr);pthread_t tids[NUMS];func_t funcs[NUMS] = {func1, func2, func3};for (int i = 0; i < NUMS; ++i){std::string name = "Thread ";name += std::to_string(i + 1);ThreadDate *td = new ThreadDate(name, funcs[i], &mtx, &cond);pthread_create(tids + i, nullptr, Entry, (void *)td);}sleep(5);// 所有线程停止5秒// 休眠的时候主线程没有继续向后执行// 新线程一直在执行waitwhile(true){std::cout << "resume thread run code ...." << std::endl;pthread_cond_signal(&cond); // 主线程唤醒一个线程,唤醒哪一个关心,只关心将条件变量由无效设为有效 —— 表征条件就绪sleep(1);}for (int i = 0; i < NUMS; ++i){pthread_join(tids[i], nullptr);std::cout << "Thread " << i + 1 << "-quit-" << std::endl;}pthread_mutex_destroy(&mtx);pthread_cond_destroy(&cond);return 0;
}
我们会发现其是具有一定的顺序的,就是因为这些线程,在条件未满足的时候,所有的线程都会在该条件变量下排队等待。所以主线程对应使用signal一个一个的唤醒线程,就会使得线程的运行是有序的。
将唤醒方式替换:
#include <pthread.h>
// 可能不止一个线程在等资源就绪 —— 将对应等待的线程全部唤醒。
int pthread_cond_broadcast(pthread_cond_t *cond);
写一个执行5次调用(唤醒)线程:
#include <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>#define NUMS 3
typedef void (*func_t)(const std::string& name, pthread_mutex_t* pmtx, pthread_cond_t* cond);
volatile bool quit = false;class ThreadDate
{
public:ThreadDate(const std::string name, func_t func, pthread_mutex_t* pmtx, pthread_cond_t* pcond): name_(name), func_(func), pmtx_(pmtx), pcond_(pcond){}public:std::string name_;func_t func_;pthread_mutex_t* pmtx_;pthread_cond_t* pcond_;
};void func1(const std::string &name, pthread_mutex_t* pmtx, pthread_cond_t* pcond)
{while (!quit){pthread_cond_wait(pcond, pmtx);std::cout << name << " running -- 下载" << std::endl;}
}void func2(const std::string &name, pthread_mutex_t* pmtx, pthread_cond_t* pcond)
{while (!quit){pthread_cond_wait(pcond, pmtx);std::cout << name << " running -- 播放" << std::endl;}
}void func3(const std::string &name, pthread_mutex_t* pmtx, pthread_cond_t* pcond)
{while (!quit){pthread_cond_wait(pcond, pmtx);std::cout << name << " running -- 扫码用户信息" << std::endl;}
}void *Entry(void *args)
{ThreadDate *td = (ThreadDate *)args; // td在每一个线程自己私有的栈空间中保存td->func_(td->name_, td->pmtx_, td->pcond_);delete td;return nullptr;
}int main()
{pthread_mutex_t mtx;pthread_cond_t cond;pthread_mutex_init(&mtx, nullptr);pthread_cond_init(&cond, nullptr);pthread_t tids[NUMS];func_t funcs[NUMS] = {func1, func2, func3};for (int i = 0; i < NUMS; ++i){std::string name = "Thread ";name += std::to_string(i + 1);ThreadDate *td = new ThreadDate(name, funcs[i], &mtx, &cond);pthread_create(tids + i, nullptr, Entry, (void *)td);}sleep(5);// 所有线程停止5秒// 休眠的时候主线程没有继续向后执行// 新线程一直在执行waitint cnt = 5;while(cnt){std::cout << "resume thread run code ...." << cnt-- << std::endl;//pthread_cond_signal(&cond); // 主线程唤醒一个线程,唤醒哪一个不用关心,只关心将条件变量由无效设为有效 —— 表征条件就绪pthread_cond_broadcast(&cond);sleep(1);}quit = true;for (int i = 0; i < NUMS; ++i){pthread_join(tids[i], nullptr);std::cout << "Thread " << i + 1 << "-quit-" << std::endl;}pthread_mutex_destroy(&mtx);pthread_cond_destroy(&cond);return 0;
}
此时,会发现处于并没线程运行,且并未结束的卡死状态。
线程中wait一定要在加锁和解锁之间进行wait。因为我们wait的原因就是,检测到临界资源条件不就绪,所以线程的做法就是等,需要检测临界资源不就绪,就一定需要在临界区内。
供货商只负责生产,
供货商不研究用户需要什么,只用关心超市想要什么
超市只关心将商品给消费者
提高效率:解耦合
让不同的角色进行逻辑解耦,以此通过超市这样的角色,来进行提高效率
相关文章:

【Linux】-- 多线程安全
目录 进程互斥 计算 -> 时序问题 加锁保护 pthread_mutex_lock pthread_mutex_unlock 使用init与destory pthread_mutex_init phtread_mutex_destory 锁的实现原理 图 可重入VS线程安全 死锁 Linux线程同步 条件变量 系统调用 进程互斥 进程线程间的互斥相关…...

Unity Avatar Camera Controller 第一、第三人称相机控制
文章目录简介Variables实现Target PositionTarget RotationOthers简介 本文介绍如何实现用于Avatar角色的相机控制脚本,支持第一人称、第三人称以及两种模式之间的切换,工具已上传至SKFramework框架的Package Manager中: Variables Avatar&…...

SRE中 的SLO,SLI等知识 归纳
SLA Service Level Agreement 服务质量/水平协议SLO Service Level Objective 服务质量/水平目标SLI Services Level Indicator 服务质量/水平指标下面用人、事、物的逻辑进行阐释。人和事用从上到下,从左到右的顺序。客户 - 每 1 个客户在使用产品服务时&…...

MS9123是一款单芯片USB投屏器,内部集成了USB2 0控制器和数据收发模块、视频DAC和音视频处理模块,MS9123可以通过USB接口显示或者扩展PC、
MS9123是一款单芯片USB投屏器,内部集成了USB2.0控制器和数据收发模块、视频DAC和音视频处理模块,MS9123可以通过USB接口显示或者扩展PC、智能手机、平板电脑的显示信息到更大尺寸的显示设备上,支持CVBS、S-Video视频接口。 主要功能特征 C…...

针孔成像模型零基础入门(三)
2020年爆火的Nerf(神经辐射场)横空出世,据说只要用手机拍照,然后喂给模型,就可以生成3D模型了,我试过了,确有此事! 那我们有想过,为什么可以从二维的图片里面获取物体三…...

你真的了解环形队列吗?(学习数据结构必须掌握的模型)
目录 0.前言 1. 什么是环形队列 2. 如何使用数组结构 / 链表结构 对环形队列封装 3. 代码手撕环形队列各个接口 3.1 代表封装一个环形队列 3.2 环形队列的初始化 3.3 环形队列的插入 3.4环形队列的删除 3.5环形队列的判空 3.6环形队列的判满 3.7环形队列的队头 3.8环…...

《痞子衡嵌入式半月刊》 第 72 期
痞子衡嵌入式半月刊: 第 72 期 这里分享嵌入式领域有用有趣的项目/工具以及一些热点新闻,农历年分二十四节气,希望在每个交节之日准时发布一期。 本期刊是开源项目(GitHub: JayHeng/pzh-mcu-bi-weekly),欢迎提交 issue,…...

对redis之键值型数据库的理解
键值数据库,首先就要考虑里面可以存什么样的数据,对数据可以做什么样的操作,也就是数据模型和操作接口。它们看似简单,实际上却是我们理解 Redis 经常被用于缓存、秒杀、分布式锁等场景的重要基础。理解了数据模型,你就…...

Linux内核中的软中断、tasklet和工作队列
软中断、tasklet和工作队列并不是Linux内核中一直存在的机制,而是由更早版本的内核中的“下半部”(bottom half)演变而来。下半部的机制实际上包括五种,但2.6版本的内核中,下半部和任务队列的函数都消失了,…...

【Java】Spring Boot 2 集成 nacos
官方文档:https://nacos.io/zh-cn/docs/quick-start-spring-boot.html pom 本次Springboot版本 2.2.6.RELEASE,nacos-config 版本 0.2.7,nacos-discovery版本 0.2.7 parent <parent><groupId>org.springframework.boot</gr…...

JavaSE学习笔记day14
二、Set Set集合是Collection集合的子接口,该集合中不能有重复元素!! Set集合提供的方法签名,与父接口Collection的方法完全一致!! 即没有关于下标操作的方法 Set接口,它有两个常用的子实现类HashSet,TreeSet 三、HashSet HashSet实现了Set接口,底层是hash表(实际上底层是HashM…...

LLVM高级架构介绍
LLVM 为什么要开一个LLVM的新坑呢? 我从智能穿戴转行到芯片软件行业,从事编译器开发,不过是AI编译器。不过基本的传统编译器还是绕不过去啊,所以开始学习LLVM,后面开始学习TVM,MLIR。 LLVM GitHub地址 L…...
全网最经典函数题型【详解】——C语言
文章目录1. 写一个函数可以判断一个数是不是素数。2. 写一个函数判断一年是不是闰年。3. 写一个函数,实现一个整形有序数组的二分查找。4. 写一个函数,每调用一次这个函数,就会将 num 的值增加1。5. 写一个函数,打印乘法口诀表。6…...

emqx桥接配置+常见问题解决+jmeter压测emqx
一,桥接资源配置及规则配置 Emqx桥接配置流程 1,配置资源并测试连接通过 规则引擎——>资源——>新建——>选择MQTT Bridge——>填写参数测试连接 参数描述详见3.1资源配置 2,配置规则 2.1根据实际业务选择合适sql 规则引擎…...

improve-1
类型及检测方式 1. JS内置类型 JavaScript 的数据类型有下图所示 其中,前 7 种类型为基础类型,最后 1 种(Object)为引用类型,也是你需要重点关注的,因为它在日常工作中是使用得最频繁,也是需要…...

华为OD机试用Python实现 -【云短信平台优惠活动】(2023-Q1 新题)
华为OD机试题 华为OD机试300题大纲云短信平台优惠活动题目描述输入描述输出描述示例一输入输出说明示例二输入输出说明Python 代码实现代码编写思路华为OD机试300题大纲 参加华为od机试,一定要注意不要完全背诵代码,需要理解之后模仿写出,通过率才会高。 华为 OD 清单查看…...

Facebook广告投放运营中的关键成功因素是什么?
在当今数字化的时代,广告投放已经成为了各种企业获取市场份额和增加品牌曝光的重要手段之一。Facebook作为全球最大的社交媒体平台之一,其广告投放运营的成功,将直接影响企业的品牌推广和市场营销效果。本文将探讨Facebook广告投放运营中的关…...

2023年1月综合预订类APP用户洞察——旅游市场复苏明显,三年需求春节集中释放
2023年1月,随着国家对新型冠状病毒感染实施“乙类乙管”,不再对入境人员和货物等采取检疫传染病管理措施,并且取消入境后全员核酸检测和集中隔离,横亘在旅游者与旅游目的地之间的隔阂从此彻底消失。2023年1月恰逢春节假期…...

基于stm32计算器设计
这里写目录标题 完整de代码可q我获取1 系统功能设计2 系统硬件系统分析设计2.1 STM32单片机核心电路设计2.2 LCD1602液晶显示模块电路设计2.3 4X4矩阵键盘模块设计3 STM32单片机系统软件设计3.1 编程语言选择3.2 Keil程序开发环境3.3 FlyMcu程序烧录软件介绍3.4 CH340串口程序烧…...
基于SpringCloud的可靠消息最终一致性02:项目骨架代码(上)
在上一节中咱们已经把分布式事务问题交代了一遍,包括两大定理、五大解决方案和一个成熟的开源框架,而咱们最终的目标是用Spring Cloud实现一个实际创业项目的可靠消息最终一致性的分布式事务方案。 先交代一下项目背景。 前几年,社会上慢慢兴起一种称为C2C同城快递的业务,也…...
synchronized 学习
学习源: https://www.bilibili.com/video/BV1aJ411V763?spm_id_from333.788.videopod.episodes&vd_source32e1c41a9370911ab06d12fbc36c4ebc 1.应用场景 不超卖,也要考虑性能问题(场景) 2.常见面试问题: sync出…...
ubuntu搭建nfs服务centos挂载访问
在Ubuntu上设置NFS服务器 在Ubuntu上,你可以使用apt包管理器来安装NFS服务器。打开终端并运行: sudo apt update sudo apt install nfs-kernel-server创建共享目录 创建一个目录用于共享,例如/shared: sudo mkdir /shared sud…...
逻辑回归:给不确定性划界的分类大师
想象你是一名医生。面对患者的检查报告(肿瘤大小、血液指标),你需要做出一个**决定性判断**:恶性还是良性?这种“非黑即白”的抉择,正是**逻辑回归(Logistic Regression)** 的战场&a…...
镜像里切换为普通用户
如果你登录远程虚拟机默认就是 root 用户,但你不希望用 root 权限运行 ns-3(这是对的,ns3 工具会拒绝 root),你可以按以下方法创建一个 非 root 用户账号 并切换到它运行 ns-3。 一次性解决方案:创建非 roo…...
.Net Framework 4/C# 关键字(非常用,持续更新...)
一、is 关键字 is 关键字用于检查对象是否于给定类型兼容,如果兼容将返回 true,如果不兼容则返回 false,在进行类型转换前,可以先使用 is 关键字判断对象是否与指定类型兼容,如果兼容才进行转换,这样的转换是安全的。 例如有:首先创建一个字符串对象,然后将字符串对象隐…...

Maven 概述、安装、配置、仓库、私服详解
目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...

Linux --进程控制
本文从以下五个方面来初步认识进程控制: 目录 进程创建 进程终止 进程等待 进程替换 模拟实现一个微型shell 进程创建 在Linux系统中我们可以在一个进程使用系统调用fork()来创建子进程,创建出来的进程就是子进程,原来的进程为父进程。…...

GruntJS-前端自动化任务运行器从入门到实战
Grunt 完全指南:从入门到实战 一、Grunt 是什么? Grunt是一个基于 Node.js 的前端自动化任务运行器,主要用于自动化执行项目开发中重复性高的任务,例如文件压缩、代码编译、语法检查、单元测试、文件合并等。通过配置简洁的任务…...
MySQL 部分重点知识篇
一、数据库对象 1. 主键 定义 :主键是用于唯一标识表中每一行记录的字段或字段组合。它具有唯一性和非空性特点。 作用 :确保数据的完整性,便于数据的查询和管理。 示例 :在学生信息表中,学号可以作为主键ÿ…...

day36-多路IO复用
一、基本概念 (服务器多客户端模型) 定义:单线程或单进程同时监测若干个文件描述符是否可以执行IO操作的能力 作用:应用程序通常需要处理来自多条事件流中的事件,比如我现在用的电脑,需要同时处理键盘鼠标…...