设计模式——单例模式

★★★原文链接★★★:https://subingwen.cn/design-patterns/singleton/

饿汉模式:

定义类的时候就创建单例对象;

在多线程下,饿汉模式没有线程安全问题(多线程可以同时访问单例对象);

#include <iostream>
#include <string>
using namespace std;

// 饿汉模式 -> 定义类的时候创建单例对象
// 定义一个单例模式的任务队列
class TaskQueue {
public:
	TaskQueue(const TaskQueue& t) = delete;
	TaskQueue& operator=(const TaskQueue& t) = delete;
	static TaskQueue* getInstance() {
		return m_taskQ;
	}
	void print() {
		cout << "我是单例对象的一个成员函数" << endl;
	}
private:
	TaskQueue() = default;
	static TaskQueue* m_taskQ;	// 静态成员变量,类内声明,类外初始化
};
TaskQueue* TaskQueue::m_taskQ = new TaskQueue;	// 饿汉模式

int main() {
	TaskQueue* taskQ = TaskQueue::getInstance();
	taskQ->print();
	
	return 0;
}

res:

  

 

懒汉模式:

使用的时候创建对象的实例;

多线程有安全问题(每个线程都会new一个对象,与单例模式的定义是相悖的);

与饿汉模式对比:节省内存空间(不需要时不占用内存);

#include <iostream>
#include <string>
using namespace std;

// 懒汉模式 -> 什么时候使用这个单例对象,在使用的时候再去创建实例
class TaskQueue {
public:
	TaskQueue(const TaskQueue& t) = delete;
	TaskQueue& operator=(const TaskQueue& t) = delete;
	static TaskQueue* getInstance() {	// 与饿汉模式有区别的地方,使用的时候才创建实例
		if (m_taskQ == nullptr) {
			m_taskQ = new TaskQueue;
		}
		return m_taskQ;
	}
	void print() {
		cout << "我是单例对象的一个成员函数" << endl;
	}
private:
	TaskQueue() = default;
	static TaskQueue* m_taskQ;	// 静态成员变量,类内声明,类外初始化
};
TaskQueue* TaskQueue::m_taskQ = nullptr;	// 懒汉模式,初始化静态成员变量为空

int main() {
	TaskQueue* taskQ = TaskQueue::getInstance();
	taskQ->print();
	
	return 0;
}

res:

  

 

懒汉模式 多线程安全问题 解决方案:加互斥锁

加了互斥锁之后的问题:线程顺序访问单例对象,不能同时访问,执行效率低

#include <iostream>
#include <mutex>	// ★★★用互斥锁要包含此头文件
using namespace std;

// 懒汉模式 -> 什么时候使用这个单例对象,在使用的时候再去创建实例
class TaskQueue {
public:
	TaskQueue(const TaskQueue& t) = delete;
	TaskQueue& operator=(const TaskQueue& t) = delete;
	static TaskQueue* getInstance() {	// 与饿汉模式有区别的地方,使用的时候才创建实例
		m_mutex.lock();	// ★★★调用互斥锁的lock方法
		if (m_taskQ == nullptr) {
			m_taskQ = new TaskQueue;
		}
		m_mutex.unlock();	// ★★★
		return m_taskQ;
	}
	void print() {
		cout << "我是单例对象的一个成员函数" << endl;
	}
private:
	TaskQueue() = default;
	static TaskQueue* m_taskQ;	// 静态成员变量,类内声明,类外初始化
	static mutex m_mutex;	// ★★★在静态成员函数里只能访问静态成员变量,所以把互斥锁定义为静态成员变量
};
TaskQueue* TaskQueue::m_taskQ = nullptr;	// 懒汉模式,初始化静态成员变量为空
mutex TaskQueue::m_mutex;	// ★★★静态成员 类外初始化

int main() {
	TaskQueue* taskQ = TaskQueue::getInstance();
	taskQ->print();
	
	return 0;
}

res:

  

 

互斥锁的改进:双重检查锁定

