【Linux】线程池 | 自旋锁 | 读写锁

news2024/11/24 5:57:08

文章目录

  • 一、线程池
    • 1. 线程池模型和应用场景
    • 2. 单例模式实现线程池(懒汉模式)
  • 二、其他常见的锁
    • 1. STL、智能指针和线程安全
    • 2. 其他常见的锁
  • 三、读者写者问题
    • 1. 读者写者模型
    • 2. 读写锁


一、线程池

1. 线程池模型和应用场景

线程池是一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。

💕 线程池模型

线程池模型本质上也是生产者消费者模型,线程池的实现原理是:在线程池中预先准备好并创建一批线程,然后上层将任务push到任务队列中,休眠的线程如果检测到任务队列中有任务,就直接被操作系统唤醒,然后去消费并处理任务,唤醒一个线程的代价比创建一个线程的代价小的很多。

在这里插入图片描述

任务线程指的是生产者,任务队列指的是交易场所,右边的一大批线程指的是消费者,因此。线程池的本质还是生产消费模型。

💕 线程池的应用场景

  1. 需要大量的线程来完成任务,且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。
  2. 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
  3. 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误。

2. 单例模式实现线程池(懒汉模式)

💕 ThreadPool.hpp

#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <queue>
#include <unistd.h>
#include "Thread.hpp"
#include "Task.hpp"
#include "lockGuard.hpp"
using namespace std;

const static int N = 5;

// 将此代码设计成单例模式————懒汉模式

template <class T>
class ThreadPool
{
private:
	ThreadPool(int num = N) : _num(num)
	{
		pthread_mutex_init(&_lock, nullptr);
		pthread_cond_init(&_cond, nullptr);
	}
	ThreadPool(const ThreadPool<T>& tp) = delete;
	void operator=(const ThreadPool<T>& tp) = delete;
public:
	// 设计一个静态成员函数来返回创建的对象
	static ThreadPool<T>* getinstance()
	{
		if(_instance == nullptr)
		{
			LockGuard lockguard(&_instance_lock);
			{
				if(_instance == nullptr)
				{
					_instance = new ThreadPool<T>();
					_instance->init();
					_instance->start();
				}
			}
		}
		return _instance;
	}

	pthread_mutex_t *getlock()
	{
		return &_lock;
	}

	void threadWait()
	{
		pthread_cond_wait(&_cond, &_lock);
	}

	void threadWake()
	{
		pthread_cond_signal(&_cond);
	}

	bool isEmpty()
	{
		return _tasks.empty();
	}

	void init()
	{
		for (int i = 0; i < _num; i++)
		{
			_threads.push_back(Thread(i + 1, threadRoutine, this));
		}
	}

	void start()
	{
		for (auto &t : _threads)
		{
			t.run();
		}
	}

	void check()
	{
		for (auto &t : _threads)
			cout << t.threadname() << " running..." << endl;
	}

	static void threadRoutine(void *args)
	{
		ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);
		while (true)
		{
			T t;
			// 检测此时有没有任务, 如果有任务就处理任务, 否则就挂起等待
			{
				LockGuard lockguard(tp->getlock());
				while (tp->isEmpty())
				{
					tp->threadWait();
				}
				t = tp->popTask();
			}
			t();
			cout << "thread handler done, result: " << t.formatRes() << endl;
		}
	}

	T popTask()
	{
		T t = _tasks.front();
		_tasks.pop();
		return t;
	}

	void pushTask(const T &t)
	{
		LockGuard lockguard(&_lock);
		_tasks.push(t);
		threadWake();
	}

	~ThreadPool()
	{
		for (auto &t : _threads)
		{
			t.join();
		}
		pthread_mutex_destroy(&_lock);
		pthread_cond_destroy(&_cond);
	}

private:
	vector<Thread> _threads;
	int _num;

	queue<T> _tasks; // 使用stl的自动扩容机制
	pthread_mutex_t _lock;
	pthread_cond_t _cond;

	static ThreadPool<T>* _instance;
	static pthread_mutex_t _instance_lock;
};

template<class T>
ThreadPool<T>* ThreadPool<T>::_instance = nullptr;

template<class T>
pthread_mutex_t ThreadPool<T>::_instance_lock = PTHREAD_MUTEX_INITIALIZER;

💕 Thread.hpp

#pragma once

#include <iostream>
#include <cstdlib>
#include <string>
#include <pthread.h>
using namespace std;

