当前位置: 首页 > news >正文

单例模式和读者写者问题

文章目录

  • 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 单例模式的特点

单例模式的主要特点包括:

  1. 唯一性:确保一个类只有一个实例。
  2. 全局访问:提供一个全局访问点来获取这个唯一的实例。

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表示任务的类型
template<class T>
class ThreadPool
{
public:// ...static ThreadPool* GetInstance(){   if(tp == nullptr) {pthread_mutex_lock(&lock);if(tp == nullptr) tp = new ThreadPool<T>();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;vector<ThreadData> _threads;queue<T> _tasks;    // 任务,这是临界资源pthread_mutex_t _mutex;pthread_cond_t _cond;static ThreadPool* tp;static pthread_mutex_t lock;
};
template<class T>
ThreadPool<T>* ThreadPool<T>::tp = nullptr;
template<class T>
pthread_mutex_t ThreadPool<T>::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)是一种锁机制,它确保了线程获取锁的顺序与它们请求锁的顺序相同。换句话说,公平锁保证了“先来先服务”(FIFO,First-In-First-Out)的原则,即最先请求锁的线程将最先获得该锁。
  • 非公平锁(Non-Fair Lock)是一种锁机制,它不保证线程获取锁的顺序与它们请求锁的顺序相同。这意味着当一个线程尝试获取一个非公平锁时,它可能会与已经等待该锁的其他线程竞争,而不管这些线程等待了多久。

13. 读者写者问题

13.1 概念

在编写多线程的时候,有一种情况是十分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地降低我们程序的效率。那么有没有一种方法,可以专门处理这种多读少写的情况呢? 有,那就是读写锁。

image-20241011150938421

  • 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_count==1)	lock(&wlock);
unlock(&rlock);
// 读者进行读取
lock(&rlock);
reader_count--;
if(reader_count==0)	unlock(&wlock);
unlock(rlock);// 写者加锁 && 解锁
lock(&wlock);
// 写者进行写入操作
unlock(&wlock)
  1. 读者加锁
    • 首先,读者尝试获取 rlock 锁,以安全地修改 reader_count 变量。
    • 获取 rlock 后,读者增加 reader_count 的值。
    • 如果这是第一个进入的读者(即 reader_count 从0变为1),则需要获取 wlock 锁,以阻止写者写入数据。这是因为一旦有读者在读取数据,写者就不应该修改数据,否则会影响读者读取的一致性。(读者优先!)
    • 完成 reader_count 的增加和可能的 wlock 获取后,读者释放 rlock 锁。
  2. 读者解锁
    • 读者完成读取操作后,再次获取 rlock 锁,以便安全地减少 reader_count 的值。
    • 如果这是最后一个离开的读者(即 reader_count 从1变为0),则需要释放 wlock 锁,允许写者进行写入操作。
    • 完成 reader_count 的减少和可能的 wlock 释放后,读者释放 rlock 锁。
  3. 写者加锁
    • 写者尝试获取 wlock 锁,以独占访问权进行写入操作。
    • 一旦获取 wlock 锁,写者可以安全地进行写入操作,因为此时没有读者在读取数据。
  4. 写者解锁
    • 写者完成写入操作后,释放 wlock 锁,允许其他读者或写者访问数据。

相关文章:

单例模式和读者写者问题

文章目录 10. 线程安全的单例模式10.1 什么是设计模式10.2 什么是单例模式10.3 单例模式的特点10.4 饿汉方式和懒汉方式10.5 单例模式的线程池 11. STL和智能指针的线程安全 问题11.1 STL中的容器是否是线程安全的?11.2 智能指针是否是线程安全的? 12. 其他常见的各种锁13. 读…...

内网wordpress更换IP后无法访问的解决办法

一、现象 一台装有wordpress的台式机&#xff0c;从一个校区移到了另一个校区&#xff0c;更换了IP地址&#xff0c;导致无法正常访问。 二、分析 安装wordpress的时候里面的ip&#xff08;或域名&#xff09;都已固定。安装好后&#xff0c;内网通过IP访问&am…...