static TaskQueue* getInstance() {	// 与饿汉模式有区别的地方,使用的时候才创建实例
    if (m_taskQ == nullptr) {	// ★★★双重检查之一
        m_mutex.lock();		// ★★★锁定
        if (m_taskQ == nullptr) {	// ★★★双重检查之二
            m_taskQ = new TaskQueue;
        }
        m_mutex.unlock();	
    }

    return m_taskQ;
}

 

原子变量:

双重检查锁定的问题:https://subingwen.cn/design-patterns/singleton/

原因出现在 m_taskQ = new TaskQueue; 在执行过程中,对应的机器指令可能会被重新排序。

 

正常过程如下:

第一步:分配内存用于保存 TaskQueue 对象。

第二步:在分配的内存中构造一个 TaskQueue 对象(初始化内存)。

第三步:使用 m_taskQ 指针指向分配的内存。

 

但是重新排列以后执行顺序可能变成这样:

第一步:分配内存用于保存 TaskQueue 对象。

第二步:使用 m_taskQ 指针指向分配的内存。

第三步:在分配的内存中构造一个 TaskQueue 对象(初始化内存)。

 

假设有A B两个线程,如果线程A按照第二种顺序执行机器指令,执行完前两步后 失去CPU时间片 被挂起了,此时线程B判断 m_taskQ 不为空,但这个指针指向的内存没有被初始化,使用此对象就会出问题。

引入原子变量解决这个问题:

#include <iostream>
#include <mutex>
#include <atomic>
using namespace std;

class TaskQueue {
public:
	TaskQueue(const TaskQueue& t) = delete;
	TaskQueue& operator=(const TaskQueue& t) = delete;
	static TaskQueue* getInstance() {	
		// 把m_taskQ从原子变量里取出来
		// 没有往m_taskQ里存数据之间就加载,取出来的是空指针
		TaskQueue* task = m_taskQ.load();	// ★★★

		if (task == nullptr) {
			m_mutex.lock();	
			task = m_taskQ.load();
			if (task == nullptr) {
				task = new TaskQueue;
				m_taskQ.store(task);	// ★★★
			}
			m_mutex.unlock();	
		}

		return task;
	}
	void print() {
		cout << "我是单例对象的一个成员函数" << endl;
	}
private:
	TaskQueue() = default;
	// static TaskQueue* m_taskQ;	
	static mutex m_mutex;	
	static atomic<TaskQueue*>m_taskQ;	// ★★★原子变量需要在静态函数里使用,所以把原子变量声明为静态变量;在原子变量里管理的是一个TaskQueue类型的指针(TaskQueue*)
};
// TaskQueue* TaskQueue::m_taskQ = nullptr;	
atomic<TaskQueue*> TaskQueue::m_taskQ;	// ★★★在类外声明就行了; 如果要往原子变量里放数据,调用store方法; 如果取数据,调用load方法
mutex TaskQueue::m_mutex;	

 

(最简单的处理方式)

懒汉模式下,另外一种解决多线程安全的方法:使用静态局部对象(前提条件:编译器支持C++11)

因为C++11有如下规定:如果指令逻辑进入一个未被初始化的声明变量,所有并发执行应当等待该变量完成初始化

class TaskQueue {
public:
	TaskQueue(const TaskQueue& t) = delete;
	TaskQueue& operator=(const TaskQueue& t) = delete;
	static TaskQueue* getInstance() {	
		static TaskQueue task;	// 静态局部变量的内存在全局区,当前执行的应用程序关闭时 才被析构; 程序运行期间一直存在; 每次调用getInstance,访问的都是同一块内存区域
		return &task;	// 因为最后返回的是指针,task是TaskQueue类型的对象,所以对task进行取地址操作
	}
	void print() {
		cout << "我是单例对象的一个成员函数" << endl;
	}
private:
	TaskQueue() = default;
};

 

一个简单的任务队列的例子:

#include <iostream>
#include <queue>	
#include <mutex>
#include <thread>	// 多线程对应的头文件
using namespace std;