class Thread
{
public:
    typedef enum{
        NEW = 0,
        RUNNING,
        EXITED
    } ThreadStatus;
    typedef void (*func_t)(void*);

public:
    Thread(int num, func_t func, void* args) :_tid(0), _status(NEW),_func(func),_args(args)
    {
        char name[128];
        snprintf(name, 128, "thread-%d", num);
        _name = name;
    }

    int status(){ return _status; }
    string threadname(){ return _name; }

    pthread_t get_id()
    {
        if(_status == RUNNING)
            return _tid;
        else
            return 0;
    }

    static void* thread_run(void* args)
    {
        Thread* ti = static_cast<Thread*>(args);
        (*ti)();
        return nullptr;
    }

    void operator()()
    {
        if(_func != nullptr)
            _func(_args);
    }

    void run() // 封装线程运行
    {
        int n = pthread_create(&_tid, nullptr, thread_run, this);
        if(n != 0)
            exit(-1);
        _status = RUNNING; // 线程状态变为运行
    }

    void join() // 疯转线程等待
    {
        int n = pthread_join(_tid, nullptr);
        if(n != 0)
        {
            cout << "main thread join thread: " << _name << "error" << endl;
            return;
        }
        _status = EXITED;
    }

    ~Thread(){}
private:
    pthread_t _tid;
    string _name;
    func_t _func; // 线程未来要执行的回调
    void* _args;
    ThreadStatus _status;
};

💕 Task.hpp

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

class Task
{
public:
    Task()
    {}
    Task(int x, int y, char op):_x(x), _y(y), _op(op), _result(0), _exitcode(0)
    {}
    void operator()()
    {
        switch (_op)
        {
        case '+':
            _result = _x + _y; 
            break;
        case '-':
            _result = _x - _y;
            break;
        case '*':
            _result = _x * _y;
            break;
        case '/':
        {
            if(_y == 0)
                _exitcode = -1;
            else 
                _result = _x / _y;
        }
        break;
        case '%':
        {
            if(_y == 0)
                _exitcode = -1;
            else 
                _result = _x % _y;
        }
        break;
        default:
            break;
        }
    }

    string formatArge()
    {
        return to_string(_x) + _op + to_string(_y) + "=";
    }

    string formatRes()
    {
        return to_string(_result) + "(" + to_string(_exitcode) + ")";
    }

    ~Task()
    {}

private:
    int _x;
    int _y;
    char _op;

    int _result;
    int _exitcode;
};

💕 lockGuard.hpp

#pragma once

#include <iostream>
#include <pthread.h>

using namespace std;

class Mutex // 自己不维护锁,有外部传入
{
public:
    Mutex(pthread_mutex_t *mutex):_pmutex(mutex)
    {}
    void lock()
    {
        pthread_mutex_lock(_pmutex);
    }
    void unlock()
    {
        pthread_mutex_unlock(_pmutex);
    }
    ~Mutex()
    {}
private:
    pthread_mutex_t *_pmutex;
};

class LockGuard // 自己不维护锁,有外部传入
{
public:
    LockGuard(pthread_mutex_t *mutex):_mutex(mutex)
    {
        _mutex.lock();
    }
    ~LockGuard()
    {
        _mutex.unlock();
    }
private:
    Mutex _mutex;
};

💕 main.cc

#include "ThreadPool_V4.hpp"
#include "Task.hpp"
#include <memory>

const string ops = "+-*/%";

int main()
{
    srand(time(nullptr) ^ getpid());

    while(true)
    {
        sleep(1);
        int x = rand() % 100;
        int y = rand() % 100;
        char op = ops[(x + y) % ops.size()];
        Task t(x, y, op);
		ThreadPool<Task>::getinstance()->pushTask(t);
        // tp->pushTask(t);
        cout << "the question is what: " << t.formatArge() << " ? " << endl;
    }
    return 0;
}

在这里插入图片描述


二、其他常见的锁

1. STL、智能指针和线程安全

💕 STL中的容器是否是线程安全的?

不是;原因是, STL 的设计初衷是将性能挖掘到极致, 而一旦涉及到加锁保证线程安全, 会对性能造成巨大的影响,而且对于不同的容器, 加锁方式的不同, 性能可能也不同(例如hash表的锁表和锁桶).
因此 STL 默认不是线程安全. 如果需要在多线程环境下使用, 往往需要调用者自行保证线程安全。

💕 智能指针是线程安全的吗?