Spring Boot课程答疑:技术难题一网打尽

4系统概要设计 4.1概述 本系统采用B/S结构(Browser/Server,浏览器/服务器结构)和基于Web服务两种模式&#xff0c;是一个适用于Internet环境下的模型结构。只要用户能连上Internet,便可以在任何时间、任何地点使用。系统工作原理图如图4-1所示&#xff1a; 图4-1系统工作原理…...

云卷云舒【超级数据库】:算力网络时代的云原生数据库

一直关注算力网络&#xff0c;再次分析下移动云的数据库团队&#xff0c;他们在做的一些事情其实比较务实&#xff0c;在推进数据库依托云原生演进到算力网络阶段&#xff0c;这都是在构建一个能够承载无限容量、无感接入、多模融合、智能调度的超级数据库。 未来数据库&#…...

电脑分盘分盘

方案一&#xff1a;使用磁盘管理工具扩展卷功能将未分配磁盘合并到C盘 按WinR输入diskmgmt.msc并按Enter键打开磁盘管理工具。在主界面中右键单击C盘驱动器并选择“扩展卷”&#xff0c;然后按照提示流程操作即可扩展C盘空间。 WinR diskmgmt.msc 注意&#xff1a;虽然系统内置…...

四元数基础知识

背景 四元数是方向的 4 元组表示形式&#xff0c;它比旋转矩阵更简洁。 四元数对于分析涉及三维旋转的情况非常有效。 四元数广泛用于机器人技术、量子力学、计算机视觉和 3D 动画。 您可以在 Wikipedia 上了解有关基本数学概念的更多信息。 您还可以观看由 3blue1brown 制…...

『网络游戏』进入游戏主城UI跳转主城【26】

首先在Unity客户端中创建一个空节点重命名为MainCityWnd 设置父物体为全局 创建空节点钉在左上角作为角色信息UI 在钉子下创建Image 创建脚本&#xff1a;MainCityWnd.cs 编写脚本&#xff1a;MainCityWnd.cs 挂载脚本 创建脚本&#xff1a;MainCitySys.cs 编写脚本&#xff1a…...

多点低压差分(M-LVDS)线路驱动器和接收器——MS2111

MS2111 是多点低压差分 (M-LVDS) 线路驱动器和接收器。经过 优化&#xff0c;可运行在高达 200Mbps 的信号速率下。所有部件均符合 M LVDS 标准 TIA / EIA-899 。该驱动器的输出支持负载低至 30Ω 的多 点总线。 MS2111 的接收器属于 Type-2 &#xff0c; 可在 -1…...

regexp_split_to_table的作用

regexp_split_to_table 是 PostgreSQL 中的一个函数&#xff0c;用于将一个字符串根据正则表达式进行分割&#xff0c;并将结果返回为一个表格&#xff08;每个分割后的部分作为一行&#xff09;。这个函数非常有用&#xff0c;特别是在处理复杂字符串时。 语法 regexp_split…...

【MATLAB】基于RSSI的蓝牙定位程序,4个锚点、二维平面

目录 ​编辑 商品描述 主要功能 技术细节 适用场景 下载链接 商品描述 这款基于接收信号强度指示&#xff08;RSSI&#xff09;原理的蓝牙定位程序&#xff0c;专为需要高效、可靠定位解决方案的开发者和研究人员设计。它能够在二维平面内&#xff0c;通过4个锚点实现对未…...

利用 langchain 和 LLM 来给 PDF 做总结

在网上看到一个PDF, 讲的是 Gstreamer 的的动态管道的构建, 一瞥而过, 没时间细看, 先写个小程序通过 langchain 和 LLM 给它做个快速总结 代码如下 from langchain.document_loaders import UnstructuredPDFLoader from langchain.llms import OpenAI from langchain.chains i…...

props 不能轻易解构,注意maxLength类似这种,不能解构出来