// 替巴基写一个任务队列
// 饿汉模式
class TaskQueue {
public:
	TaskQueue(const TaskQueue& t) = delete;
	TaskQueue& operator=(const TaskQueue& t) = delete;
	static TaskQueue* getInstance() {
		return m_taskQ;
	}
	void print() {
		cout << "我是单例对象的一个成员函数" << endl;
	}

	// 判断任务队列是否为空
	bool isEmpty() {
		// 通过lock_guard关键字管理互斥锁
		lock_guard<mutex>locker(m_mutex);	// 此时加锁,析构时解锁(isEmpty()函数运行完,这个局部变量locker就自动析构了,避免了死锁的问题)
		bool flag = m_data.empty();
		return flag;
	}

	// 添加任务
	void addTask(int node) {	// 任务队列里存储的是什么类型,添加的任务就要是什么类型
		lock_guard<mutex>locker(m_mutex);
		m_data.push(node);
	}

	// 删除任务(删除队头任务)
	bool popTask() {
		lock_guard<mutex>locker(m_mutex);
		if (m_data.empty()) {
			return false;
		}
		m_data.pop();
		return true;
	}

	// 取出一个任务(不删除任务)
	int taskTask() {
		lock_guard<mutex>locker(m_mutex);
		if (m_data.empty()) {
			return false;
		}
		int data = m_data.front();
		return data;
	}

private:
	TaskQueue() = default;
	static TaskQueue* m_taskQ;
	// 定义任务队列(此成员变量没有必要定义为静态,它属于那个唯一的实例)
	queue<int>m_data;
	mutex m_mutex;
};
TaskQueue* TaskQueue::m_taskQ = new TaskQueue;


int main() {
	TaskQueue* taskQ = TaskQueue::getInstance();
	// 生产者
    // thread t1([=]() { ... }) 创建了一个新线程 t1,这个线程会执行lambda表达式中的代码块,而且使用捕获列表 [=] 捕获了外部作用域的变量值
    // [=]() 是一个lambda表达式的开头,[] 是lambda表达式的捕获列表。在这个情况下,使用= 表示捕获外部作用域中的所有变量值,使得在lambda内部可以访问这些变量
    // { ... } 是lambda表达式的主体,它包含了在新线程中要执行的代码
	thread t1([=]() {
		for (int i = 0; i < 10; i++) {
			taskQ->addTask(i + 100);
			cout << "+++ push data:" << i + 100 << " threadID:" << this_thread::get_id() << endl;	// 输出添加的数据和当前的线程id
			// 让当前线程休眠一段时间
			this_thread::sleep_for(chrono::milliseconds(500));	// 休眠500毫秒
		}
	});

	// 消费者
	thread t2([=]() {
		this_thread::sleep_for(chrono::milliseconds(100));
		while (!taskQ->isEmpty()) {
			int num = taskQ->taskTask();	// 取任务
			cout << "+++ take data:" << num << " threadID:" << this_thread::get_id() << endl;
			taskQ->popTask();	// 删除任务
			this_thread::sleep_for(chrono::milliseconds(1000));
		}
	});
    // t1.join(): 这行代码会阻塞当前线程(通常是主线程),直到线程 t1 完成执行为止。换句话说,当执行到 t1.join() 时,主线程会等待线程 t1 完成其任务后再继续执行主线程的后续代码。这是一种等待线程执行完毕的方式,确保主线程不会在子线程还在运行时就结束。
    // 线程 t1、线程 t2 和主线程是三个并行运行的执行单元。每个线程都独立地执行自己的任务。通过使用 t1.join() 和 t2.join(),主线程会等待子线程执行完毕后再继续执行
	t1.join();
	t2.join();
	
	return 0;
}

res:

  

 

★★★原文链接★★★:https://subingwen.cn/design-patterns/singleton/

(〃>_<;〃)(〃>_<;〃)(〃>_<;〃)

posted @ 2023-08-16 15:16  我会变强的  阅读(13)  评论(0编辑  收藏  举报