线程池的设计以及CAS操作、自旋锁、读写锁

线程池

用数组来管理多个线程,挨个存放线程的地址。具体代码详见我的gitee库
https://gitee.com/hepburn0504-yyq/linux-class/tree/master/2023_04_05_ThreadPool

线程池的结构定义为

template <class T>
class ThreadPool
{
private:
    int _num; // 线程数
    std::vector<Thread *> _threads;
    std::queue<T> _task_queue; // 任务队列
    pthread_mutex_t _mtx;      // 锁
    pthread_cond_t _cond;      // 条件变量
};

单例模式

懒汉模式的核心思想是“延迟加载”。

new和malloc也是“延迟加载”!

缺页中断

进程调用malloc后,OS是否直接在物理内存开辟空间?

不是,进程申请空间并不一定立即使用(进行写入操作等),那这就浪费了这段物理内存,故OS先在虚拟地址上开辟一块空间给进程,当进程要往这块空间写入时,触发缺页中断,OS才会真正去开辟物理内存空间,重新构建物理内存和虚拟地址之间的映射关系。

缺页中断,就是当虚拟内存通过页表映射访问物理内存时,发现并没有这段物理内存就会触发缺页中断。让OS去开辟物理内存再重新构建映射关系。

注意:STL中的容器不是线程安全的!如果需要在多线程环境下使用, 往往需要调用者自行保证线程安全。

所谓内存可见性问题,指的是多个线程同时操作一个变量,其中某个线程修改了变量的值之后,其他线程感知不到变量的修改,这就是内存可见性问题。 而使用 volatile 就可以解决内存可见性问题。

STL容器不是线程安全的

智能指针中,unique_ptr不涉及线程安全问题;shared_ptr存在线程安全问题,采用CAS的方式保证原子操作实现引用计数。

补充1:类中的静态成员访问关系

静态成员属于类而不是属于类对象

静态成员变量是一种特殊的成员变量,类体中的数据成员声明时前面加上关键字static,即成为该类的静态数据成员,即静态成员变量。静态成员变量实际上就是类域中的全局变量,必须初始化,且只能在类体外。初始化时不受private和protected访问限制。

static成员变量不占用对象内存,在所有对象外开辟内存,不创建对象也可以访问。

static成员变量和普通static变量一样,编译时在静态数据区分配内存,到程序结束时才释放。这就意味着static成员变量不随对象的创建而分配内存,也不随对象的销毁而释放内存。而普通成员变量在对象创建时分配内存,在对象销毁时释放内存。

static成员变量初始化不赋值会被默认初始化,一般是 0。静态数据区的变量都有默认的初始值,而动态数据区(堆区、栈区)的变量默认是垃圾值。

静态成员变量可以被自己所在类派生类的对象共享。

静态成员变量可以成为成员函数的可选参数,而普通成员变量不可以。

普通成员函数可以访问所有成员变量,而静态成员函数只能访问静态成员变量。

调用一个对象的非静态成员函数时,系统会把当前对象的起始地址赋给 this 指针。而静态成员函数并不属于某一对象,它与任何对象都无关,因此静态成员函数没有 this 指针。

**静态成员函数与非静态成员函数的根本区别是:有无this指针。**由此决定了静态成员函数不能访问本类中的非静态成员。

如果类的成员函数想作为回调函数来使用,一般情况下只能将它定义为静态成员才行。

补充2:单例模式volatile的问题

理论上来说,为了保持内存可见性,应该声明单例对象为volatile,让不同线程都能及时看到该单例对象的改变。

但是在实际应用中,我们不会给单例对象加该关键字,因为用volatile修饰的单例对象,只能调用volatile修饰的成员函数(构造、析构等等),在实际操作中比较麻烦。

注意ThreadPool类的成员设计

class Thread
{
public:
    void start(func_t func, void *args = nullptr)
    {//...
        _func = func;
        _args = args;//有ThreadPool的指针
        int n = pthread_create(&_tid, nullptr, start_routime, this);
    }
    void *callback() { return _func(_args); }
    //ThreadPool::static void *handlerTask(void *args); 把参数包给handlerTask函数

private:
    static void *start_routime(void *args) // start_routime是类内成员,它的参数会有个this指针,要加个static确保只有1个参数
    {//...
        Thread *_this = static_cast<Thread *>(args);
        return _this->callback();
    }
}

template <class T>
class ThreadPool
{
public:
    void run(){//...
    	thread->start(handlerTask, (void *)td);
    }
private:
    // 每个线程将要执行的routime函数
    static void *handlerTask(void *args)
    {
        //从任务队列里取出任务
        task = thread_data->_threadpool->pop();
    }
private:
    std::queue<T> _task_queue; // 任务队列
};

