目录
1、单例模式
饿汉方式实现单例模式
懒汉方式实现单例模式
单例模式实现线程池(懒汉模式)
2、STL、智能指针、线程安全
STL中的容器不是线程安全的
智能制造是否是线程安全的
其它常见的各种锁
3、读者写者问题
读写锁的函数接口
代码实现读者写者优先问题
读者加锁和写者加锁的基本原理
单例模式的概念:
- 单例(Singleton)模式,是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中,应用该模式的类一个类只有一个实例。即一个类只有一个对象实例 ;
单例模式的使用场景:
- 语义上只需要一个
- 该对象内部存在大量的空间,保存了大量的数据,如果允许该对象存在多份,或者允许发生各种拷贝,内存中存在冗余数据;
单例模式有两种实现模式:
- 饿汉模式:吃完饭,立刻洗碗,这种就是饿汉方式。因为下一顿吃的时候可以立刻拿着碗就能吃饭。
- 懒汉模式:吃完饭,先把碗放下,然后下一顿饭用到这个碗了再洗碗,就是懒汉方式。懒汉模式最核心的思想是 "延时加载"。(例如我们之前所学过的写时拷贝)从而能够优化服务器的启动速度。
该模式在类被加载时就会实例化一个对象,具体代码如下:
template
class Singleton { private: static Singleton data;//饿汉模式,在加载的时候对象就已经存在了 public: static Singleton * GetInstance() { return &data; } }; 该模式能简单快速的创建一个单例对象,而且是线程安全的(只在类加载时才会初始化,以后都不会)。但它有一个缺点,就是不管你要不要都会直接创建一个对象,会消耗一定的性能(当然很小很小,几乎可以忽略不计,所以这种模式在很多场合十分常用而且十分简单)
该模式只在你需要对象时才会生成单例对象(比如调用GetInstance方法)
template
class Singleton { private: static Singleton * inst; //懒汉式单例,只有在调用GetInstance时才会实例化一个单例对象 public: static Singleton * GetInstance() { if (inst == NULL) { inst = new Singleton (); } return inst; } }; 看上去,这段代码没什么明显问题,但它不是线程安全的。假设当前有多个线程同时调用GetInstance()方法,由于当前还没有对象生成,那么就会由多个线程创建多个对象。
// 懒汉模式, 线程安全 template
class Singleton { private: static Singleton * inst; static std::mutex lock; public: static T* GetInstance() { if (inst == NULL) // 双重判定空指针, 降低锁冲突的概率, 提高性能 { lock.lock(); // 使用互斥锁, 保证多线程情况下也只调用一次 new if (inst == NULL) { inst = new T(); } lock.unlock(); } return inst; } }; 这种形式是在懒汉方式的基础上增加的,当多个线程调用GetInstance方法时,此时类中没有对象,那么多个线程就会来到锁的位置,竞争锁。必然只能有一个线程竞争锁成功,此时再次判断有没有对象被创建(就是inst指针),如果没有就会new一个对象,如果有就会解锁,并返回已有的对象;总的来说,这样的形式使得多个线程调用GetInstance方法时,无论成功与否,都会有返回值;
我们在上篇博文原有线程池的基础上做修改,改成单例模式版本。变动如下:
ThreadPool.hpp文件:
- 将构造函数私有化,利用delete删除拷贝构造和拷贝复制函数。在类内定义线程池类型的static指针变量instance,类外初始化此static变量。为了在类外获得此static静态变量instance,在内部实现一个getInstance函数,需要对其加锁,直接复用先前实现的RAII风格的加锁解锁方式。我们可以使用prctl来指定创建线程的名字,便于后续使用ps命令查看相关信息。
#pragma once #include
#include #include #include #include #include #include //智能指针 #include #include "Log.hpp" #include "Lock.hpp" using namespace std; int gThreadNum = 5; // 线程池的容量 template class ThreadPool { private: // 构造函数 ThreadPool(int threadNum = gThreadNum) : threadNum_(threadNum), isStart_(false) { assert(threadNum_ > 0); pthread_mutex_init(&mutex_, nullptr); pthread_cond_init(&cond_, nullptr); } ThreadPool(const ThreadPool &) = delete; void operator=(const ThreadPool &) = delete; public: static ThreadPool *getInstance() { static Mutex mutex; if (nullptr == instance) // 仅仅是过滤重读的判断 { LockGuard lockguard(&mutex); // RAII风格的加锁解锁方式 if (nullptr == instance) { instance = new ThreadPool (); } } return instance; } // 线程函数(注意这里是类内成员函数,具有隐含的this指针, 而定义成static成员函数,则没有this指针) static void *threadRoutine(void *args) { pthread_detach(pthread_self()); ThreadPool *tp = static_cast *>(args); prctl(PR_SET_NAME, "follower"); while (1) { tp->lockQueue(); while (!tp->haveTask()) { // 没有任务 tp->waitForTask(); } // 有任务, 这个任务被拿到线程的上下文中 T t = tp->pop(); tp->unlockQueue(); // for debug int one, two; char oper; t.get(one, two, oper); // 所有的任务都必须有一个run方法 Log() << "新线程完成计算任务: " << one << oper << two << "=" << t.run() << endl; } } // 让线程池启动, 让所有线程跑起来 void start() { assert(!isStart_); for (int i = 0; i < threadNum_; i++) { // 创建线程 pthread_t temp; pthread_create(&temp, nullptr, threadRoutine, this); } isStart_ = true; } // 向任务队列里放任务(主线程调用) void push(const T &in) { lockQueue(); taskQueue_.push(in); choiceThreadForHandler(); unlockQueue(); } // 析构函数 ~ThreadPool() { pthread_mutex_destroy(&mutex_); pthread_cond_destroy(&cond_); } private: // 加锁 void lockQueue() { pthread_mutex_lock(&mutex_); } // 解锁 void unlockQueue() { pthread_mutex_unlock(&mutex_); } // 检测是否有任务 bool haveTask() { return !taskQueue_.empty(); } // 等待任务 void waitForTask() { pthread_cond_wait(&cond_, &mutex_); } // 选择某一个线程去执行 void choiceThreadForHandler() { pthread_cond_signal(&cond_); } // 从任务队列里获取任务(线程池中的线程调用) T pop() { T temp = taskQueue_.front(); taskQueue_.pop(); return temp; } private: bool isStart_; // 表示当前线程是否已经启动 int threadNum_; // 线程池中线程的数量 queue taskQueue_; // 任务队列 pthread_mutex_t mutex_; // 让线程互斥的获得任务队列里的内容 pthread_cond_t cond_; // 让线程没有任务时在条件变量下等待,有任务时再唤醒线程 static ThreadPool *instance; // 定义线程池指针 }; template ThreadPool *ThreadPool ::instance = nullptr; Task.hpp文件:
#pragma once #include
#include class Task { public: Task() : elemOne_(0), elemTwo_(0), operator_('0') {} Task(int one, int two, char op) : elemOne_(one), elemTwo_(two), operator_(op) {} // 仿函数 int operator()() { return run(); } // 执行任务 int run() { int result = 0; switch (operator_) { case '+': result = elemOne_ + elemTwo_; break; case '-': result = elemOne_ - elemTwo_; break; case '*': result = elemOne_ * elemTwo_; break; case '/': { if (elemTwo_ == 0) { std::cout << "div zero, abort" << std::endl; result = -1; } else { result = elemOne_ / elemTwo_; } } break; case '%': { if (elemTwo_ == 0) { std::cout << "mod zero, abort" << std::endl; result = -1; } else { result = elemOne_ % elemTwo_; } } break; default: std::cout << "非法操作: " << operator_ << std::endl; break; } return result; } // 获取参与计算的三个操作数 int get(int &e1, int &e2, char &op) { e1 = elemOne_; e2 = elemTwo_; op = operator_; } private: int elemOne_; int elemTwo_; char operator_; // 具体的运算符号 }; Log.hpp文件:
#pragma once #include
#include #include std::ostream &Log() { std::cout << "Fot Debug |" << " timestamp: " << (uint64_t)time(nullptr) << " | " << "Thread[" << pthread_self() << "] | "; return std::cout; } Lock.hpp文件:
#pragma once #include
#include using namespace std; class Mutex { public: Mutex() { pthread_mutex_init(&lock_, nullptr); } void lock() { pthread_mutex_lock(&lock_); } void unlock() { pthread_mutex_unlock(&lock_); } ~Mutex() { pthread_mutex_destroy(&lock_); } private: pthread_mutex_t lock_; }; class LockGuard { public: LockGuard(Mutex *mutex) : mutex_(mutex) { mutex_->lock(); cout << "加锁成功..." << endl; } ~LockGuard() { mutex_->unlock(); cout << "解锁成功..." << endl; } private: Mutex *mutex_; }; ThreadPoolTest.cc文件:
#include "ThreadPool.hpp" #include "Task.hpp" #include
int main() { prctl(PR_SET_NAME, "master"); const string operators = "+-*/%"; unique_ptr > tp(ThreadPool ::getInstance()); tp->start(); // 定义一个随机数 srand((unsigned long)time(nullptr) ^ getpid() ^ pthread_self()); // 派发任务的线程 while (true) { // 构建任务 int one = rand() % 50; int two = rand() % 10; char oper = operators[rand() % operators.size()]; Log() << "主线程派发计算任务: " << one << oper << two << "=?" << endl; Task t(one, two, oper); // 派发任务 tp->push(t); sleep(1); } return 0; } Makefile文件:
CC=g++ FLAGS=-std=c++11 LD=-lpthread bin=threadpool src=ThreadPoolTest.cc $(bin):$(src) $(CC) -o $@ $^ $(LD) $(FLAGS) .PHONY:clean clean: rm -f $(bin)
测试结果:
我们使用如下指令复制观察现象:
[xzy@ecs-333953 threadpool]$ ps -aL | grep -E 'master|follower'
原因如下:
- STL 的设计初衷是将性能挖掘到极致,而一旦涉及到加锁保证线程安全,会对性能造成巨大的影响。
- 而且对于不同的容器,加锁方式的不同,性能可能也不同(例如hash表的锁表和锁桶)。
- 因此 STL 默认不是线程安全。如果需要在多线程环境下使用,往往需要调用者自行保证线程安全,智能指针是否是线程安全的。
- 对于 unique_ptr,由于只是在当前代码块范围内生效,因此不涉及线程安全问题.
- 对于 shared_ptr,多个对象需要共用一个引用计数变量,所以会存在线程安全问题。但是标准库实现的时候考虑到了这个问题,基于原子操作(CAS)的方式保证 shared_ptr 能够高效,原子的操作引用计数。
- 悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。
- 乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。
- CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。
- 挂起等待锁:当某个线程没有申请到锁的时候,此时该线程会被挂起,即加入到等待队列等待。当锁被释放的时候,就会被唤醒,重新竞争锁。当临界区运行的时间较长时,我们一般使用挂起等待锁。我们先让线程PCB加入到等待队列中等待,等锁被释放时,再重新申请锁。之前所学的互斥锁就是挂起等待锁
- 自旋锁:当某个线程没有申请到锁的时候,该线程不会被挂起,而是每隔一段时间检测锁是否被释放。如果锁被释放了,那就竞争锁;如果没有释放,过一会儿再来检测。如果这里使用挂起等待锁,可能线程刚加入等待队列,锁就被释放了,因此,当临界区运行的时间较短时,我们一般使用自旋锁。
pthread_spin_lock
自旋锁的函数接口只需要把mutex变成spin即可,这里不过多演示。
在编写多线程的时候,有一种情况是十分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地降低我们程序的效率。那么有没有一种方法,可以专门处理这种多读少写的情况呢? 有,那就是读写锁。
读和写的行为:
当前锁状态 读锁请求 写锁请求 无锁 可以 可以 读锁 可以 阻塞 写锁 阻塞 阻塞
- 注意:写独占,读共享,读锁优先级高
对比生产者消费者模型:
生产者消费者模型中,我们说到过321原则:
- 三种关系:生产者和生产者(互斥),消费者和消费者(互斥),生产者和消费者(互斥、同步)
- 两种角色:生产者和消费者(通常是由线程承担的)
- 一个交易场所:通常是指内存中特定的一种内存结构(数据结构)
在讲解读者写者问题前,来看如下的一个例子:
- 比如一个人在画画,可能会有很多人在欣赏,在他画完之前,人们对其作品的各种猜测,在他画完前,人们读到的信息都不准确,因为该画家(写者)并没有画完,这就是读取数据不一致的问题。
- 在他画画的时候,不能说另一个人也参与此作品的绘画过程,这是常识,每个人的画工,笔法都不一样,不能一同参与绘画。对于吃瓜群众(读者和读者之间),它们之间是没有关系的。
下面来总结读者写者的321原则:
- 3种关系:写者和写者(互斥),读者和读者(没有关系),读者和写者(互斥关系)
- 2种角色:读者、写者
- 1个交易场所:读写场所
问:为什么读者和读者之间没有像消费者和消费者之间的互斥关系?
- 因为消费者会把数据拿走,而读者不会。这也是读者写者 vs 生产者消费者的本质区别。
总结:一般读者很多(n),写者很少(1),它们对应的操作伪代码如下
读者 写者 加读锁 加写锁 读取内容 写入修改内容 释放锁 释放锁
定义读写锁变量
pthread_rwlock_t rwlock;
初始化读写锁
动态初始化:pthread_rwlock_init
#include
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr); 参数:
- rwlock: 读写锁的变量的地址
- attr:属性设置为NULL即可。
返回值:
- 成功:0
- 失败:非0
静态初始化:PTHREAD_RWLOCK_INITIALIZER
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
上读锁pthread_rwlock_rdlock
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
参数:
- rwlock:读写锁的变量的地址
返回值:
- 成功:0
- 失败:非0
上写锁pthread_rwlock_wrlock
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
参数:
- rwlock:读写锁的变量的地址
返回值:
- 成功:0
- 失败:非0
解锁pthread_rwlock_unlock
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
参数:
- rwlock:读写锁的变量的地址
返回值:
- 成功:0
- 失败:非0
销毁读写锁pthread_rwlock_destroy
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
参数:
- rwlock:读写锁的变量的地址
返回值:
- 成功:0
- 失败:非0
#include
#include #include using namespace std; int board = 0; pthread_rwlock_t rw; void *reader(void *args) { const char *name = static_cast (args); sleep(2); cout << "sleep done" << endl; while (true) { // 给读者加锁 pthread_rwlock_rdlock(&rw); cout << "reader read: " << board << endl; // 释放锁 pthread_rwlock_unlock(&rw); } } void *writer(void *args) { const char *name = static_cast (args); while (true) { // 给写者加锁 pthread_rwlock_wrlock(&rw); board++; cout << "I am a writer" << endl; sleep(10); // 释放锁 pthread_rwlock_unlock(&rw); } } int main() { // 初始化读写锁 pthread_rwlock_init(&rw, nullptr); pthread_t r1, r2, r3, r4, r5, r6, w; pthread_create(&r1, nullptr, reader, (void *)"reader1"); pthread_create(&r2, nullptr, reader, (void *)"reader2"); pthread_create(&r3, nullptr, reader, (void *)"reader3"); pthread_create(&r4, nullptr, reader, (void *)"reader4"); pthread_create(&r5, nullptr, reader, (void *)"reader5"); pthread_create(&r6, nullptr, reader, (void *)"reader6"); pthread_create(&w, nullptr, writer, (void *)"writer"); pthread_join(r1, nullptr); pthread_join(r2, nullptr); pthread_join(r3, nullptr); pthread_join(r4, nullptr); pthread_join(r5, nullptr); pthread_join(r6, nullptr); pthread_join(w, nullptr); // 释放读写锁 pthread_rwlock_destroy(&rw); return 0; } 如上我用sleep函数控制读者先不读,让写者先进入,并待的时间长一点,当读者醒来的时候,一定是写者先拿到锁了,所以看到的现象应该是写者拿到锁进入临界区休眠了,读者醒来时再想来加锁是会阻塞挂住的,后续很长的时间写者什么都不干,读者也不会区读取:
现在控制代码让读者先跑起来,让读者拿到锁后sleep休眠10s ,写者休眠1s后再启动。此时观察的现象是读者拿到锁进入临界区休眠了,写者醒来时是进不来的
#include
#include #include using namespace std; int board = 0; pthread_rwlock_t rw; void *reader(void *args) { const char *name = static_cast (args); cout << "run..." << endl; while (true) { // 给读者加锁 pthread_rwlock_rdlock(&rw); cout << "reader read: " << board << endl; sleep(10); // 释放锁 pthread_rwlock_unlock(&rw); } } void *writer(void *args) { const char *name = static_cast (args); sleep(1); while (true) { // 给写者加锁 pthread_rwlock_wrlock(&rw); board++; cout << "I am a writer" << endl; sleep(10); // 释放锁 pthread_rwlock_unlock(&rw); } } int main() { // 初始化读写锁 pthread_rwlock_init(&rw, nullptr); pthread_t r1, r2, r3, r4, r5, r6, w; pthread_create(&r1, nullptr, reader, (void *)"reader1"); pthread_create(&r2, nullptr, reader, (void *)"reader2"); pthread_create(&r3, nullptr, reader, (void *)"reader3"); pthread_create(&r4, nullptr, reader, (void *)"reader4"); pthread_create(&r5, nullptr, reader, (void *)"reader5"); pthread_create(&r6, nullptr, reader, (void *)"reader6"); pthread_create(&w, nullptr, writer, (void *)"writer"); pthread_join(r1, nullptr); pthread_join(r2, nullptr); pthread_join(r3, nullptr); pthread_join(r4, nullptr); pthread_join(r5, nullptr); pthread_join(r6, nullptr); pthread_join(w, nullptr); // 释放读写锁 pthread_rwlock_destroy(&rw); return 0; } 多个读者是可以并行运行的,下面修改代码来观察现象:
#include
#include #include using namespace std; int board = 0; pthread_rwlock_t rw; void *reader(void *args) { const char *name = static_cast (args); cout << "run..." << endl; while (true) { // 给读者加锁 pthread_rwlock_rdlock(&rw); cout << "reader read: " << board << "tid: " << pthread_self() << endl; sleep(10); // 释放锁 pthread_rwlock_unlock(&rw); } } void *writer(void *args) { const char *name = static_cast (args); sleep(1); while (true) { // 给写者加锁 pthread_rwlock_wrlock(&rw); board++; cout << "I am a writer" << endl; sleep(10); // 释放锁 pthread_rwlock_unlock(&rw); } } int main() { // 初始化读写锁 pthread_rwlock_init(&rw, nullptr); pthread_t r1, r2, r3, r4, r5, r6, w; pthread_create(&r1, nullptr, reader, (void *)"reader1"); pthread_create(&r2, nullptr, reader, (void *)"reader2"); pthread_create(&r3, nullptr, reader, (void *)"reader3"); pthread_create(&r4, nullptr, reader, (void *)"reader4"); pthread_create(&r5, nullptr, reader, (void *)"reader5"); pthread_create(&r6, nullptr, reader, (void *)"reader6"); pthread_create(&w, nullptr, writer, (void *)"writer"); pthread_join(r1, nullptr); pthread_join(r2, nullptr); pthread_join(r3, nullptr); pthread_join(r4, nullptr); pthread_join(r5, nullptr); pthread_join(r6, nullptr); pthread_join(w, nullptr); // 释放读写锁 pthread_rwlock_destroy(&rw); return 0; }
读者写者的伪代码如下:
- 一旦读者先进入了,readers计数器++了,只要读者不退出,写者无论如何都拿不到锁,只有当readers--到0的时候,写者才能进入写操作,
读者写者进行操作的时候,读者非常多,频率个别高,写者比较少,频率不高。写者随时都会来,但是和读者没有同步关系,只有等读者读完才轮到写者,所以会存在写者饥饿的问题。默认读者优先。所以读者写者同时到来的时候,我们会有两种优先级策略来解决读者写者问题:
- 读者优先:读写同时到来时,让读者优先拿到锁,读者没读完就一直读,写者必须等读者读完才轮到自己
- 写者优先:限制往后到来的其它读者先不要进入临界资源,等当前正在读取的人读完了,先让写者去写,写完再读