当您从 props 对象中解构 msg 时&#xff0c;msg 变量将会获取到当时的 props.msg 值。解构操作仅仅是将当前值复制到 msg 变量中&#xff0c;它并不会建立响应式连接。因此&#xff0c;当 props.msg 发生变化时&#xff0c;解构出的 msg 变量仍保持其原始值&#xff0c;不会自…...

总结拓展十三:SAP系统采购订单关闭实例分享

1、案例分享 我们集团A基地和B基地存在外包加工业务。A基地向B基地外包采购了多起不同类型的物料&#xff0c;近期有部分外包采购暂停&#xff0c;需要采购关闭未完成交货的采购订单。采购在关闭时出现2类报错问题&#xff0c;向我们IT咨询解决方案。 1&#xff09;报错类型 …...

内嵌服务器Netty Http Server

内嵌式服务器不需要我们单独部署&#xff0c;列如SpringBoot默认内嵌服务器Tomcat,它运行在服务内部。使用Netty 编写一个 Http 服务器的程序&#xff0c;类似SpringMvc处理http请求那样。举例&#xff1a;xxl-job项目的核心包没有SpringMvc的Controller层&#xff0c;客户端却…...

Maven打包运行,引入三方jar及打包,不导入本地库的方法

Maven打包运行&#xff0c;引入三方jar及打包&#xff0c;不导入本地库的方法 maven、打包、springboot、jar、本地、引入背景 业务系统要对接某硬件&#xff0c;需要用到其三方jar&#xff0c;maven官方仓库没有这个&#xff0c;我也没有maven&#xff0c;又不想mvn install…...

02复写零

复写零 我们先进行异地复写&#xff1a;代码如下 public class Test {public static void main(String[] args) {int []array {1,0,2,3,0,4};duplicateZeros(array);}public static void duplicateZeros(int[] arr) {int [] elemnew int[arr.length];for(int cur0,dest0;des…...

01-gcc编译c++过程

当然&#xff0c;可以为您简要概述GCC编译C程序的各个步骤及其对应的具体命令。 GCC编译C的四个主要步骤 预处理&#xff08;Preprocessing&#xff09;编译&#xff08;Compilation&#xff09;汇编&#xff08;Assembly&#xff09;链接&#xff08;Linking&#xff09; 1…...

互动式教育技术:Spring Boot师生共评作业管理系统

3系统分析 3.1可行性分析 通过对本师生共评的作业管理系统实行的目的初步调查和分析&#xff0c;提出可行性方案并对其一一进行论证。我们在这里主要从技术可行性、经济可行性、操作可行性等方面进行分析。 3.1.1技术可行性 本师生共评的作业管理系统采用JAVA作为开发语言&…...

【云从】三、计算机网络基础

文章目录 1、网络2、网络通信2.1 IP地址2.2 子网掩码2.3 网关2.4 私有地址和公有地址2.5 NAT网络地址转换 3、网络架构及设备 1、网络 网络&#xff0c;即通过通信线路&#xff08;如光纤、网线&#xff09;和通信设备&#xff08;如路由器、光猫&#xff09;&#xff0c;将各…...

读书笔记《向上生长》关于记忆、链接的一些思考

摘录 NOTES 1.大脑非常低效&#xff0c;记性不好&#xff0c;还会自圆其说。很多成绩不好的学生绝大部分不记笔记&#xff0c;记了也不看。 2.注意力和肌肉一样&#xff0c;存在耗损&#xff0c;也可以通过锻炼来加强。 3. 所有知识必须链接到已经有的知识&#xff0c;到用的时…...

Kubesphere4.1版本创建应用Mysql并实现外网访问

目前Kubesphere4.1版本可查创建应用资料较少&#xff0c;特此记录作为参考。 目标 使用Kubesphere4.1完成Mysql服务部署并实现外网访问。 具体流程如下&#xff1a; 1.创建企业空间 2.创建项目 3.创建应用仓库 4.创建应用&#xff08;mysql&#xff09; 5.配置外网访…...

