哈尔滨网站建设团队,重庆快速网站推广,法人查询网,外贸皮包网站模板文章目录 10. 线程安全的单例模式10.1 什么是设计模式10.2 什么是单例模式10.3 单例模式的特点10.4 饿汉方式和懒汉方式10.5 单例模式的线程池 11. STL和智能指针的线程安全 问题11.1 STL中的容器是否是线程安全的?11.2 智能指针是否是线程安全的? 12. 其他常见的各种锁13. 读… 文章目录 10. 线程安全的单例模式10.1 什么是设计模式10.2 什么是单例模式10.3 单例模式的特点10.4 饿汉方式和懒汉方式10.5 单例模式的线程池 11. STL和智能指针的线程安全 问题11.1 STL中的容器是否是线程安全的?11.2 智能指针是否是线程安全的? 12. 其他常见的各种锁13. 读者写者问题13.1 概念13.2 读写锁接口13.3 读者优先的伪代码 10. 线程安全的单例模式
10.1 什么是设计模式
设计模式Design Pattern是软件工程中的一种最佳实践它是在特定场景下解决特定问题的成熟模板或方案。设计模式是面向对象软件开发过程中经过验证的经验和智慧的结晶它们提供了一种通用的、可复用的解决方案来解决在软件设计中遇到的常见问题。
10.2 什么是单例模式
单例模式Singleton Pattern是一种常用的软件设计模式其核心目的是确保一个类只有一个实例并提供一个全局访问点来获取这个实例。这种模式在需要控制资源访问、节省系统资源、协调系统中的共享资源时非常有用。
10.3 单例模式的特点
单例模式的主要特点包括
唯一性确保一个类只有一个实例。全局访问提供一个全局访问点来获取这个唯一的实例。
10.4 饿汉方式和懒汉方式
饿汉方式Eager Initialization
饿汉方式是指在程序启动时就立即创建单例对象。这种方式的优点是简单、线程安全因为对象的创建是在程序启动时完成的不存在多线程同时访问的问题。缺点是如果单例对象的创建比较耗时或者占用资源较多可能会影响程序的启动速度。
懒汉方式的单例模式实现如下
class Singleton
{
public:static Singleton getInstance() {return instance;}
private:static Singleton instance; // 静态成员变量饿汉式直接在类中创建实例Singleton() {} // 私有构造函数Singleton(const Singleton) delete; // 禁止拷贝构造Singleton operator(const Singleton) delete; // 禁止赋值操作
};// 在类外初始化静态成员变量
Singleton Singleton::instance;懒汉方式Lazy Initialization
懒汉方式是指在第一次使用单例对象时才创建它。这种方式的优点是可以延迟对象的创建从而加快程序的启动速度并且只有在真正需要时才创建对象。缺点是如果多个线程同时访问单例对象可能会存在线程安全问题所以要加锁。
线程不安全的懒汉方式实现的单例模式
class Singleton
{
public:static Singleton* getInstance() {if (instance nullptr) {instance new Singleton();}return instance;}
private:static Singleton* instance; // 静态成员变量指针懒汉式延迟创建实例Singleton() {} // 私有构造函数Singleton(const Singleton) delete; // 禁止拷贝构造Singleton operator(const Singleton) delete; // 禁止赋值操作
};// 在类外初始化静态成员变量指针
Singleton* Singleton::instance nullptr;使用局部静态变量来实现线程安全的懒汉式单例因为局部静态变量的初始化在C 11中是线程安全的。
class Singleton
{
public:static Singleton getInstance() {static Singleton instance; // 局部静态变量线程安全的懒汉式return instance;}
private:Singleton() {} // 私有构造函数Singleton(const Singleton) delete; // 禁止拷贝构造Singleton operator(const Singleton) delete; // 禁止赋值操作
};getInstance方法中的局部静态变量instance只会在第一次调用getInstance时被创建之后的调用都会返回同一个实例这种方式既实现了懒汉式的延迟加载又保证了线程安全。
使用加锁的方式
class Singleton
{
public:static Singleton* getInstance() {if (instance nullptr) { // 双重判定空指针, 降低锁冲突的概率, 提高性能.pthread_mutex_lock(mutex); // 使用互斥锁, 保证多线程情况下也只调用一次 new.if (instance nullptr) instance new Singleton();pthread_mutex_unlock(mutex);}return instance;}
private:static Singleton* instance; // 静态成员变量指针懒汉式延迟创建实例static pthread_mutex_t mutex; // 锁Singleton() {} // 私有构造函数Singleton(const Singleton) delete; // 禁止拷贝构造Singleton operator(const Singleton) delete; // 禁止赋值操作
};// 在类外初始化静态成员变量指针
Singleton* Singleton::instance nullptr;
pthread_mutex_t Singleton::mutex PTHREAD_MUTEX_INITIALIZER;10.5 单例模式的线程池
#pragma once
#include iostream
#include string
#include unistd.h
#include semaphore.h
#include pthread.h
#include vector
#include queue
using namespace std;
struct ThreadData
{pthread_t tid;string name;
};// T表示任务的类型
templateclass T
class ThreadPool
{
public:// ...static ThreadPool* GetInstance(){ if(tp nullptr) {pthread_mutex_lock(lock);if(tp nullptr) tp new ThreadPoolT();pthread_mutex_unlock(lock);}return tp;}
private:ThreadPool(size_t num defaultNum) : _threads(num) {pthread_mutex_init(_mutex, nullptr);pthread_cond_init(_cond, nullptr);}~ThreadPool(){pthread_mutex_destroy(_mutex);pthread_cond_destroy(_cond);}ThreadPool(const ThreadPool tp) delete;const ThreadPool operator(const ThreadPool tp) delete;vectorThreadData _threads;queueT _tasks; // 任务这是临界资源pthread_mutex_t _mutex;pthread_cond_t _cond;static ThreadPool* tp;static pthread_mutex_t lock;
};
templateclass T
ThreadPoolT* ThreadPoolT::tp nullptr;
templateclass T
pthread_mutex_t ThreadPoolT::lock PTHREAD_MUTEX_INITIALIZER;11. STL和智能指针的线程安全 问题
11.1 STL中的容器是否是线程安全的?
原因是, STL 的设计初衷是将性能挖掘到极致, 而一旦涉及到加锁保证线程安全, 会对性能造成巨大的影响.而且对于不同的容器, 加锁方式的不同, 性能可能也不同(例如hash表的锁表和锁桶).因此 STL 默认不是线程安全. 如果需要在多线程环境下使用, 往往需要调用者自行保证线程安全.
11.2 智能指针是否是线程安全的?
对于 unique_ptr, 由于只是在当前代码块范围内生效, 因此不涉及线程安全问题.
对于 shared_ptr, 多个对象需要共用一个引用计数变量, 所以会存在线程安全问题. 但是标准库实现的时候考虑到了这个问题, 基于原子操作(CAS)的方式保证 shared_ptr 能够高效, 原子的操作引用计数.
12. 其他常见的各种锁 悲观锁Pessimistic Locking在每次取数据时总是担心数据会被其他线程修改所以会在取数据前先加锁读锁写锁行锁等当其他线程想要访问数据时被阻塞挂起。乐观锁Optimistic Locking每次取数据时候总是乐观的认为数据不会被其他线程修改因此不上锁。但是在更新数据前会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式版本号机制和CAS操作。 CAS操作当需要更新数据时判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败失败则重试一般是一个自旋的过程即不断重试。 自旋锁Spinlock当一个线程尝试获取一个已经被其他线程持有的锁时该线程不会立即进入等待状态即不会释放CPU而是在原地“自旋”也就是不停地进行忙等待busy-waiting直到获取到锁。 当一个线程尝试获取一个已经被占用的自旋锁时它会在原地循环检查锁的状态直到锁变为可用。自旋锁不会使线程进入睡眠状态因此它是一种非阻塞的同步机制。由于线程不会进入睡眠状态自旋锁避免了线程上下文切换的开销。由于自旋锁会导致CPU资源的占用因此它更适合于那些预计会很快释放的锁。如果锁的持有时间较长自旋锁可能会导致CPU资源的浪费。如果持有自旋锁的线程发生阻塞那么等待该锁的线程可能会无限期地自旋下去导致死锁。之前使用的都属于悲观锁是否采用自旋锁取决于线程在临界资源会待多长时间。 公平锁Fair Lock是一种锁机制它确保了线程获取锁的顺序与它们请求锁的顺序相同。换句话说公平锁保证了“先来先服务”FIFOFirst-In-First-Out的原则即最先请求锁的线程将最先获得该锁。非公平锁Non-Fair Lock是一种锁机制它不保证线程获取锁的顺序与它们请求锁的顺序相同。这意味着当一个线程尝试获取一个非公平锁时它可能会与已经等待该锁的其他线程竞争而不管这些线程等待了多久。 13. 读者写者问题
13.1 概念
在编写多线程的时候有一种情况是十分常见的。那就是有些公共数据修改的机会比较少。相比较改写它们读的机会反而高的多。通常而言在读的过程中往往伴随着查找的操作中间耗时很长。给这种代码段加锁会极大地降低我们程序的效率。那么有没有一种方法可以专门处理这种多读少写的情况呢 有那就是读写锁。 3种关系 写者 vs 写者 互斥读者 vs 写者 互斥同步读者 vs 读者 共享关系这是和生产消费者模型的区别 2个角色读者和写者1个交易场所数据交换的地点 为什么读者写者问题中读者和读者关系是共享 而生产消费者模型中 消费者和消费者的关系是互斥呢
因为读者并不会对数据做处理只是对数据进行读操作。而消费者会对数据进行数据处理。 一般来说读者多写者少。所以概率上讲读者更容易竞争到锁写者可能会出现饥饿问题。 这是读者写者问题的特点。也可以更改这个现象设置同步策略让写者优先
读者优先在这种策略下如果读者和写者同时等待访问临界区读者会被优先允许进入。这种策略可以减少写者的等待时间因为读者通常持有锁的时间较短。然而如果读者持续不断地访问数据写者可能会遭遇饥饿即长时间无法获得对数据的访问权。写者优先在这种策略下如果读者和写者同时等待访问临界区写者会被优先允许进入。这种策略可以防止读者饥饿因为写者一旦获得访问权会阻止新的读者进入直到写者完成写操作。但是如果写者频繁地访问数据读者可能会遭遇饥饿。
13.2 读写锁接口
// 初始化
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t* restrict attr);// 销毁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);// 加锁和解锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);13.3 读者优先的伪代码
int reader_count 0;
mutex_t rlock, wlock;// 读者加锁 解锁
lock(rlock);
read_count;
if(reader_count1) lock(wlock);
unlock(rlock);
// 读者进行读取
lock(rlock);
reader_count--;
if(reader_count0) unlock(wlock);
unlock(rlock);// 写者加锁 解锁
lock(wlock);
// 写者进行写入操作
unlock(wlock)读者加锁 首先读者尝试获取 rlock 锁以安全地修改 reader_count 变量。获取 rlock 后读者增加 reader_count 的值。如果这是第一个进入的读者即 reader_count 从0变为1则需要获取 wlock 锁以阻止写者写入数据。这是因为一旦有读者在读取数据写者就不应该修改数据否则会影响读者读取的一致性。读者优先完成 reader_count 的增加和可能的 wlock 获取后读者释放 rlock 锁。 读者解锁 读者完成读取操作后再次获取 rlock 锁以便安全地减少 reader_count 的值。如果这是最后一个离开的读者即 reader_count 从1变为0则需要释放 wlock 锁允许写者进行写入操作。完成 reader_count 的减少和可能的 wlock 释放后读者释放 rlock 锁。 写者加锁 写者尝试获取 wlock 锁以独占访问权进行写入操作。一旦获取 wlock 锁写者可以安全地进行写入操作因为此时没有读者在读取数据。 写者解锁 写者完成写入操作后释放 wlock 锁允许其他读者或写者访问数据。