智能指针是线程安全的吗?unique_ptr 是和资源强关联,只是在当前代码块范围内生效,因此不涉及线程安全问题。对于 shared_ptr,多个对象需要共有一个引用计数变量,所以会存在线程安全问题。但是标准库实现的时候也考虑到了这个问题,就基于原子操作(Compare And Swap(CAS)) 的方式保证 shared_ptr 能够高效原子地操作引用计数。shared_ptr 是线程安全的,但不意味着对其管理的资源进行操作是线程安全的,所以对 shared_ptr 管理的资源进行操作时也可能需要进行加锁保护。


2. 其他常见的锁

  • 悲观锁:悲观锁做事比较悲观,它认为多线程同时修改共享资源的概率比较高,于是很容易出现冲突,所以访问贡献资源前,先要进行加锁保护。常见的悲观锁有:互斥锁、自旋锁和读写锁等。
  • 乐观锁:乐观锁做事比较乐观,它乐观地认为共享数据不会被其他线程修改,因此不上锁。它的工作方式是:先修改完共享数据,再判断这段时间内有没有发生冲突。如果其他线程没有修改共享数据,那么则操作成功。如果发现其他线程已经修改该共享数据,就放弃本次操作。乐观锁全程并没有加锁,所以它也叫无锁编程。乐观锁主要采取两种方式:版本号机制(Gitee等)和 CAS 操作。乐观锁虽然去除了加锁和解锁的操作,但是一旦发生冲突,重试的成本是很高的,所以只有在冲突概率非常低,且加锁成本非常高的场景下,才考虑使用乐观锁。
  • CAS 操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。
  • 自旋锁:使用自旋锁的时候,当多线程发生竞争锁的情况时,加锁失败的线程会忙等待(这里的忙等待可以用 while 循环等待实现),直到它拿到锁。而互斥锁加锁失败后,线程会让出 CPU 资源给其他线程使用,然后该线程会被阻塞挂起。如果临界区代码执行时间过长,自旋的线程会长时间占用 CPU 资源,所以自旋的时间和临界区代码执行的时间是成正比的关系。如果临界区代码执行的时间很短,就不应该使用互斥锁,而应该选用自旋锁。因为互斥锁加锁失败,是需要发生上下文切换的,如果临界区执行的时间比较短,那可能上下文切换的时间会比临界区代码执行的时间还要长。

三、读者写者问题

1. 读者写者模型

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

这就需要我们的读者写者模型出场了,读者写者模型其实也是维护321原则;三种关系:读者与读者、读者与写者、写者与写者。两种对象:读者和写者。一个交易场所:需要写入和从中读取的缓冲区。

下面我们来看一下读者写者模型的三种关系:

  • 读者与读者:没有关系
  • 读者与写者:互斥与同步
  • 写者与写者:互斥

那么,为什么在生产者消费者模型中,消费者和消费者是互斥关系,而在读者写者问题中,读者和读者之间没有关系呢?

读者写者模型和生产者消费者模型的最大区别就是:消费者会将数据拿走,而读者不会拿走数据,读者仅仅是对数据做读取,并不会进行任何修改的操作,因此共享资源也不会因为有多个读者来读取而导致数据不一致的问题。


2. 读写锁

在读者写者模型中,pthread库为我们提供了 读写锁 来维护其中的同步与互斥关系。读写锁由读锁写锁两部分构成,如果只读取共享资源用读锁加锁,如果要修改共享资源则用写锁加锁。所以,读写锁适用于能明确区分读操作和写操作的场景。

读写锁的工作原理:

当写锁没有被写线程持有时,多个读线程能够并发地持有读锁,这大大提高了共享资源的访问效率。因为读锁是用于读取共享资源的场景,所以多个线程同时持有读锁也不会破坏共享资源的数据。但是,一旦写锁被写进程持有后,读线程获取读锁的操作会被阻塞,而其它写线程的获取写锁的操作也会被阻塞。

伪代码:

// 写者进程/线程执行的函数
void Writer()
{
	while(true)
	{
		P(wCountMutex); // 进入临界区
		if(wCount == 0)
			P(rMutex); // 当第一个写者进入,如果有读者则阻塞读者
		wCount++;// 写者计数 + 1
		V(wCountMutex); // 离开临界区

		P(wDataMutex); // 写者写操作之间互斥,进入临界区
		write(); // 写数据
		V(wDataMutex); // 离开临界区

		P(wCountMutex); // 进入临界区
		wCount--; // 写完数据,准备离开
		if(wCount == 0)
		{
			V(rMutex);  // 最后一个写者离开了,则唤醒读者
		}
		V(wCountMutex); //离开临界区
	}
}