小猿口算跟风版——没想到吧,这也能暴力

前言 “接口也是口&#xff0c;算法也是算”&#xff0c;综合来看就是口算&#xff01; 看到全网都在炸鱼也忍不住来玩一玩… 参考了大佬的OCR版&#xff0c;试用后发现影响速度的最大卡点并不是识别速度等&#xff0c;而是““由于检测异常导致的等待”。 基于体验几局的感受…...

【RabbitMQ——消息应答机制——分布式事务解决方式】

1. RabbitMQ高级-消息确认机制的配置 NONE值是禁用发布确认模式&#xff0c;是默认值 CORRELATED值是发布消息成功到交换器后会触发回调方法&#xff0c;如1示例SIMPLE值经测试有两种效果&#xff0c;其一效果和CORRELATED值一样会触发回调方法&#xff0c;其二在发布消息成功…...

Android Studio Koala中Kotlin引入序列化Parcelable

找了一堆资料没有新构建序列化的方法&#xff0c;踩坑经历如下&#xff1a; 前提是使用Kotlin创建的项目 之前的build.gradle版本写法如下&#xff1a; 但是新版Android Studio Koala使用序列化模式发生了改变&#xff0c;如下&#xff1a; 测试成功如下&#xff1a; 发出来…...

安装postgresql和对应wal2json和pg_tm_aux插件避坑

1、安装原则&#xff1a;要用源码都用源码&#xff0c;要用yum都用yum 安装wal2json避坑 涉及的yum源&#xff0c;可以解决安装postgresql缺少依赖导致的wal2json和pg_tm_aux的报错问题 [centos-sclo-rh] nameCentOS-7 - SCLo rh baseurlhttps://mirrors.aliyun.com/centos/7/…...

OneDrive 全新时代:AI、生产力和回忆触手可及

OneDrive&#xff0c;无人不知&#xff0c;无人不晓。 无论您是在公司、家庭还是在通勤的路上&#xff0c;OneDrive 都能让您无缝管理文件、照片和回忆。OneDrive 受到 94% 的财富 500 强公司、数百万小型企业和无数个人的信赖&#xff0c;它正在改变世界协作、存储和连接的方…...

List子接口

1.特点&#xff1a;有序&#xff0c;有下标&#xff0c;元素可以重复 2.方法&#xff1a;包含Collection中的所有方法&#xff0c;还包括自己的独有的方法&#xff08;API中查找&#xff09; 还有ListIterator&#xff08;迭代器&#xff09;&#xff0c;功能更强大。 包含更多…...

快来了解 Java 内存数据库 H2,不要错过哦

H2 是一个用 Java 编写的开源关系型数据库管理系统&#xff0c;它具有小巧、快速、易于嵌入等特点。 H2 数据库可以以多种模式运行&#xff0c;包括嵌入式模式、服务器模式和内存模式。 在嵌入式模式下&#xff0c;H2 数据库直接嵌入到应用程序中&#xff0c;就像一个本地的库…...

基于Java的可携宠物酒店管理系统的设计与实现(论文+源码)_kaic

摘 要 随着社会经济的不断发‎‏展&#xff0c;现如今出行并住酒店的人越来越多&#xff0c;与之而来的是酒店行业的工作量日益增加&#xff0c;酒店的管理效率亟待提升。此外很多人出门旅游时会有携带宠物的情况&#xff0c;但是现如今酒店对宠物的限制&#xff0c;导致许多…...

吸毛效果好的宠物空气净化器分享,希喂、霍尼韦尔、米家实测

说起宠物空气净化器&#xff0c;几年前我可能会一脸鄙夷&#xff1a;为啥要花这种智商税冤枉钱&#xff1f; 直到之前养了一只猫&#xff0c;被家中乱飞的浮毛和滂臭的异味搞到头晕&#xff0c;于是作为i一个养宠的家电测评博主&#xff0c;索性对宠物空气净化器这玩意做了超级…...