注意传参的细节:1、ThreadPool::run成员方法会调用Thread::start(),start函数先保存回调函数和参数包;2、Thread::start会调用pthread_create函数,给pthread_create函数传的参数是Thread自己类的指针(为了调用Thread::callback函数);3、pthread_create会调用Thread::start_routine函数;4、Thread::start_routine再调用Thread::callback把带有ThreadPool指针的参数包传下去;4、即调用ThreadPool::handleTask函数,相当于该函数虽然是静态的,但也拥有this指针可以访问类内成员_task_queue,可以执行取出任务的动作。

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

日志输出log.hpp

如何处理可变参数。可变参数在函数参数里写作(const char* format, ...)

1、va_系列函数 挨个读取可变参数

头文件
#include <stdarg.h>
函数参数
void va_start(va_list ap, last);//初始化可变参数列表ap,必须要用。last表示第一个变量参数前的命名参数
type va_arg(va_list ap, type);//取出可变参数列表ap里的第一个参数,格式为last,用循环就是挨个取出
void va_end(va_list ap); //相当于ap=nullptr
void va_copy(va_list dest, va_list src);

2、v系列的c语言格式化输出函数

#include <stdarg.h>

int vprintf(const char *format, va_list ap);
int vfprintf(FILE *stream, const char *format, va_list ap);
int vsprintf(char *str, const char *format, va_list ap);
int vsnprintf(char *str, size_t size, const char *format, va_list ap);

vprintf的使用如下

logMsg(NORMAL, "%s: %d %c %.2f", "日志信息", 20230411, 'y', 15.51);
#define NORMAL 1
#define WARNING 2
#define ERROR 3 // 程序出错但不影响执行
#define FATAL 4 // 致命错误

// 日志信息:日志等级 日期 日志内容 支持用户自定义 (代码行数 用户)
void logMsg(int level, const char *format, ...)
{
    va_list args;
    va_start(args, format);
    vprintf(format, args);
    va_end(args); // 相当于ap = nullptr;
}

vsnprintf的使用如下

char logBuffer[1024];//自定义部分

va_list args;
va_start(args, format);
vsnprintf(logBuffer, sizeof logBuffer, format, args);
va_end(args); // 相当于ap = nullptr;

其他常见的各种锁

  1. 悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。
  2. 乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。
  3. CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。

CAS伪代码

CAS(CompareAndSwap),是用来实现lock-free编程的重要手段之一,多数处理器都支持这一原子操作,其用伪代码描述如下

template bool CAS(T*addr,T expected,T value)//内存地址,备份的旧数据,新数据 
{
    if(*addr==expected){
        *addr=value;
        return true;
    }
    return false;
}
int count=0;
void count_atomic_inc(int*addr)
{
    int oldval=0;
    int newval=0;
    do{
        oldval=*addr;//备份旧数据
        newval=old+1;//基于旧数据构造新数据
    }until CAS(addr,oldval,newval)
}

自旋锁spinlock

自旋锁是指当一个线程尝试获取某个锁时,如果该锁已被其他线程占用,就一直循环检测锁是否被释放,而不是进入线程挂起或睡眠状态(互斥锁)。

自旋锁适用于锁保护的临界区很小的情况,临界区很小的话,锁占用的时间就很短。

读写锁rwlock

适用场景:某些公共数据的读频率比写频率高得多。

读写锁的行为:写独占,读共享,读锁优先级高

当前状态读锁请求写锁请求
无锁可以可以
读锁可以阻塞
写锁阻塞阻塞
reader_count = 0;//全局数据,表示读者个数
//伪代码:读者
lock();
read_count++;
unclock();
//读取行为
lock();
read_count--;
unlock();

//伪代码:写者
lock();
if(read_count > 0)//读锁优先级更高{
    unlock();
	goto end;
}
//走到这说明此时没有读锁,就可以写入
//写入行为
unlock();

读者写者与生产消费的本质区别

生产消费模型:

  • 3种关系:生产者与生产者之间是互斥关系,消费者与消费者之间也是互斥关系,生产者和消费者之间既互斥又同步;
  • 2种角色:生产者线程、消费者线程;
  • 1个交易场所:一段特定结构的共享缓冲区。

区别:1、**消费者会取走数据,但是读者不会取走数据。**所以消费者之间必须是互斥关系,读者之间无互斥关系。2、生产者和消费者地位尽可能对等,工作效率高;读者优先级更高,写者可能长期处于饥饿状态。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值