// 读者进程/线程执行的次数
void reader()
{
	while(TRUE)
	{
		P(rMutex);
		P(rCountMutex); // 进入临界区
		if ( rCount == 0 )
			P(wDataMutex); // 当第一个读者进入,如果有写者则阻塞写者写操作
		rCount++;
		V(rCountMutex); // 离开临界区
		V(rMutex);
		read( ); // 读数据

		P(rCountMutex); // 进入临界区
		rCount--;
		if ( rCount == 0 )
			V(wDataMutex); // 当没有读者了,则唤醒阻塞中写者的写操作
		V(rCountMutex); // 离开临界区
	}
}

在这里插入图片描述

初始化

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);

读者写者问题很明显会存在读者优先还是写者优先的问题,如果是读者优先的话,可能就会带来写者饥饿的问题。而写者优先可以保证写线程不会饿死,但如果一直有写线程获取写锁,那么读者也会被饿死。所以使用读写锁时,需要考虑应用场景。读写锁通常用于数据被读取的频率非常高,而被修改的频率非常低。注:Linux 下的读写锁默认是读者优先的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1022924.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux安装kafka-manager

相关链接https://github.com/yahoo/kafka-manager/releases kafka-manager-2.0.0.2下载地址 百度云链接&#xff1a;https://pan.baidu.com/s/1XinGcwpXU9YBF46qkrKS_A 提取码&#xff1a;tzvg 一、安装部署 1.把kafka-manager-2.0.0.2.zip拷贝到目录 /opt/app/elk 2.解压…

【Python】让Anaconda不再下载慢下载失败,Anaconda的下载源更换为国内源(保姆级图文)

目录 对于源的添加与修改1. 查看源3. 移除源4. 恢复默认源 检查添加源是否生效清理原来的索引配置总结 欢迎关注 『Python』 系列&#xff0c;持续更新中 欢迎关注 『Python』 系列&#xff0c;持续更新中 对于源的添加与修改 1. 查看源 conda config --show-sources## 2. 添…

天宇微纳芯片ic测试软件如何测试芯片上下电功能?

芯片的上电与下电功能测试是集成电路生产和研发过程中的关键环节&#xff0c;可以帮助企业确保产品的可靠性、整合性和兼容性&#xff0c;同时提高生产效率和产品质量。 因此在芯片的研发设计中&#xff0c;企业会对芯片的上下电有严格的要求&#xff0c;包括上下电的时序&…

飞腾主板显卡接eDP屏,显示花屏问题

故障现象: 开机过程中,在系统启动前无显示输出的阶段中。会有随机的白色竖条显示,位置不固定,且会抖动。 硬件环境: FT-1500A/16+显卡e8860; eDP屏京东方NV156FHM-N42; eDP屏数据(DP0+/-、DP1+/-、AUX)接口e8860直出; eDP屏背光接口控制BL_EN、BL_PWM由主板CPLD…

主要的经典模拟电路图 你都完懂了吗?

经典模拟电路图是电子学中非常重要的概念之一&#xff0c;在各种电子设备中&#xff0c;我们都可以看到这些电路图的身影。 一、差分放大电路&#xff0c;具有电路对称性的特点&#xff0c;此特点可以起到稳定工作点的作用&#xff0c;被广泛用干直接耦合电路和测量电路的输入级…

SSM02

SSM02 此时我们已经做好了登录模块接下来可以做一下学生管理系统的增删改查操作 首先&#xff0c;我们应当有一个登录成功后的主界面 在webapp下新建 1.main.html <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"&…

各种电机驱动原理

步进电机 步进电机参考资料 野火官方文档 步进电机驱动原理 上面参考文档中有的内容就不写了&#xff0c;写一下我自己的总结吧。 说明&#xff1a; 电机驱动器输入信号有电机转动方向信号DIR&#xff0c;电机转速信号PWM&#xff0c;电机使能信号EN&#xff1b;电机驱动器…

JVM调优工具

JVM调优工具 Jmap 查看类信息 此命令可以查看内存信息&#xff0c;实例个数以及占用内存大小。 num&#xff1a;序号instances&#xff1a;实例数量bytes&#xff1a;占用空间大小class name&#xff1a;类名称&#xff0c;[C is a char[]&#xff0c;[S is a short[]&#…

72.Linux系统下printf函数的输出问题

目录 printf函数输出问题 为啥要放到缓冲区呢&#xff1f; \n 强制刷新缓冲区 fflush 强制刷新缓冲区 printf函数输出问题 在 Linux 下&#xff0c;printf 函数通常会先将输出放入缓冲区而不是立即将其发送到终端或文件。这是为了提高性能&#xff0c;因为逐个字符或逐个…

JS 原型的原理

