Linux之线程同步与互斥
目录
一、线程互斥
1.1、进程线程间的互斥相关背景概念
1.2、互斥量mutex
1.2.1、互斥量的接⼝
1.3、互斥量实现原理探究
1.4、互斥量的封装
二、线程同步
2.1、条件变量
2.2、同步概念与竞态条件
2.3、条件变量函数
2.4、⽣产者消费者模型
2.4.1、为何要使⽤⽣产者消费者模型
2.4.2、⽣产者消费者模型优点
2.5、基于BlockingQueue的⽣产者消费者模型
2.5.1、BlockingQueue
2.5.2、C++ queue模拟阻塞队列的⽣产消费模型
2.6、为什么 pthread_cond_wait 需要互斥量?
2.7、条件变量使⽤规范
2.8、条件变量的封装
2.9、POSIX信号量
2.9.1、基于环形队列的⽣产消费模型
三、线程池
3.1、⽇志与策略模式
3.2、线程池设计
3.3、线程安全的单例模式
3.3.1、什么是单例模式
3.3.2、饿汉实现⽅式和懒汉实现⽅式的理解
3.3.3、饿汉⽅式实现单例模式
3.3.4、懒汉⽅式实现单例模式
3.3.6、懒汉⽅式实现单例模式(线程安全版本)
3.4、单例式线程池
四、线程安全和重入问题
五、常见锁概念
5.1、死锁
5.2、死锁四个必要条件
5.3、避免死锁
六、STL,智能指针和线程安全
6.1、STL中的容器是否是线程安全的?
6.2、智能指针是否是线程安全的?
一、线程互斥
1.1、进程线程间的互斥相关背景概念
- 临界资源:多线程执⾏流共享的资源就叫做临界资源。
- 临界区:每个线程内部,访问临界资源的代码,就叫做临界区。
- 互斥:任何时刻,互斥保证有且只有⼀个执⾏流进⼊临界区,访问临界资源,通常对临界资源起保护作⽤。
- 原⼦性(后⾯讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成, 要么未完成。
1.2、互斥量mutex
- ⼤部分情况,线程使⽤的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量 归属单个线程,其他线程⽆法获得这种变量。
- 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完 成线程之间的交互。
- 多个线程并发的操作共享变量,会带来⼀些问题。
售票系统:
ticket.cc:
#include <iostream>
#include <vector>
#include "Thread.hpp"using namespace ThreadModule;#define NUM 4int ticketnum = 10000;void Ticket()
{while(true){if(ticketnum > 0){usleep(1000);printf("get a new ticket,id: %d\n", ticketnum--);}else{break;}}
}int main()
{//构建线程对象std::vector<Thread> threads;for(int i = 0; i < NUM; i++){threads.emplace_back(Ticket);}//启动线程for(auto &thread : threads){thread.Start();}//等待线程for(auto &thread : threads){thread.Join();}return 0;
}
pthread.hpp:
#ifndef _THREAD_HPP__
#define _THREAD_HPP__#include <iostream>
#include <string>
#include <pthread.h>
#include <functional>
#include <sys/types.h>
#include <unistd.h>namespace ThreadModule
{using func_t = std::function<void()>;static int number = 1;enum class TSTATUS{NEW,RUNNING,STOP};class Thread{private:// 成员方法!static void *Routine(void *args){Thread *t = static_cast<Thread *>(args);t->_status = TSTATUS::RUNNING;t->_func();return nullptr;}void EnableDetach() { _joinable = false; }public:Thread(func_t func) : _func(func), _status(TSTATUS::NEW), _joinable(true){_name = "Thread-" + std::to_string(number++);_pid = getpid();}bool Start(){if (_status != TSTATUS::RUNNING){int n = ::pthread_create(&_tid, nullptr, Routine, this); // TODOif (n != 0)return false;return true;}return false;}bool Stop(){if (_status == TSTATUS::RUNNING){int n = ::pthread_cancel(_tid);if (n != 0)return false;_status = TSTATUS::STOP;return true;}return false;}bool Join(){if (_joinable){int n = ::pthread_join(_tid, nullptr);if (n != 0)return false;_status = TSTATUS::STOP;return true;}return false;}void Detach(){EnableDetach();pthread_detach(_tid);}bool IsJoinable() { return _joinable; }std::string Name() {return _name;}~Thread(){}private:std::string _name;pthread_t _tid;pid_t _pid;bool _joinable; // 是否是分离的,默认不是func_t _func;TSTATUS _status;};
}#endif
Makefile:
bin=ticket
cc=g++
src=$(wildcard *.cc)
obj=$(src:.cc=.o)$(bin):$(obj)$(cc) -o $@ $^ -lpthread
%.o:%.cc$(cc) -c $< -std=c++17.PHONY:clean
clean:rm -f $(bin) $(obj).PHONY:test
test:echo $(src)echo $(obj)
效果:
为什么可能⽆法获得正确结果?
- --ticket 操作本⾝就不是⼀个原⼦操作。
- if 语句不是原子操作。
- 线程间的调度切换执行。
- 线程或者进程什么时候会切换?
- 时间片耗尽
- 更高优先级的进程要调度
- 通过sleep,然后从内核返回用户时,会进行时间片是否到达的检测,进而导致切换
- 线程或者进程什么时候会切换?
- usleep 这个模拟漫⻓业务的过程,在这个漫⻓的业务过程中,可能有很多个线程会进⼊该代码段。
具体解释:假设当前票数只剩一个,出现两个或以上线程执行以下操作,将1(剩余票数)放到寄存器后,进行 if 的条件判断之前,线程切换了,当这些线程再次被调度时,恢复进程上下文信息,因为切换前寄存器中是1,恢复后寄存器的内容就还是1,所以恢复执行后的这些线程都会认为还有一张票,if 判断为真,都会执行抢票工作,但实际上票数可以已经没了或者只剩一张,但多个线程执行了抢票代码,所以票数出现负数问题。
取出ticket--部分的汇编代码
objdump -d a.out > test.objdump
152 40064b: 8b 05 e3 04 20 00 mov 0x2004e3(%rip),%eax #
600b34 <ticket>
153 400651: 83 e8 01 sub $0x1,%eax
154 400654: 89 05 da 04 20 00 mov %eax,0x2004da(%rip) #
600b34 <ticket>
--(减减) 操作并不是原⼦操作,⽽是对应三条汇编指令:
- load :将共享变量ticket从内存加载到寄存器中。
- update :更新寄存器⾥⾯的值,执⾏-1操作。
- store :将新值,从寄存器写回共享变量ticket的内存地址。
要解决以上问题,需要做到三点:
- 代码必须要有互斥⾏为:当代码进⼊临界区执⾏时,不允许其他线程进⼊该临界区。
- 如果多个线程同时要求执⾏临界区的代码,并且临界区没有线程在执⾏,那么只能允许⼀个线程进⼊该临界区。
- 如果线程不在临界区中执⾏,那么该线程不能阻⽌其他线程进⼊临界区。
要做到这三点,本质上就是需要⼀把锁。Linux上提供的这把锁叫互斥量。
1.2.1、互斥量的接⼝
初始化互斥量有两种⽅法:
⽅法1,静态分配:(静态和全局的锁一般使用这种方式)
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
⽅法2,动态分配:(局部的锁一般使用这种方式)
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const
pthread_mutexattr_t *restrict attr);
参数:
mutex:要初始化的互斥量
attr:NULL
销毁互斥量需要注意:
- 使⽤ PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁。
- 不要销毁⼀个已经加锁的互斥量。
- 已经销毁的互斥量,要确保后⾯不会有线程再尝试加锁。
int pthread_mutex_destroy(pthread_mutex_t *mutex);
互斥量加锁和解锁:
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号
调⽤ pthread_ lock 时,可能会遇到以下情况:
- 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功。
- 发起函数调⽤时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_lock调⽤会陷⼊阻塞(执⾏流被挂起),等待互斥量解锁。
注意:
- 锁本身也只是一种共享资源,不过加锁和解锁操作是原子的。
- 如果申请锁的时候,锁已经被别人拿走了,其他线程要进行阻塞等待。
- 线程在加锁情况下访问临界区代码的时候,是可以被切换走的。只不过锁还没有释放,其他线程仍无法进入临界区。
改进的售票系统:
全局锁:(makefile,Thread.hpp同上,只需要修改ticket.cc)ticket.cc如下:
#include <iostream>
#include <vector>
#include "Thread.hpp"using namespace ThreadModule;#define NUM 4//创建锁 加锁和解锁操作是原子的
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;int ticketnum = 10000;//共享资源void Ticket()
{while(true){//加锁pthread_mutex_lock(&lock);if(ticketnum > 0){usleep(1000);printf("get a new ticket,id: %d\n", ticketnum--);//解锁pthread_mutex_unlock(&lock);}else{pthread_mutex_unlock(&lock); //解锁break;}}
}int main()
{//构建线程对象std::vector<Thread> threads;for(int i = 0; i < NUM; i++){threads.emplace_back(Ticket);}//启动线程for(auto &thread : threads){thread.Start();}//等待线程for(auto &thread : threads){thread.Join();}return 0;
}
局部锁:(makefile同上)
Thread.hpp:(增加了模版,使其可以为线程执行的方法传递参数)
#ifndef _THREAD_HPP__
#define _THREAD_HPP__#include <iostream>
#include <string>
#include <pthread.h>
#include <functional>
#include <sys/types.h>
#include <unistd.h>// v2
namespace ThreadModule
{static int number = 1;enum class TSTATUS{NEW,RUNNING,STOP};template <typename T>class Thread{using func_t = std::function<void(T&)>;private:// 成员方法!static void *Routine(void *args){Thread<T> *t = static_cast<Thread<T> *>(args);t->_status = TSTATUS::RUNNING;t->_func(t->_data);return nullptr;}void EnableDetach() { _joinable = false; }public:Thread(func_t func, T &data) : _func(func), _data(data), _status(TSTATUS::NEW), _joinable(true){_name = "Thread-" + std::to_string(number++);_pid = getpid();}bool Start(){if (_status != TSTATUS::RUNNING){int n = ::pthread_create(&_tid, nullptr, Routine, this); // TODOif (n != 0)return false;return true;}return false;}bool Stop(){if (_status == TSTATUS::RUNNING){int n = ::pthread_cancel(_tid);if (n != 0)return false;_status = TSTATUS::STOP;return true;}return false;}bool Join(){if (_joinable){int n = ::pthread_join(_tid, nullptr);if (n != 0)return false;_status = TSTATUS::STOP;return true;}return false;}void Detach(){EnableDetach();pthread_detach(_tid);}bool IsJoinable() { return _joinable; }std::string Name() { return _name; }~Thread(){}private:std::string _name;pthread_t _tid;pid_t _pid;bool _joinable; // 是否是分离的,默认不是func_t _func;TSTATUS _status;T &_data;};
}
#endif
ticket.cc:
#include <iostream>
#include <vector>
#include "Thread.hpp"using namespace ThreadModule;#define NUM 4int ticketnum = 10000;//共享资源class ThreadData
{
public:std::string name;pthread_mutex_t *lock_ptr;
};void Ticket(ThreadData &td)
{while(true){//加锁pthread_mutex_lock(td.lock_ptr);if(ticketnum > 0){usleep(1000);printf("get a new ticket,who get it: %s,id: %d\n", td.name.c_str(), ticketnum--);//解锁pthread_mutex_unlock(td.lock_ptr);}else{pthread_mutex_unlock(td.lock_ptr);break;}}
}int main()
{pthread_mutex_t lock; //创建锁pthread_mutex_init(&lock, nullptr); //初始化锁//构建线程对象std::vector<Thread<ThreadData>> threads;for(int i = 0; i < NUM; i++){ThreadData *td = new ThreadData();td->lock_ptr = &lock;threads.emplace_back(Ticket, *td);td->name = threads.back().Name();}//启动线程for(auto &thread : threads){thread.Start();}//等待线程for(auto &thread : threads){thread.Join();}pthread_mutex_destroy(&lock); //销毁锁return 0;
}
1.3、互斥量实现原理探究
- 经过上⾯的例⼦,⼤家已经意识到单纯的 i++ 或者 ++i 都不是原⼦的,有可能会有数据⼀致性问题。
- 为了实现互斥锁操作,⼤多数体系结构都提供了swap或exchange指令,该指令的作⽤是把寄存器和 内存单元的数据相交换,由于只有⼀条指令,保证了原⼦性,即使是多处理器平台,访问内存的总线周 期也有先后,⼀个处理器上的交换指令执⾏时另⼀个处理器的交换指令只能等待总线周期。现在 我们把lock和unlock的伪代码改⼀下。
1.4、互斥量的封装
Mutex.hpp:
#pragma once#include <iostream>
#include <pthread.h>namespace LockMoudle
{class Mutex{public:Mutex(const Mutex&) = delete;const Mutex& operator=(const Mutex&) = delete;Mutex(){int n = pthread_mutex_init(&lock, nullptr);//下面可以对n进行判断,判断是否初始化成功//这里就不判断了,直接强转一下,防止报warning(void)n;}~Mutex(){int n = pthread_mutex_destroy(&lock);(void)n; //和上面同理}void Lock(){int n = pthread_mutex_lock(&lock);(void)n;}void Unlock(){int n = pthread_mutex_unlock(&lock);(void)n;}private:pthread_mutex_t lock;};//使用时可以创建局部的LockGuard对象//这样创建时,通过构造方法自动加锁//因为是局部的,出作用域自动销毁,通过析构函数释放锁//这样我们就只需要创建LockGuard对象,申请和释放锁都不需要我们管//这种代码风格叫做RAII风格class LockGuard{public:LockGuard(Mutex &mtx) :_mtx(mtx){_mtx.Lock();}~LockGuard(){_mtx.Unlock();}private:Mutex &_mtx;};
}
二、线程同步
2.1、条件变量
- 当⼀个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
- 例如⼀个线程访问队列时,发现队列为空,它只能等待,只到其它线程将⼀个节点添加到队列 中。这种情况就需要⽤到条件变量。
2.2、同步概念与竞态条件
- 同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从⽽有效避免饥饿问题,叫做同步。
- 竞态条件:因为时序问题,⽽导致程序异常,我们称之为竞态条件。在线程场景下,这种问题也不难理解。
2.3、条件变量函数
初始化:
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
参数:
cond:要初始化的条件变量
attr:NULL
销毁:
int pthread_cond_destroy(pthread_cond_t *cond)
等待条件满⾜:
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
参数:
cond:要在这个条件变量上等待
mutex:互斥量,后⾯详细解释
注意:使用该方法,线程等待时会自动释放拥有的锁,被唤醒时会自动申请锁。
唤醒等待:
唤醒某条件变量下所有等待线程
int pthread_cond_broadcast(pthread_cond_t *cond);
唤醒某条件变量下等待的第一个线程
int pthread_cond_signal(pthread_cond_t *cond);
简单案例:
- 我们先使⽤PTHREAD_COND/MUTEX_INITIALIZER进⾏测试,对其他细节暂不追究。
#include <iostream>
#include <cstdio>
#include <string>
#include <pthread.h>
#include <unistd.h>pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;void *active(void *args)
{std::string name = static_cast<const char *>(args);while(true){pthread_mutex_lock(&mutex);//等待时会先自动释放锁再去等待//被唤醒时会自动拥有锁pthread_cond_wait(&cond, &mutex);printf("%s is active\n", name.c_str());pthread_mutex_unlock(&mutex);}
}int main()
{pthread_t tid1, tid2, tid3;pthread_create(&tid1, nullptr, active, (void *)"thread-1");pthread_create(&tid2, nullptr, active, (void *)"thread-1");pthread_create(&tid3, nullptr, active, (void *)"thread-1");sleep(1);printf("Main thread ctrl begin...\n");while(true){printf("main wakeup thread...\n");pthread_cond_signal(&cond);// pthread_cond_broadcast(&cond);sleep(1);}pthread_join(tid1, nullptr);pthread_join(tid2, nullptr);pthread_join(tid3, nullptr);return 0;
}
2.4、⽣产者消费者模型
2.4.1、为何要使⽤⽣产者消费者模型
⽣产者消费者模式就是通过⼀个容器来解决⽣产者和消费者的强耦合问题。⽣产者和消费者彼此之间不直接通讯,⽽通过阻塞队列来进⾏通讯,所以⽣产者⽣产完数据之后不⽤等待消费者处理,直接扔给阻塞队列,消费者不找⽣产者要数据,⽽是直接从阻塞队列⾥取,阻塞队列就相当于⼀个缓冲区, 平衡了⽣产者和消费者的处理能⼒。这个阻塞队列就是⽤来给⽣产者和消费者解耦的。
2.4.2、⽣产者消费者模型优点
- 解耦
- ⽀持并发
- ⽀持忙闲不均
2.5、基于BlockingQueue的⽣产者消费者模型
2.5.1、BlockingQueue
在多线程编程中阻塞队列(Blocking Queue)是⼀种常⽤于实现⽣产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放⼊了元素;当队列满时,往队列⾥存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)
2.5.2、C++ queue模拟阻塞队列的⽣产消费模型
示例代码:(单生产者,单消费者)
BlockQueue:
#pragma once#include <iostream>
#include<queue>
#include <pthread.h>namespace BlockQueueModule
{static const int gcap = 10;template<typename T>class BlockQueue{private:bool IsFull(){ return _q.size() == _cap; }bool IsEmpty(){ return _q.empty(); }public:BlockQueue(int cap = gcap):_cap(cap),_cwait_num(0),_pwait_num(0){pthread_mutex_init(&_mutex, nullptr);pthread_cond_init(&_productor_cond, nullptr);pthread_cond_init(&_consumer_cond, nullptr);}void Equeue(const T& in) //生产者{pthread_mutex_lock(&_mutex);//生产数据是有条件的,需要先判断是否为满,满了就不能放数据了while(IsFull()) //用while对条件进行判断,防止伪唤醒,这样唤醒后会多判断一次是否满足条件,如果不满足就继续等待{std::cout << "生产者进入等待..." << std::endl; //打印信息,方便调试_pwait_num++;pthread_cond_wait(&_productor_cond, &_mutex); //wait时会先释放锁_pwait_num--;//线程真正被唤醒需要重新申请并持有锁(他会在临界区内醒来)std::cout << "生产者被唤醒..." << std::endl;} //队列没满或者线程被唤醒了_q.push(in); //生产//当前一定有数据//判断是否有消费者等待,如果有就唤醒一个if(_cwait_num){std::cout << "叫醒消费者" << std::endl;pthread_cond_signal(&_consumer_cond);}//解锁pthread_mutex_unlock(&_mutex);}void Pop(T *out) //消费者{pthread_mutex_lock(&_mutex);while(IsEmpty()){std::cout << "消费者进入等待..." << std::endl;_cwait_num++;pthread_cond_wait(&_consumer_cond, &_mutex);_cwait_num--;std::cout << "消费者被唤醒..." << std::endl;}*out = _q.front();_q.pop();if(_pwait_num){std::cout << "叫醒生产者..." << std::endl;pthread_cond_signal(&_productor_cond);}pthread_mutex_unlock(&_mutex);}~BlockQueue(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_productor_cond);pthread_cond_destroy(&_consumer_cond);}private:std::queue<T> _q; //保存数据的容器,临界资源int _cap; //容器的最大容量pthread_mutex_t _mutex; //互斥锁pthread_cond_t _productor_cond; //生产者条件变量pthread_cond_t _consumer_cond; //消费者条件变量int _cwait_num; //消费者等待数量int _pwait_num; //生产者等待数量};
}
Main.cc:
#include "BlockQueue.hpp"
#include <pthread.h>
#include <unistd.h>using namespace BlockQueueModule;void *Consumer(void *args)
{BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(args);while(true){int data;// 1. 从bq拿到数据bq->Pop(&data);// 2.做处理printf("Consumer, 消费了一个数据: %d\n", data);}
}void *Productor(void *args)
{BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(args);int data = 10;while (true){sleep(2);// 2. 生产到bq中bq->Equeue(data);printf("producter 生产了一个数据: %d\n", data);data++;}
}int main()
{// 交易场所,不仅仅可以用来进行传递数据BlockQueue<int> *bq = new BlockQueue<int>(5); // 共享资源 -> 临界资源// 单生产,单消费pthread_t c1, p1; //,c2, , p2, p3;pthread_create(&c1, nullptr, Consumer, bq);// pthread_create(&c2, nullptr, Consumer, bq);pthread_create(&p1, nullptr, Productor, bq);// pthread_create(&p2, nullptr, Productor, bq);// pthread_create(&p3, nullptr, Productor, bq);pthread_join(c1, nullptr);// pthread_join(c2, nullptr);pthread_join(p1, nullptr);// pthread_join(p2, nullptr);// pthread_join(p3, nullptr);delete bq;return 0;
}
2.6、为什么 pthread_cond_wait 需要互斥量?
- 条件等待是线程间同步的⼀种⼿段,如果只有⼀个线程,条件不满⾜,⼀直等下去都不会满⾜, 所以必须要有⼀个线程通过某些操作,改变共享变量,使原先不满⾜的条件变得满⾜,并且友好 的通知等待在条件变量上的线程。
- 条件不会⽆缘⽆故的突然变得满⾜了,必然会牵扯到共享数据的变化。所以⼀定要⽤互斥锁来保 护。没有互斥锁就⽆法安全的获取和修改共享数据。
按照上⾯的说法,我们设计出如下的代码:先上锁,发现条件不满⾜,解锁,然后等待在条件变 量上不就⾏了,如下代码:
// 错误的设计
pthread_mutex_lock(&mutex);
while (condition_is_false)
{
pthread_mutex_unlock(&mutex);
pthread_cond_wait(&cond,&mutex);
pthread_mutex_lock(&mutex);
//等待这里不需要自己释放锁和申请锁,它会自动释放和申请
}
pthread_mutex_unlock(&mutex);
- 由于解锁和等待不是原⼦操作。调⽤解锁之后, pthread_cond_wait 之前,如果已经有其他线程获取到互斥量,摒弃条件满⾜,发送了信号,那么 pthread_cond_wait 将错过这个信号,可能会导致线程永远阻塞在这个 pthread_cond_wait 。所以解锁和等待必须是⼀个原⼦操作。
- int pthread_cond_wait(pthread_cond_ t *cond,pthread_mutex_ t * mutex); 进⼊该函数后,会去看条件量等于0不?等于,就把互斥量变成1,直到cond_wait返 回,把条件量改成1,把互斥量恢复成原样。
2.7、条件变量使⽤规范
等待条件代码:
pthread_mutex_lock(&mutex);
while (条件为假)
pthread_cond_wait(cond, mutex);
修改条件
pthread_mutex_unlock(&mutex);
给条件发送信号代码:
pthread_mutex_lock(&mutex);
设置条件为真
pthread_cond_signal(cond);
pthread_mutex_unlock(&mutex);
2.8、条件变量的封装
Cond.hpp:
#pragma once#include <iostream>
#include <pthread.h>
#include "Mutex.hpp"namespace CondModule
{using namespace LockModule;class Cond{public:Cond(){int n = ::pthread_cond_init(&_cond, nullptr);(void)n;}void Wait(Mutex &mutex) {int n = ::pthread_cond_wait(&_cond, mutex.LockPtr());(void)n;}void Notify(){int n = ::pthread_cond_signal(&_cond);(void)n;}void NotifyAll(){int n = ::pthread_cond_broadcast(&_cond);(void)n;}~Cond(){int n = ::pthread_cond_destroy(&_cond);}private:pthread_cond_t _cond;};
}
Mutex.hpp:
#pragma once
#include <iostream>
#include <pthread.h>namespace LockModule
{class Mutex{public:Mutex(const Mutex&) = delete;const Mutex& operator = (const Mutex&) = delete;Mutex(){int n = ::pthread_mutex_init(&_lock, nullptr);(void)n;}~Mutex(){int n = ::pthread_mutex_destroy(&_lock);(void)n;}void Lock(){int n = ::pthread_mutex_lock(&_lock);(void)n;}pthread_mutex_t *LockPtr(){return &_lock;}void Unlock(){int n = ::pthread_mutex_unlock(&_lock);(void)n;}private:pthread_mutex_t _lock;};class LockGuard{public:LockGuard(Mutex &mtx):_mtx(mtx){_mtx.Lock();}~LockGuard(){_mtx.Unlock();}private:Mutex &_mtx;};
}
注意:为了让条件变量更具有通⽤性,建议封装的时候,不要在Cond类内部引⽤对应的封装互斥 量,要不然后⾯组合的时候,会因为代码耦合的问题难以初始化,因为⼀般⽽⾔Mutex和 Cond基本是⼀起创建的。
2.9、POSIX信号量
POSIX信号量和SystemV信号量作⽤相同,都是⽤于同步操作,达到⽆冲突的访问共享资源⽬的。但 POSIX可以⽤于线程间同步。
初始化信号量:
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
pshared:0表⽰线程间共享,⾮零表⽰进程间共享
value:信号量初始值
销毁信号量:
int sem_destroy(sem_t *sem);
等待信号量:
功能:等待信号量,会将信号量的值减1
int sem_wait(sem_t *sem); //P()
发布信号量:
功能:发布信号量,表⽰资源使⽤完毕,可以归还资源了。将信号量值加1。
int sem_post(sem_t *sem); //V()
2.9.1、基于环形队列的⽣产消费模型
- 环形队列采⽤数组模拟,⽤模运算来模拟环状特性
- 环形结构起始状态和结束状态都是⼀样的,不好判断为空或者为满,所以可以通过加计数器或者标记位来判断满或者空。另外也可以预留⼀个空的位置,作为满的状态
但是我们现在有信号量这个计数器,就很简单的进⾏多线程间的同步过程。示例代码:
Main.cc:
#include "RingBuffer.hpp"
#include <pthread.h>
#include <unistd.h>
#include <ctime>using namespace RingBufferModule;void *Consumer(void *args)
{RingBuffer<int> *ring_buffer = static_cast<RingBuffer<int> *>(args);while(true){sleep(1);// sleep(1);// 1. 消费数据int data;ring_buffer->Pop(&data);// 2. 处理:花时间std::cout << "消费了一个数据: " << data << std::endl;}
}void *Productor(void *args)
{RingBuffer<int> *ring_buffer = static_cast<RingBuffer<int> *>(args);int data = 0;while (true){// 1. 获取数据:花时间// sleep(1);// 2. 生产数据ring_buffer->Equeue(data);std::cout << "生产了一个数据: " << data << std::endl;data++;}
}int main()
{RingBuffer<int> *ring_buffer = new RingBuffer<int>(5); // 共享资源 -> 临界资源// 单生产,单消费pthread_t c1, p1, c2,c3,p2;pthread_create(&c1, nullptr, Consumer, ring_buffer);pthread_create(&c2, nullptr, Consumer, ring_buffer);pthread_create(&c3, nullptr, Consumer, ring_buffer);pthread_create(&p1, nullptr, Productor, ring_buffer);pthread_create(&p2, nullptr, Productor, ring_buffer);pthread_join(c1, nullptr);pthread_join(c2, nullptr);pthread_join(c3, nullptr);pthread_join(p1, nullptr);pthread_join(p2, nullptr);delete ring_buffer;return 0;
}
RingBuffer.hpp:
#pragma once#include <iostream>
#include <vector>
#include <pthread.h>#include "Sem.hpp"
#include "Mutex.hpp"namespace RingBufferModule
{using namespace SemModule;using namespace LockModule;template<typename T>class RingBuffer{public:RingBuffer(int cap):_ring(cap),_cap(cap),_p_step(0),_c_step(0),_datasem(0),_spacesem(cap){}//生产者void Equeue(const T& in){ _spacesem.P();{LockGuard(_p_lock);_ring[_p_step] = in;_p_step++;_p_setp %= _cap;}_datasem.V();}//消费者void Pop(T *out){_datasem.P();{LockGuard(_c_lock);*out = _ring[_c_step];_c_step++;_c_step %= _cap;}_spacesem.V();}~RingBuffer(){}private:std::vector<T> _ring; //用数组实现环形队列 临界资源int _cap; //总容量int _p_step; //生产者位置int _c_step; //消费者位置Sem _datasem; //数据信号量Sem _spacesem; //空间信号量Mutex _p_lock;Mutex _c_lock;};
}
Mutex.hpp:
#pragma once
#include <iostream>
#include <pthread.h>namespace LockModule
{class Mutex{public:Mutex(const Mutex&) = delete;const Mutex& operator = (const Mutex&) = delete;Mutex(){int n = ::pthread_mutex_init(&_lock, nullptr);(void)n;}~Mutex(){int n = ::pthread_mutex_destroy(&_lock);(void)n;}void Lock(){int n = ::pthread_mutex_lock(&_lock);(void)n;}pthread_mutex_t *LockPtr(){return &_lock;}void Unlock(){int n = ::pthread_mutex_unlock(&_lock);(void)n;}private:pthread_mutex_t _lock;};class LockGuard{public:LockGuard(Mutex &mtx):_mtx(mtx){_mtx.Lock();}~LockGuard(){_mtx.Unlock();}private:Mutex &_mtx;};
}
Sem.hpp:
#pragma once#include <semaphore.h>namespace SemModule
{int defaultsemval = 1;class Sem{public:Sem(int value = defaultsemval):_init_value(value){int n = ::sem_init(&_sem, 0, _init_value);(void)n;}void P(){int n = ::sem_wait(&_sem);(void)n;}void V(){int n = ::sem_post(&_sem);(void)n;}~Sem(){int n = ::sem_destroy(&_sem);(void)n;}private:sem_t _sem;int _init_value;};
}
三、线程池
3.1、⽇志与策略模式
什么是设计模式
IT⾏业这么⽕,涌⼊的⼈很多。俗话说林⼦⼤了啥⻦都有。⼤佬和菜鸡们两极分化的越来越严重。为了让菜鸡们不太拖⼤佬的后腿,于是⼤佬们针对⼀些经典的常⻅的场景,给定了⼀些对应的解决⽅案,这个就是设计模式。
⽇志认识
计算机中的⽇志是记录系统和软件运⾏中发⽣事件的⽂件,主要作⽤是监控运⾏状态、记录异常信 息,帮助快速定位问题并⽀持程序员进⾏问题修复。它是系统维护、故障排查和安全管理的重要⼯ 具。
⽇志格式以下⼏个指标是必须得有的
- 时间戳
- ⽇志等级
- ⽇志内容
以下⼏个指标是可选的
- ⽂件名⾏号
- 进程,线程相关id信息等
⽇志有现成的解决⽅案,如:spdlog、glog、Boost.Log、Log4cxx等等,我们依旧采⽤⾃定义⽇志的⽅式。 这⾥我们采⽤设计模式-策略模式来进⾏⽇志的设计。我们想要的⽇志格式如下:
[可读性很好的时间] [⽇志等级] [进程pid] [打印对应⽇志的⽂件名] [⾏号] - 消息内容, ⽀持可变参数
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [16] - hello world
[2024-08-04 12:27:03] [WARNING] [202938] [main.cc] [23] - hello world
示例代码:(注意:Log.hpp中使用的Mutex和上面的自己封装的Mutex.hpp是同一份代码,这里就不重复展示了)
Log.hpp:
#include <fstream>
#include <sstream>
#include <memory>
#include <filesystem> //C++17
#include <unistd.h>
#include <time.h>
#include "Mutex.hpp"namespace LogModule
{using namespace LockModule;// 获取当前系统的时间std::string CurrentTime(){time_t time_stamp = ::time(nullptr); // 获取时间戳struct tm curr; // 用作输出型参数localtime_r(&time_stamp, &curr);char buffer[1024];snprintf(buffer, sizeof(buffer), "%4d-%02d-%02d %02d:%02d:%02d",curr.tm_year + 1900,curr.tm_mon + 1,curr.tm_mday,curr.tm_hour,curr.tm_min,curr.tm_sec);return buffer;}// 日志文件的默认路径和文件名const std::string defaultlogpath = "./log/";const std::string defaultlogname = "log.txt";// 日志等级enum class LogLevel{DEBUG = 1,INFO,WARNING,ERROR,FATAL};std::string Level2String(LogLevel level){switch (level){case LogLevel::DEBUG:return "DEBUG";case LogLevel::INFO:return "INFO";case LogLevel::WARNING:return "WARNING";case LogLevel::ERROR:return "ERROR";case LogLevel::FATAL:return "FATAL";default:return "None";}}//刷新策略class LogStrategy{public:virtual ~LogStrategy() = default;virtual void SyncLog(const std::string &message) = 0;};//控制台策略class ConsoleLogStrategy : public LogStrategy{public:ConsoleLogStrategy(){}~ConsoleLogStrategy(){}void SyncLog(const std::string &message){LockGuard lockguard(_lock);std::cout << message << std::endl;}private:Mutex _lock;};//文件级策略class FileLogStrategy : public LogStrategy{public:FileLogStrategy(const std::string &logpath = defaultlogpath, const std::string &logname = defaultlogname):_logpath(logpath),_logname(logname){//确认_logpath是存在的LockGuard lockguard(_lock);if(std::filesystem::exists(_logpath)){return ;}try{std::filesystem::create_directories(_logpath);}catch(std::filesystem::filesystem_error &e){std::cerr << e.what() << "\n";}}~FileLogStrategy(){}void SyncLog(const std::string &message){LockGuard lockguard(_lock);std::string log = _logpath + _logname; //默认是 ./log/log.txtstd::ofstream out(log, std::ios::app); //日志写入,一定是追加if(!out.is_open()){return;}out << message << "\n";out.close();}private:std::string _logpath;std::string _logname;Mutex _lock;};class Logger{public:Logger(){//默认采用ConsoleLogStrategy策略_strategy = std::make_shared<ConsoleLogStrategy>();}void EnableConsoleLog(){_strategy = std::make_shared<ConsoleLogStrategy>();}void EnableFileLog(){_strategy = std::make_shared<FileLogStrategy>();}~Logger(){}// 一条完整的信息: [2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [16] + 日志的可变部分(<< "hello world" << 3.14 << a << b;)class LogMessage{public:LogMessage(LogLevel level, const std::string &filename, int line, Logger &logger):_currtime(CurrentTime()),_level(level),_pid(getpid()),_filename(filename),_line(line),_logger(logger){std::stringstream ssbuffer;ssbuffer << "[" << _currtime << "] "<< "[" << Level2String(_level) << "] "<< "[" << _pid << "] "<< "[" << _filename << "] "<< "[" << _line << "] - ";_loginfo = ssbuffer.str();}template<typename T>LogMessage &operator<<(const T &info){std::stringstream ss;ss << info;_loginfo += ss.str();return *this;}~LogMessage(){if (_logger._strategy){_logger._strategy->SyncLog(_loginfo);}}private:std::string _currtime; // 当前日志时间LogLevel _level; // 日志等级pid_t _pid; // 进程pidstd::string _filename; // 源文件名称int _line; // 日志所在行号Logger &_logger; // 负责根据不同的策略进行刷新std::string _loginfo; // 一条完整的日志记录};//这里就是需要返回拷贝的临时对象LogMessage operator()(LogLevel level, const std::string &filename, int line){return LogMessage(level, filename, line, *this);}private:std::shared_ptr<LogStrategy> _strategy; //日志刷新的策略方案};Logger logger;#define LOG(Level) logger(Level, __FILE__, __LINE__)
#define ENABLE_CONSOLE_LOG() logger.EnableConsoleLog()
#define ENABLE_FILE_LOG() logger.EnableFileLog()
}
Main.cc:
#include "Log.hpp"using namespace LogModule;int main()
{ENABLE_FILE_LOG();LOG(LogLevel::DEBUG) << "hello file";LOG(LogLevel::DEBUG) << "hello file";LOG(LogLevel::DEBUG) << "hello file";LOG(LogLevel::DEBUG) << "hello file";ENABLE_CONSOLE_LOG();LOG(LogLevel::DEBUG) << "hello world";LOG(LogLevel::DEBUG) << "hello world";LOG(LogLevel::DEBUG) << "hello world";LOG(LogLevel::DEBUG) << "hello world";return 0;
}
3.2、线程池设计
线程池:
⼀种线程使⽤模式。线程过多会带来调度开销,进⽽影响缓存局部性和整体性能。⽽线程池维护着多 个线程,等待着监督管理者分配可并发执⾏的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利⽤,还能防⽌过分调度。可⽤线程数量应该取决于可⽤的并发处理器、处理器内核、内存、⽹络sockets等的数量。
线程池的应用场景:
需要⼤量的线程来完成任务,且完成任务的时间⽐较短。⽐如WEB服务器完成⽹⻚请求这样的任 务,使⽤线程池技术是⾮常合适的。因为单个任务⼩,⽽任务数量巨⼤,你可以想象⼀个热⻔⽹站 的点击次数。但对于⻓时间的任务,⽐如⼀个Telnet连接请求,线程池的优点就不明显了。因为 Telnet会话时间⽐线程的创建时间⼤多了。
对性能要求苛刻的应⽤,⽐如要求服务器迅速响应客⼾请求。
接受突发性的⼤量请求,但不⾄于使服务器因此产⽣⼤量线程的应⽤。突发性⼤量客⼾请求,在没 有线程池情况下,将产⽣⼤量线程,虽然理论上⼤部分操作系统线程数⽬最⼤值不是问题,短时间 内产⽣⼤量线程可能使内存到达极限,出现错误。
线程池的种类:
- a. 创建固定数量线程池,循环从任务队列中获取任务对象,获取到任务对象后,执⾏任务对象中的任务接⼝。
- b. 浮动线程池,其他同上。
此处,我们选择固定线程个数的线程池。
示例代码:(注意:ThreadPool.hpp中使用的锁,条件变量,日志均来自于上面的示例中封装的代码,即Mutex.hpp,Cond.hpp,Log.hpp,使用的线程来自于线程概念与控制文章中的线程封装,即Thread.hpp,这里就不在重复展示了)
Task.hpp:
#pragma once#include <iostream>
#include <string>
#include <functional>
#include "Log.hpp"using namespace LogMudule;using task_t = std::function<void(std::string name)>;void Push(std::string name)
{LOG(LogLevel::DEBUG) << "我是一个推送数据到服务器的一个任务, 我正在被执行" << "[" << name << "]";
}
ThreadPool.hpp:
#pragma once#include <iostream>
#include <string>
#include <queue>
#include <vector>
#include <memory>
#include "Log.hpp"
#include "Mutex.hpp"
#include "Cond.hpp"
#include "Thread.hpp"namespace ThreadPoolModule
{using namespace LogMudule;using namespace ThreadModule;using namespace LockModule;using namespace CondModule;//用来测试的线程方法void DefaultTest(){while(true){LOG(LogLevel::DEBUG)<<"我是一个测试方法";sleep(1);}}using thread_t = std::shared_ptr<Thread>;const static int defaultnum = 5;template<typename T>class ThreadPool{private:bool IsEmpty(){ return _taskq.empty(); }void HandlerTask(std::string name){LOG(LogLevel::INFO)<<"线程"<<name<<",进入HandlerTask的逻辑";while(true){//拿任务T t;{LockGuard lockguard(_lock);while(IsEmpty() && _isrunning){_wait_num++;_cond.Wait(_lock);_wait_num--;}//任务队列为空,并且线程池处于退出状态if(IsEmpty() && !_isrunning)break;t = _taskq.front();_taskq.pop();}//处理任务t(name); //假设规定未来所有任务都必须提供重载()方法}LOG(LogLevel::INFO)<<"线程:"<<name<<"退出";}public:ThreadPool(int num = defaultnum):_num(num),_wait_num(0),_isrunning(false){for(int i = 0; i < _num; i++){_threads.push_back(std::make_shared<Thread>(std::bind(&ThreadPool::HandlerTask, this, std::placeholders::_1)));LOG(LogLevel::INFO) << "构建线程" << _threads.back()->Name() << "对象 ... 成功";}}void Equeue(T &&in){LockGuard lockguard(_lock);if(!_isrunning) return;_taskq.push(std::move(in));if(_wait_num > 0)_cond.Notify();}void Start(){if(_isrunning) return;_isrunning = true;for(auto &thread_ptr : _threads){LOG(LogLevel::INFO)<<"启动线程"<<thread_ptr->Name()<<"...成功";thread_ptr->Start();}}void Wait(){for(auto &thread_ptr : _threads){thread_ptr->Join();LOG(LogLevel::INFO) << "回收线程" << thread_ptr->Name() << " ... 成功";}}void Stop(){LockGuard lockguard(_lock);if(_isrunning){//不能在入任务了_isrunning = false; //不工作//让线程自己退出//将休眠线程都唤醒,确保历史人任务被处理完了if(_wait_num > 0){_cond.NotifyAll();}}}~ThreadPool(){}private:std::vector<thread_t> _threads; //管理所有线程的容器int _num; //线程总数量int _wait_num; //休眠状态的线程数量std::queue<T> _taskq; //存放任务的队列,临界资源Mutex _lock;Cond _cond;bool _isrunning;};
}
ThreadPool.cc:
#include "ThreadPool.hpp"
#include "Task.hpp"
#include <memory>using namespace ThreadPoolModule;int main()
{ENABLE_CONSOLE_LOG();// ENABLE_FILE_LOG();std::unique_ptr<ThreadPool<task_t>> tp = std::make_unique<ThreadPool<task_t>>();tp->Start();int cnt = 10;// char c;while (true){// std::cin >> c;tp->Equeue(Push);cnt--;sleep(1);}tp->Stop();sleep(3);tp->Wait();return 0;
}
3.3、线程安全的单例模式
3.3.1、什么是单例模式
某些类,只应该具有⼀个对象(实例),就称之为单例。
在很多服务器开发场景中,经常需要让服务器加载很多的数据(上百G)到内存中。此时往往要⽤⼀个单例的类来管理这些数据。
3.3.2、饿汉实现⽅式和懒汉实现⽅式的理解
[洗碗的例⼦]
吃完饭, ⽴刻洗碗, 这种就是饿汉⽅式. 因为下⼀顿吃的时候可以⽴刻拿着碗就能吃饭。
吃完饭, 先把碗放下, 然后下⼀顿饭⽤到这个碗了再洗碗, 就是懒汉⽅式。
懒汉⽅式最核⼼的思想是"延时加载"。从⽽能够优化服务器的启动速度。
3.3.3、饿汉⽅式实现单例模式
template<typename T>
class Singleton
{
static T data;
public:
static T* GetInstance()
{
return &data;
}
};
3.3.4、懒汉⽅式实现单例模式
template<typename T>
class Singleton
{
static T* inst;
public:
static T* GetInstance()
{
if (inst == NULL)
{
inst = new T();
}
return inst;
}
};
注意:这里存在⼀个严重的问题,线程不安全。第⼀次调⽤ GetInstance 的时候,如果两个线程同时调⽤,可能会创建出两份 T 对象的实例。但是后续再次调⽤,就没有问题了。
3.3.6、懒汉⽅式实现单例模式(线程安全版本)
template<typename T>
class Singleton
{
volatile static T* inst; // 需要设置 volatile 关键字, 否则可能被编译器优化.
static std::mutex lock;
public:
static T* GetInstance()
{
if (inst == NULL)
{ // 双重判定空指针, 降低锁冲突的概率, 提⾼性能.
lock.lock(); // 使⽤互斥锁, 保证多线程情况下也只调⽤⼀次 new.
if (inst == NULL)
{
inst = new T();
}
lock.unlock();
}
return inst;
}
};
注意事项:
- 加锁解锁的位置
- 双重 if 判定,避免不必要的锁竞争
- volatile关键字防⽌过度优化
3.4、单例式线程池
注意:Mutex.hpp,Cond.hpp,Log.hpp,Thread.hpp,Task.hpp同上
ThreadPool.hpp:
#pragma once#include <iostream>
#include <string>
#include <queue>
#include <vector>
#include <memory>
#include "Log.hpp"
#include "Mutex.hpp"
#include "Cond.hpp"
#include "Thread.hpp"namespace ThreadPoolModule
{using namespace LogMudule;using namespace ThreadModule;using namespace LockModule;using namespace CondModule;// 用来做测试的线程方法void DefaultTest(){while (true){LOG(LogLevel::DEBUG) << "我是一个测试方法";sleep(1);}}using thread_t = std::shared_ptr<Thread>;const static int defaultnum = 5;template <typename T>class ThreadPool{private:bool IsEmpty() { return _taskq.empty(); }void HandlerTask(std::string name){LOG(LogLevel::INFO) << "线程: " << name << ", 进入HandlerTask的逻辑";while (true){// 1. 拿任务T t;{LockGuard lockguard(_lock);while (IsEmpty() && _isrunning){_wait_num++;_cond.Wait(_lock);_wait_num--;}// 2. 任务队列为空 && 线程池退出了if (IsEmpty() && !_isrunning)break;t = _taskq.front();_taskq.pop();}// 2. 处理任务t(name); // 规定,未来所有的任务处理,全部都是必须提供()方法!}LOG(LogLevel::INFO) << "线程: " << name << " 退出";}ThreadPool(const ThreadPool<T> &) = delete;ThreadPool<T> &operator=(const ThreadPool<T> &) = delete;ThreadPool(int num = defaultnum) : _num(num), _wait_num(0), _isrunning(false){for (int i = 0; i < _num; i++){_threads.push_back(std::make_shared<Thread>(std::bind(&ThreadPool::HandlerTask, this, std::placeholders::_1)));LOG(LogLevel::INFO) << "构建线程" << _threads.back()->Name() << "对象 ... 成功";}}public:static ThreadPool<T> *getInstance(){if(instance == NULL){LockGuard lockguard(mutex);if(instance == NULL){LOG(LogLevel::INFO) << "单例首次被执行,需要加载对象...";instance = new ThreadPool<T>();}}return instance;}void Equeue(T &&in){LockGuard lockguard(_lock);if (!_isrunning)return;_taskq.push(std::move(in));if (_wait_num > 0)_cond.Notify();}void Start(){if (_isrunning)return;_isrunning = true; // bug fixfor (auto &thread_ptr : _threads){LOG(LogLevel::INFO) << "启动线程" << thread_ptr->Name() << " ... 成功";thread_ptr->Start();}}void Wait(){for (auto &thread_ptr : _threads){thread_ptr->Join();LOG(LogLevel::INFO) << "回收线程" << thread_ptr->Name() << " ... 成功";}}void Stop(){LockGuard lockguard(_lock);if (_isrunning){// 3. 不能在入任务了_isrunning = false; // 不工作// 1. 让线程自己退出(要唤醒) && // 2. 历史的任务被处理完了if (_wait_num > 0)_cond.NotifyAll();}}~ThreadPool(){}private:std::vector<thread_t> _threads;int _num;int _wait_num;std::queue<T> _taskq; // 临界资源Mutex _lock;Cond _cond;bool _isrunning;static ThreadPool<T> *instance;static Mutex mutex; //只用来保护单例};template<typename T>ThreadPool<T> *ThreadPool<T>::instance = NULL;template<typename T>Mutex ThreadPool<T>::mutex; //只用来保护单例
}
ThreadPool.cc:
#include "ThreadPool.hpp"
#include "Task.hpp"
#include <memory>using namespace ThreadPoolModule;int main()
{ENABLE_CONSOLE_LOG();ThreadPool<task_t>::getInstance()->Start();char c;int cnt = 5;while (cnt){// std::cin >> c;ThreadPool<task_t>::getInstance()->Equeue(Push);cnt--;sleep(1);}ThreadPool<task_t>::getInstance()->Stop();ThreadPool<task_t>::getInstance()->Wait();return 0;
}
四、线程安全和重入问题
概念:
- 线程安全:就是多个线程在访问共享资源时,能够正确地执⾏,不会相互⼲扰或破坏彼此的执⾏结 果。⼀般⽽⾔,多个线程并发同⼀段只有局部变量的代码时,不会出现不同的结果。但是对全局变量或者静态变量进⾏操作,并且没有锁保护的情况下,容易出现该问题。
- 重⼊:同⼀个函数被不同的执⾏流调⽤,当前⼀个流程还没有执⾏完,就有其他的执⾏流再次进⼊, 我们称之为重⼊。⼀个函数在重⼊的情况下,运⾏结果不会出现任何不同或者任何问题,则该函数被 称为可重⼊函数,否则,是不可重⼊函数。
重⼊其实可以分为两种情况:
- 多线程重⼊函数
- 信号导致⼀个执⾏流重复进⼊函数
常⻅的线程不安全的情况:
- 不保护共享变量的函数
- 函数状态随着被调⽤,状态发⽣变化的函数
- 返回指向静态变量指针的函数
- 调⽤线程不安全函数的函数
常⻅的线程安全的情况:
- 每个线程对全局变量或者静态变量只有读取 的权限,⽽没有写⼊的权限,⼀般来说这些 线程是安全的。
- 类或者接⼝对于线程来说都是原⼦操作。
- 多个线程之间的切换不会导致该接⼝的执⾏结果存在⼆义性。
常⻅不可重⼊的情况:
- 调⽤了malloc/free函数,因为malloc函数 是⽤全局链表来管理堆的。
- 调⽤了标准I/O库函数,标准I/O库的很多实 现都以不可重⼊的⽅式使⽤全局数据结构。
- 可重⼊函数体内使⽤了静态的数据结构。
常⻅可重⼊的情况:
- 不使⽤全局变量或静态变量
- 不使⽤ malloc或者new开辟出的空间
- 不调⽤不可重⼊函数
- 不返回静态或全局数据,所有数据都有函数 的调⽤者提供
- 使⽤本地数据,或者通过制作全局数据的本 地拷⻉来保护全局数据
可重⼊与线程安全联系:
- 函数是可重⼊的,那就是线程安全的(其实知道这⼀句话就够了)。
- 函数是不可重⼊的,那就不能由多个线程使⽤,有可能引发线程安全问题
- 如果⼀个函数中有全局变量,那么这个函数既不是线程安全也不是可重⼊的。
可重⼊与线程安全区别:
- 可重⼊函数是线程安全函数的⼀种
- 线程安全不⼀定是可重⼊的,⽽可重⼊函数则⼀定是线程安全的。
- 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重⼊函数若锁还未释放则会产⽣死锁,因此是不可重⼊的。
注意:如果不考虑信号导致⼀个执⾏流重复进⼊函数这种重⼊情况,线程安全和重⼊在安全⻆度不做区分。但是线程安全侧重说明线程访问公共资源的安全情况,表现的是并发线程的特点。可重⼊描述的是⼀个函数是否能被重复进⼊,表⽰的是函数的特点。
五、常见锁概念
5.1、死锁
- 死锁是指在⼀组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站⽤不会释放的资源⽽处于的⼀种永久等待状态。
为了⽅便表述,假设现在线程A,线程B必须同时持有锁1和锁2,才能进⾏后续资源的访问。
申请⼀把锁是原⼦的,但是申请两把锁就不⼀定了。如图:
造成的结果是:
5.2、死锁四个必要条件
互斥条件:⼀个资源每次只能被⼀个执⾏流使⽤
请求与保持条件:⼀个执⾏流因请求资源⽽阻塞时,对已获得的资源保持不放
不剥夺条件:⼀个执⾏流已获得的资源,在末使⽤完之前,不能强⾏剥夺
循环等待条件:若⼲执⾏流之间形成⼀种头尾相接的循环等待资源的关系
5.3、避免死锁
破坏死锁的四个必要条件
- 破坏循环等待条件问题:资源⼀次性分配,使⽤超时机制、加锁顺序⼀致
- 避免锁未释放的场景
六、STL,智能指针和线程安全
6.1、STL中的容器是否是线程安全的?
不是
原因:STL的设计初衷是将性能挖掘到极致,⽽⼀旦涉及到加锁保证线程安全,会对性能造成巨⼤的影响。⽽且对于不同的容器,加锁⽅式的不同,性能可能也不同(例如hash表的锁表和锁桶)。因此STL默认不是线程安全。如果需要在多线程环境下使⽤,往往需要调⽤者⾃⾏保证线程安全。
6.2、智能指针是否是线程安全的?
对于unique_ptr,由于只是在当前代码块范围内⽣效,因此不涉及线程安全问题。 对于shared_ptr,多个对象需要共⽤⼀个引⽤计数变量,所以会存在线程安全问题。但是标准库实现的时候考虑到了这个问题,基于原⼦操作(CAS)的⽅式保证shared_ptr能够⾼效,原⼦的操作引⽤计数。
相关文章:
Linux之线程同步与互斥
目录 一、线程互斥 1.1、进程线程间的互斥相关背景概念 1.2、互斥量mutex 1.2.1、互斥量的接⼝ 1.3、互斥量实现原理探究 1.4、互斥量的封装 二、线程同步 2.1、条件变量 2.2、同步概念与竞态条件 2.3、条件变量函数 2.4、⽣产者消费者模型 2.4.1、为何要使⽤⽣产者…...
uniapp开发小程序,导出文件打开并保存,实现过程downloadFile下载,openDocument打开
uniapp开发小程序,导出文件打开并保存 实现思路 1、调用请求获取到后端接口返回的下载文件的url路径 (注意必须是https的路径,域名需要配置在微信小程序后台的合法域名里面) 2、使用 uni.downloadFile 方法 (下载文件…...
腾讯云COS“私有桶”下,App如何安全获得音频调用流程
流程图 #mermaid-svg-Phy4VCltBRZ90UH8 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-Phy4VCltBRZ90UH8 .error-icon{fill:#552222;}#mermaid-svg-Phy4VCltBRZ90UH8 .error-text{fill:#552222;stroke:#552222;}#me…...
简单的 Flask 后端应用
from flask import Flask, request, jsonify, session import os app Flask(__name__) app.secret_key os.urandom(24) users { 123: admin, admin: admin } # 登录接口 app.route(/login, methods[POST]) def login(): data request.get_json() username data.get(usern…...
Android 中 解析 XML 字符串的几种方式
在 Android 开发中,解析 XML 文件有多种方式,每种方式都有其特点和适用场景。常见的 XML 解析方式有 DOM 解析、SAX 解析 和 XmlPullParser 解析。 1、DOM 解析 DOM(Document Object Model)解析是一种基于树结构的解析方式&#…...
git commit
git commit 是版本控制的核心操作之一,用于将暂存区的修改记录为新的版本提交。以下是关键步骤和最佳实践: 基础操作 提交单个文件: bash Copy Code git commit -m “提交信息” 提交多个文件: bash Copy Code …...
【新手向】GitHub Desktop 的使用说明(含 GitHub Desktop 和 Git 的功能对比)
GitHub Desktop 是 GitHub 公司推出的一款桌面应用程序,旨在帮助开发人员更轻松地使用 GitHub,以下是其简单的使用说明: 安装与登录 下载 GitHub Desktop |GitHub 桌面 访问GitHub Desktop 官方网站,根据自己的操作系统下载对应的…...
Tomcat项目本地部署(Servlet为例)
在Windows上部署 在idea中打开项目 首先我们需要准备一个Servlet项目,我之前的Servlet项目是用eclipse写的,这种情况下如果用idea直接打开的话会出现左侧目录无法显示的情况,这个时候我们就需要用别的方法打开 打开项目管理 如下图&#…...
Linux——linux的基本命令
目录 一、linux的目录结构 二、绝对路径和相对路径 三、文件类型(linux下所有东西都可看作文件) 四、文件的权限 五、文件权限的修改(chmod) 六、linux常用的命令 七、文件查看命令 八、文件编辑命令 九、文件压缩与解压…...
wireshark过滤显示rtmp协议
wireshark中抓包显示的数据报文中,明明可以看到有 rtmp 协议的报文,但是过滤的时候却显示一条都没有 查看选项中的配置,已经没有 RTMP 这个协议了,已经被 RTMPT 替换了,过滤框中输入 rtmpt 过滤即可...
Fiddler抓包工具使用技巧:如何结合Charles和Wireshark提升开发调试效率
在开发过程中,网络调试工具是每个程序员的必备利器,特别是当涉及到Web应用和移动应用的调试时,抓包工具的作用尤为突出。无论是处理复杂的API调用、分析性能瓶颈,还是排查网络通信问题,抓包工具都能够帮助开发者精准地…...
LVS负载均衡群集
这里写目录标题 案例:部署Tomcat案例分析案例概述案例前置知识点Tomcat 简介应用场景 案例环境 案例实施实施准备关闭 firewalld 防火墙在安装Tomcat之前必须先安装JDK 查看JDK是否安装安装配置 TomcatTomcat 的安装和配置步骤如下:解压后生成 apache-tomcat-9.0.8文件夹&#…...
【unitrix】 3.5 类型级别的比较系统(cmp.rs)
一、源码 这段代码定义了一个类型级别的比较系统,主要用于在编译时比较类型并得出比较结果。它使用了 Rust 的类型系统和标记特征(trait)来实现这一功能。 use crate::sealed::Sealed; use crate::number::{Z0, P1, N1}; use core::cmp::Ordering;// 比较结果类型…...
防御式编程:防止 XSS 攻击
对用户输入进行编码和过滤是防止 XSS 攻击的关键。以下是改进后的代码示例: from flask import Flask, request, escape from markupsafe import Markup app Flask(__name__) app.route(/comment, methods[POST]) def comment(): user_comment escape(re…...
【Java项目设计】基于Springboot+Vue的OA办公自动化系统
介绍: 基于Springboot为后端,vue为前端的企业综合性OA办公自动化平台,涵盖九大核心模块,全方位解决企业日常办公需求,提升工作效率和管理水平。系统采用模块化设计,功能全面且易于扩展,从基础登…...
WebServer实现:muduo库的主丛Reactor架构
前言 作为服务器,核心自然是高效的处理来自client的多个连接啦,那问题在于,如何高效的处理client的连接呢?这里就介绍两种架构:单Reactor架构和主丛Reactor架构。 单Reactor架构 单Reactor架构的核心为,由一个主线程监…...
每天一个前端小知识 Day 7 - 现代前端工程化与构建工具体系
现代前端工程化与构建工具体系 1. 为什么要工程化?(面试高频问题) 问题痛点: 模块太多、无法组织;代码冗长、性能差;浏览器兼容性差;团队协作混乱,缺少规范与自动化。 工程化目标…...
nginx的下载与安装 mac
1. 下载 方法一:本地下载 链接:https://nginx.org/en/download.html(可直接搜官网) 下载到本地后,上传到linux的某个文件夹中 方法二:直接linux上下载(推荐) wget -c http://ngi…...
[持续集成]
学习目标 能够使用 Git 代码托管平台管理代码能够实现 jenkinspostman 的持续集成能够实现 jenkins代码 的持续集成 持续集成 概念 : 将自己工作成果持续不断地把代码聚集在一起,成员可以每天集成一次或多次相关工具 : git : 代码管理工具,自带本地仓库gitee : 远程代码管理…...
Spring Aop @AfterThrowing (异常通知): 使用场景
核心定义 AfterThrowing 是 Spring AOP 中专门用于处理异常场景的**通知(Advice)**类型。它的核心作用是: 仅在目标方法(连接点)的执行过程中抛出异常时,执行一段特定的逻辑。如果目标方法成功执行并正常…...
【赵渝强老师】Kubernetes的安全框架
Kubernetes集群的安全框架主要由以下认证、鉴权和准入控制三个阶段组成。这三个阶段的关系如下图所示。 视频讲解如下 【赵渝强老师】Kubernetes的安全框架 认证(Authentication) 当客户端与Kubernetes集群建立HTTP通信时,首先HTTP请求会进…...
【Python小练习】3D散点图
资产风险收益三维分析 背景 王老师是一名金融工程研究员,需要对多个资产的预期收益、风险(波动率)和与市场的相关性进行综合分析,以便为投资组合优化提供决策依据。 代码实现 import matplotlib.pyplot as plt from mpl_toolk…...
腾讯混元3D制作简单模型教程-2
以下是腾讯混元3D制作简单模型的详细教程,整合最新版本特性(截至2025年6月),操作门槛低且无需专业基础: 🖥 一、在线生成(最快30秒完成) 访问平台 打开 腾讯混元3D创作引擎官网…...
NVIDIA开源Fast-dLLM!解析分块KV缓存与置信度感知并行解码技术
Talk主页:http://qingkeai.online/ 文章原文:https://mp.weixin.qq.com/s/P0PIAMo1GVYH4mdWdIde_Q Fast-dLLM 是NVIDIA联合香港大学、MIT等机构推出的扩散大语言模型推理加速方案。 论文:Fast-dLLM: Training-free Acceleration of Diffusion…...
大白话说目标检测中的IOU(Intersection over Union)
很多同学在学习目标检测时都会遇到IoU这个概念,但总觉得理解不透彻。这其实很正常,因为IoU就像个"多面手",在目标检测的各个阶段都要"打工",而且每个阶段的"工作内容"还不太一样。 今天我就让IoU自…...
CentOS 8解决ssh连接github时sign_and_send_pubkey失败问题
我在一台centos8机器上安装git环境以连接到github,首先第一步需配置好ssh环境,因为我已经有一台Ubuntu机器已经配置好ssh环境,所以我ftp Ubuntu机器取得id_rsa id_rsa.pub known_hosts三个文件,然后执行命令: $ git …...
回答 如何通过inode client的SSLVPN登录之后,访问需要通过域名才能打开的服务
需要dns代理 1 配置需求或说明 1.1 适用的产品系列 本案例适用于软件平台为Comware V7系列防火墙:本案例适用于如F5080、F5060、F5030、F5000-M等F5000、F5000-X系列的防火墙。 注:本案例是在F100-C-G2的Version 7.1.064, Release 9510P08版本上进行…...
OpenCV实现二值图细化(骨架提取)
对二值图进行细化(骨架提取),也就是把每根线条细化到一个像素的宽度。有两个比较成熟的算法实现此功能,分别是Zhang-Suen算法和Guo-Hall算法。 我们下面使用OpenCVSharp,使用C#实现上述两个算法: private…...
Excel常用公式大全
资源宝整理分享:https://www.httple.net Excel常用公式大全可以帮助用户提高工作效率,掌握常用的Excel公式,让数据处理和计算工作更加便捷高效。了解公式学习方法、用途,不再死记硬背,拒绝漫无目的。 命令用途注释说…...
在 Windows 上使用 Docker Desktop 快速搭建本地 Kubernetes 环境(附详细部署教程)
言简意赅的讲解Docker Desktop for Windows搭建Kubernetes解决的痛点 目标读者: 对 Docker Desktop 有一定了解,能在 Windows 上成功安装和使用 Docker Desktop。想要在本地快速搭建一套 Kubernetes 环境进行测试或学习的开发者。 一、准备工作 安装 Doc…...
Python设计模式终极指南:18种模式详解+正反案例对比+框架源码剖析
下面我将全面解析18种Python设计模式,每种模式都包含实际应用场景、优缺点分析、框架引用案例、可运行代码示例以及正反案例对比,帮助您深入理解设计模式的价值。 一、创建型模式(5种) 1. 单例模式(Singleton&#x…...
第1章: 伯努利模型的极大似然估计与贝叶斯估计
伯努利模型的极大似然估计与贝叶斯估计 import numpy as np import matplotlib.pyplot as plt from scipy.stats import beta, bernoulli from scipy.optimize import minimize_scalar# 设置中文字体 plt.rcParams[font.sans-serif] [SimHei] # 使用黑体 plt.rcParams[axes.…...
IPv4编址及IPv4路由基础
一、实验目的 掌握接口 IPv4 地址的配置方法理解 LoopBack 接口的作用与含义理解直连路由的产生原则掌握静态路由的配置方法并理解其生效的条件掌握通过 PING 工具测试网络层连通性掌握并理解特殊静态路由的配置方法与应用场景 二、实验环境 安装有eNSP模拟器的PC一台&#…...
基于Python的机动车辆推荐及预测分析系统
博主介绍:java高级开发,从事互联网行业六年,熟悉各种主流语言,精通java、python、php、爬虫、web开发,已经做了六年的毕业设计程序开发,开发过上千套毕业设计程序,没有什么华丽的语言࿰…...
SpringBoot扩展——发送邮件!
发送邮件 在日常工作和生活中经常会用到电子邮件。例如,当注册一个新账户时,系统会自动给注册邮箱发送一封激活邮件,通过邮件找回密码,自动批量发送活动信息等。邮箱的使用基本包括这几步:先打开浏览器并登录邮箱&…...
啊啊啊啊啊啊啊啊code
前序遍历和中序遍历构建二叉树 /*** Definition for a binary tree node.* public class TreeNode {* int val;* TreeNode left;* TreeNode right;* TreeNode() {}* TreeNode(int val) { this.val val; }* TreeNode(int val, TreeNode left, TreeNod…...
不同程度多径效应影响下的无线通信网络电磁信号仿真数据生成程序
生成.mat数据: %创建时间:2025年6月19日 %zhouzhichao %遍历生成不同程度多径效应影响的无线通信网络拓扑推理数据用于测试close all clearsnr 40; n 30;dataset_n 100;for bias 0.1:0.1:0.9nodes_P ones(n,1);Sampling_M 3000;%获取一帧信号及对…...
C语言学习day17-----位运算
目录 1.位运算 1.1基础知识 1.1.1定义 1.1.2用途 1.1.3软件控制硬件 1.2运算符 1.2.1与 & 1.2.2或 | 1.2.3非 ~ 1.2.4异或 ^ 1.2.5左移 << 1.2.6右移 >> 1.2.7代码实现 1.2.8置0 1.2.9置1 1.2.10不借助第三方变量,实现两个数的交换…...
Spring MVC参数绑定终极手册:单多参对象集合JSON文件上传精讲
我们通过浏览器访问不同的路径,就是在发送不同的请求,在发送请求时,可能会带一些参数,本文将介绍了Spring MVC中处理不同请求参数的多种方式 一、传递单个参数 接收单个参数,在Spring MVC中直接用方法中的参数就可以…...
宽度优先遍历(bfs)(2)——fllodfill算法
欢迎来到博主的专栏:算法解析 博主ID:代码小豪 文章目录 floodfiil算法leetcode733——图像渲染题目解析算法原理题解代码 leetcode130——被围绕的区域题目解析算法原理题解代码 floodfiil算法 floodfill算法,在博主这里看来则是一个区域填…...
嵌入式编译工具链熟悉与游戏移植
一、Linux 系统编译工具链使用与 mininim 源码编译 在 Ubuntu 系统上编译 mininim 开源游戏需要正确配置编译工具链和依赖库。以下是详细的操作步骤和故障解决方法: 1. 环境准备与源码获取 首先需要安装必要的编译工具和依赖库: # 更新系统软件包索引…...
STUN (Session Traversal Utilities for NAT) 服务器是一种网络协议
STUN (Session Traversal Utilities for NAT) 服务器是一种网络协议,主要用于帮助位于网络地址转换 (NAT) 设备(如路由器)后面的客户端发现自己的公共 IP 地址和端口号。这对于建立点对点 (P2P) 通信至关重要,尤其是在 VoIP&#…...
Transformer结构介绍
[编码器 Encoder] ←→ [解码器 Decoder] 编码器: 输入:源语言序列输出:每个词的上下文表示(embedding) 解码器:输入:目标语言序列编码器输出输出:下一个词的概率分布(目标句子生成)…...
SpringBoot扩展——应用Web Service!
应用Web Service Web Service是一个SOA(面向服务的编程)架构,这种架构不依赖于语言,不依赖于平台,可以在不同的语言之间相互调用,通过Internet实现基于HTTP的网络应用间的交互调用。Web Service是一个可以…...
5G核心网周期性注册更新机制:信令流程与字段解析
一、周期性注册更新的技术背景与流程概述 1.1 注册更新的核心目的 在5G网络中,UE通过周期性注册更新维持与核心网的连接状态,主要作用包括: 状态保活:避免AMF因超时而释放UE上下文(T3512定时器超时前需完成更新);位置更新:通知网络UE的当前位置,确保寻呼可达;能力同…...
【LLM学习笔记3】搭建基于chatgpt的问答系统(下)
目录 一、检查结果检查有害内容检查是否符合产品信息 二、搭建一个简单的问答系统三、评估输出1.当存在一个简单的正确答案2.当不存在一个简单的正确答案 一、检查结果 本章将引领你了解如何评估系统生成的输出。在任何场景中,无论是自动化流程还是其他环境&#x…...
算法导论第十九章 并行算法:解锁计算新维度
第十九章 并行算法:解锁计算新维度 “并行计算不是未来,而是现在。” —— David Patterson 在单核性能增长放缓的时代,并行算法成为突破计算极限的关键。本章将带你探索多核处理器、分布式系统和GPU加速的奇妙世界,揭示如何通过协…...
Python 数据分析与可视化 Day 1 - Pandas 数据分析基础入门
🎯 今日目标 理解 Pandas 的作用和核心概念学会创建 Series 和 DataFrame掌握基本数据读取(CSV)与常用查看方法 🧰 1. 什么是 Pandas? Pandas 是基于 NumPy 的强大数据分析库,提供了灵活的表格数据结构 Da…...
【数字人开发】Unity+百度智能云平台实现长短文本个性化语音生成功能
一、创建自己的应用 百度智能云控制台网址:https://console.bce.baidu.com/ 1、创建应用 2、获取APIKey和SecretKey 3、Api调试 调试网址:https://console.bce.baidu.com/support/?timestamp1750317430400#/api?productAI&project%E8%AF%AD%E9%…...
(哈希)128. 最长连续序列
题目 给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。 请你设计并实现时间复杂度为 O(n) 的算法解决此问题。 示例 1: 输入:nums [100,4,200,1,3,2] 输出ÿ…...