接下来&#xff0c;我们要说一个很重要的东西&#xff0c;就是原型&#xff0c;也就是 prototype。 原型这个东西在 js 里面是非常有用的一个东西&#xff0c;也非常的重要。 对于前端来说&#xff0c;原型肯定不会陌生。 但是有几个问题&#xff1a;这玩意到底是干啥的&…

【论文笔记】图神经网络采样相关工作整理9.19

【论文笔记】图神经网络采样相关工作整理9.19 GraphSAGE NIPS2017 论文&#xff1a;Inductive Representation Learning on Large Graphs 目前引用数&#xff1a;11628 本文提出了一种称为GraphSAGE的新的图嵌入方法&#xff0c;该方法可以在大型图上进行高效的无监督和有监…

STM32F103RCT6学习笔记1:GPIO认识—点灯

今日开始快速掌握这款STM32F103RCT6芯片的环境与编程开发&#xff0c;有关基础知识的部分不会多唠&#xff0c;直接实践与运用&#xff01;文章贴出代码测试工程与测试效果图&#xff1a; 目录 STM32F103RCT6参数解读&#xff1a; GPIO的基础认识与分类&#xff1a; 串口相…

新老用户看过来~最实用的 Milvus 迁移手册来啦!

毫无疑问&#xff0c;Milvus 已经成为全球诸多用户构建生产环境时必不可少的向量数据库。 近期&#xff0c;Milvus 发布了全新升级的 Milvus 2.3 版本&#xff0c;内核引擎加速的同时也加入了诸如支持 GPU 这样实用且强大的特性。可以说&#xff0c;以 Milvus 2.3 为代表的 Mil…

如何第一时间把DLL插件注入到目标进程

在windows下&#xff0c;把DLL插件注入到一个进程的方法有很多&#xff0c;比较常用的比如用远程线程(CreateRemoteThread)注入&#xff0c;或者用windows的API&#xff1a;SetWindowsHookEx来注入&#xff0c;如下图&#xff0c;可以用RemoteDll这个注入工具进行注入操作&…

【双分支混合CNN-transforme:Pansharpening】

DBCT-Net:A dual branch hybrid CNN-transformer network for remote sensing image fusion (DBCT-Net&#xff1a;一种用于遥感图像融合的双分支混合CNN-transformer网络) 遥感图像融合是指将高空间分辨率的单波段全色图像与光谱信息丰富的多光谱图像进行融合&#xff0c;生成…

HTTPS 的加密流程的总结

什么是HTTPS 和HTTP一样也是应用层协议,但在HTTP协议的基础上引入了一个加密层(SSL/TLS). HTTP协议内容都是按照文本的方式明文传输的这导致在传输过程第三方者能够轻易获取传输的内容&#xff0c;而HTTPS在HTTP协议基础上引入一个加密以防止传输内容泄露或被篡改。 因此HTTPS…

项目经理不容错过的一个认证——《研发效能(DevOps)工程师国家职业技术认证》

一、引言 在当今的商业环境中&#xff0c;软件研发已成为企业保持竞争力的关键因素之一。项目经理在软件研发中扮演着重要角色&#xff0c;而随着企业对于研发效率的追求&#xff0c;考取《研发效能(DevOps)工程师国家职业技术认证》对于项目经理的职业发展具有重要意义。本文…

许战海战略文库|无增长则消亡:大型制造集团增长困境

竞争环境不是匀速变化&#xff0c;而是加速变化。企业的衰退与进化、兴衰更迭在不断发生&#xff0c;这成为一种不可避免的现实。在过去的100年里,全球经济周期的时间长度明显缩短,周期内的波动也更为剧烈。联合国教科文组织的研究表明&#xff0c;18世纪知识更新的周期约为80到…

什么国产工作流引擎好?

要想提升办公协作效率&#xff0c;获得高效发展&#xff0c;可以借助低代码技术平台的力量。它的轻量级、灵活、易维护、好操作等优势特点都是现代化办公环境里的得力助手&#xff0c;也可以帮助大家打破信息孤岛&#xff0c;连接内部数据资源&#xff0c;为企业未来发展提供强…

在编译源码的环境下,搭建起Discuz!社区论坛和WordPress博客的LNMP架构

目录 一.编译安装nginx 二.编译安装MySQL 三.编译安装PHP 四.安装论坛 五.安装wordpress博客 六.yum安装LNMP架构&#xff08;简要过程参考&#xff09; 一.编译安装nginx 1&#xff09;关闭防火墙&#xff0c;将安装nginx所需软件包传到/opt目录下 systemctl stop fire…