热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

[Linux]多线程的同步和互斥(线程池|单例模式|其他常见的锁|读者写者问题)

文章目录线程池线程安全的单例模式单例模式的特点饿汉方式和懒汉方式单例模式实现线程池其他常见的锁读者写者问题线程池线程池是一种线程使用模式。线程过多会带来调度开销,进


在这里插入图片描述



文章目录


  • 线程池
  • 线程安全的单例模式
    • 单例模式的特点
    • 饿汉方式和懒汉方式

  • 单例模式实现线程池
  • 其他常见的锁
  • 读者写者问题



线程池

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

线程池的运用场景:


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

设计一个线程池:


  • 创建固定数量线程池,循环从任务队列中获取任务对象.
  • 获取到任务对象后,执行任务对象中的任务接口.

代码实现:

Makefile文件:

thraed_pool:main.cc
g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -rf thraed_pool

thread_pool.hpp文件:

#pragma once
#include
#include
#include
#include
// 基于工作队列的线程池
namespace ns_thread_pool
{
const int g_num = 10;
template <class T>
class ThreadPool
{
private:
std::queue<T> _task_queue; // 工作队列
int _num; // 工作队列的容量上限
pthread_mutex_t _mtx; // 保护临界资源的互斥量
pthread_cond_t _cond;
public:
void Lock()
{
pthread_mutex_lock(&_mtx);
}
void Unlock()
{
pthread_mutex_unlock(&_mtx);
}
void Wakeup()
{
pthread_cond_signal(&_cond);
}
void Wait()
{
pthread_cond_wait(&_cond, &_mtx);
}
bool IsEmpty()
{
return _task_queue.empty();
}
public:
ThreadPool(int num &#61; g_num) : _num(num)
{
pthread_mutex_init(&_mtx, nullptr);
pthread_cond_init(&_cond, nullptr);
}
static void *Rountine(void *args)
{
pthread_detach(pthread_self());
ThreadPool<T> *tp &#61; (ThreadPool<T> *)args;
while (true)
{
tp->Lock();
while (tp->IsEmpty())
{
tp->Wait();
}
T t;
tp->PopTask(&t);
tp->Unlock();
t();
}
}
void ThreadInit()
{
pthread_t tid;
for (int i &#61; 0; i < _num; i&#43;&#43;)
{
pthread_create(&tid, nullptr, Rountine, (void *)this);
}
}
void PushTask(const T &in)
{
Lock();
_task_queue.push(in);
Unlock();
Wakeup();
}
void PopTask(T *out)
{
*out &#61; _task_queue.front();
_task_queue.pop();
}
~ThreadPool()
{
pthread_mutex_destroy(&_mtx);
pthread_cond_destroy(&_cond);
}
};
}

Task.hpp文件&#xff1a;

#pragma once
#include
#include
#include
namespace ns_task
{
class Task
{
private:
int _x;
int _y;
char _op;
public:
Task() {}
Task(int x, int y, char op):_x(x), _y(y), _op(op) {}
std::string message()
{
std::string msg &#61; std::to_string(_x);
msg &#43;&#61; _op;
msg &#43;&#61; std::to_string(_y);
msg &#43;&#61; "&#61;?";
return msg;
}
int Run()
{
int res &#61; 0;
switch(_op)
{
case &#39;&#43;&#39;:
res &#61; _x &#43; _y;
break;
case &#39;-&#39;:
res &#61; _x - _y;
break;
case &#39;*&#39;:
res &#61; _x * _y;
break;
case &#39;/&#39;:
res &#61; _x / _y;
break;
case &#39;%&#39;:
res &#61; _x % _y;
break;
default:
break;
}
std::cout << "Thread: " << pthread_self() << " Task: " << _x << _op << _y << "&#61;" << res << std::endl;
return res;
}

int operator()()
{
return Run();
}
~Task(){}
};
}

main.cc文件&#xff1a;

#include "thread_pool.hpp"
#include "Task.hpp"
#include
#include
#include
using namespace ns_task;
using namespace ns_thread_pool;
int main()
{
srand((long long)time(nullptr));
ThreadPool<Task>* tp &#61; new ThreadPool<Task>();
tp->ThreadInit();
while(true)
{
Task t(rand()%20&#43;1, rand()%10&#43;1, "&#43;-*/%"[rand()%5]);
tp->PushTask(t);
}
return 0;
}

运行结果&#xff1a;

[cwx&#64;VM-20-16-centos normal_thread_pool]$ make
g&#43;&#43; -o thraed_pool main.cc -std&#61;c&#43;&#43;11 -lpthread
[cwx&#64;VM-20-16-centos normal_thread_pool]$ ./thraed_pool
Thread: 140412644165376 Task: 10-7&#61;3
Thread: 140412644165376 Task: 17&#43;10&#61;27
Thread: 140412669343488 Task: 6-7&#61;-1
Thread: 140412711307008 Task: 2/6&#61;0
Thread: 140412702914304 Task: 10/7&#61;1
......



线程安全的单例模式

单例模式的特点


  • 某些类, 只应该具有一个对象(实例), 就称之为单例.
  • 在很多服务器开发场景中, 经常需要让服务器加载很多的数据 (上百G) 到内存中. 此时往往要用一个单例的类来管理这些数据.

饿汉方式和懒汉方式

洗碗的例子解释饿汉方式和懒汉方式&#xff1a;


  • 吃完饭立刻洗碗&#xff0c;就是饿汉方式&#xff0c;这样下一次就可以立即拿着碗吃饭
  • 吃完饭不立刻洗碗&#xff0c;这就是懒汉方式&#xff0c;下一次用到再洗碗吃饭

懒汉方式最核心的思想就是"延时加载"&#xff0c;从而优化服务器的启动速度。

饿汉方式实现单例模式&#xff1a;

template <typename T>
class Singleton {
static T data;
public:
static T* GetInstance() {
return &data;
}
};

只要通过 Singleton这个类来使用 T 对象, 则一个进程中只有一个 T 对象的实例.

懒汉方式实现单例模式&#xff1a;

template <typename T>
class Singleton {
static T* inst;
public:
static T* GetInstance() {
if (inst &#61;&#61; NULL) {
inst &#61; new T();
}
return inst;
}
};

存在一个严重的问题, 线程不安全.
第一次调用 GetInstance 的时候, 如果两个线程同时调用, 可能会创建出两份 T 对象的实例.

饿汉方式实现单例模式&#xff08;线程安全版本&#xff09;:

template <typename T>
class Singleton {
volatile static T* inst; // 需要设置 volatile 关键字, 否则可能被编译器优化.
static std::mutex lock;
public:
static T* GetInstance() {
if (inst &#61;&#61; NULL) { // 双重判定空指针, 降低锁冲突的概率, 提高性能.
lock.lock(); // 使用互斥锁, 保证多线程情况下也只调用一次 new.
if (inst &#61;&#61; NULL) {
inst &#61; new T();
}
lock.unlock();
}
return inst;
}
};

注意事项:


  1. 加锁解锁的位置
  2. 双重 if 判定, 避免不必要的锁竞争
  3. volatile关键字防止过度优化



单例模式实现线程池

thread_pool.hpp文件&#xff1a;

#pragma once
#include
#include
#include
#include
// 懒汉模式单例模式多线程池
namespace ns_thread_pool
{
const int g_num &#61; 10;
template <class T>
class ThreadPool
{
private:
std::queue<T> _task_queue; // 工作队列 -- 临界资源
int _num; // 工作队列的容量
pthread_mutex_t _mtx; // 保护工作队列临界资源的锁
pthread_cond_t _cond; // 队列为空&#xff0c;在此环境变量等待
static ThreadPool<T>* inst; // 单例模式的静态对象指针
private:
// 单例模式构造函数必须定义以及必须为私有private
ThreadPool(int num &#61; g_num) : _num(num)
{
// 互斥锁和环境变量初始化
pthread_mutex_init(&_mtx, nullptr);
pthread_cond_init(&_cond, nullptr);
}
ThreadPool(const ThreadPool<T> &tp) &#61; delete;
ThreadPool<T> &operator&#61;(ThreadPool<T> &tp) &#61; delete;
public:
void Lock()
{
pthread_mutex_lock(&_mtx);
}
void Unlock()
{
pthread_mutex_unlock(&_mtx);
}
void Wakeup()
{
pthread_cond_signal(&_cond);
}
void Wait()
{
pthread_cond_wait(&_cond, &_mtx);
}
bool IsEmpty()
{
return _task_queue.empty();
}
public:
static ThreadPool<T> *GetInstance()
{
// 静态互斥锁初始化
static pthread_mutex_t lock &#61; PTHREAD_MUTEX_INITIALIZER;
// 双条件判断可以减少锁的争用&#xff0c;提高效率
if(inst &#61;&#61; nullptr)
{
pthread_mutex_lock(&lock);
if(inst &#61;&#61; nullptr)
{
inst &#61; new ThreadPool<T>();
inst->ThreadInit();
}
pthread_mutex_unlock(&lock);
}
return inst;
}
static void *Rountine(void *args)
{
pthread_detach(pthread_self()); // 线程分离&#xff0c;主线程无需关注该线程
ThreadPool<T> *tp &#61; (ThreadPool<T> *)args;
while (true)
{
tp->Lock();
while (tp->IsEmpty())
{
// 工作队列为空&#xff0c;线程需要等待
tp->Wait();
}
T t;
tp->PopTask(&t);
tp->Unlock();
t();
}
}
void ThreadInit()
{
// 创建_num个线程的多线程池
pthread_t tid;
for (int i &#61; 0; i < _num; i&#43;&#43;)
{
pthread_create(&tid, nullptr, Rountine, (void *)this);
}
}
void PushTask(const T &in)
{
Lock();
_task_queue.push(in);
Unlock();
Wakeup();
}
void PopTask(T *out)
{
*out &#61; _task_queue.front();
_task_queue.pop();
}
~ThreadPool()
{
pthread_mutex_destroy(&_mtx);
pthread_cond_destroy(&_cond);
}
};
template<class T>
ThreadPool<T>* ThreadPool<T>::inst &#61; nullptr;
}



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


挂起等待特性的锁 vs 自旋锁

example&#xff1a;小明准备和女朋友小红约会&#xff0c;小明来到小红的宿舍楼下给小红打电话问小红何时下楼&#xff0c;小红表示需要化个妆才能约会需要半个小时的时间&#xff0c;小明听后前往网吧边打游戏边等待小红。如果小红说无需化妆只需要五分钟就可以下楼&#xff0c;小明就不会前往网吧&#xff0c;而是在楼下每一分钟发消息询问小红的下楼情况。
小红如果需要化妆&#xff0c;小明前往网吧的情况等价于挂起等待。
小红无需化妆&#xff0c;小明在楼下不断发消息&#xff0c;检测小红的状态等价于自旋的过程。

决定小明是否前往网吧的因素是小红需要多长时间才能下楼&#xff0c;如果小红需要化妆就前往网吧&#xff0c;等价于线程挂起等待&#xff0c;无需化妆就不断进行检测&#xff0c;等待于自旋的过程&#xff0c;这是因为线程挂起等待是有成本的。

线程如果访问临界资源花费的时长比较短&#xff0c;比较适合自旋锁&#xff1a;

#include
int pthread_spin_destroy(pthread_spinlock_t *lock);
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);

线程如果访问临界资源花费的时长比较长&#xff0c;比较适合挂起等待性质的锁&#xff0c;比如悲观锁。




读者写者问题
  • 在读者写者模型中&#xff0c;对数据的大部分操作是读取&#xff0c;少部分操作是写入。
  • 进行数据读取的一端如果不会将数据取走&#xff0c;就可以考虑读者写者模型。

在这里插入图片描述

321原则&#xff1a;

三种关系&#xff1a;


  • 读者 vs 读者&#xff1a;无关系
  • 写者 vs 写者&#xff1a;互斥关系
  • 读者 vs 写者&#xff1a;同步、互斥关系

两种角色&#xff1a;


  • 读者
  • 写者

一个交易场所&#xff1a;


  • 一段缓冲区


基本操作&#xff1a;

设置读者优先&#xff1a;

int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t *attr, int pref);
/*
pref 共有 3 种选择
PTHREAD_RWLOCK_PREFER_READER_NP (默认设置) 读者优先&#xff0c;可能会导致写者饥饿情况
PTHREAD_RWLOCK_PREFER_WRITER_NP 写者优先&#xff0c;目前有 BUG&#xff0c;导致表现行为和
PTHREAD_RWLOCK_PREFER_READER_NP 一致
PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP 写者优先&#xff0c;但写者不能递归加锁
*/

初始化&#xff1a;

int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
const pthread_rwlockattr_t *restrict attr);

销毁&#xff1a;

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






推荐阅读
  • 本文介绍了Python高级网络编程及TCP/IP协议簇的OSI七层模型。首先简单介绍了七层模型的各层及其封装解封装过程。然后讨论了程序开发中涉及到的网络通信内容,主要包括TCP协议、UDP协议和IPV4协议。最后还介绍了socket编程、聊天socket实现、远程执行命令、上传文件、socketserver及其源码分析等相关内容。 ... [详细]
  • Nginx使用AWStats日志分析的步骤及注意事项
    本文介绍了在Centos7操作系统上使用Nginx和AWStats进行日志分析的步骤和注意事项。通过AWStats可以统计网站的访问量、IP地址、操作系统、浏览器等信息,并提供精确到每月、每日、每小时的数据。在部署AWStats之前需要确认服务器上已经安装了Perl环境,并进行DNS解析。 ... [详细]
  • 图解redis的持久化存储机制RDB和AOF的原理和优缺点
    本文通过图解的方式介绍了redis的持久化存储机制RDB和AOF的原理和优缺点。RDB是将redis内存中的数据保存为快照文件,恢复速度较快但不支持拉链式快照。AOF是将操作日志保存到磁盘,实时存储数据但恢复速度较慢。文章详细分析了两种机制的优缺点,帮助读者更好地理解redis的持久化存储策略。 ... [详细]
  • 本文介绍了Web学习历程记录中关于Tomcat的基本概念和配置。首先解释了Web静态Web资源和动态Web资源的概念,以及C/S架构和B/S架构的区别。然后介绍了常见的Web服务器,包括Weblogic、WebSphere和Tomcat。接着详细讲解了Tomcat的虚拟主机、web应用和虚拟路径映射的概念和配置过程。最后简要介绍了http协议的作用。本文内容详实,适合初学者了解Tomcat的基础知识。 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • Webpack5内置处理图片资源的配置方法
    本文介绍了在Webpack5中处理图片资源的配置方法。在Webpack4中,我们需要使用file-loader和url-loader来处理图片资源,但是在Webpack5中,这两个Loader的功能已经被内置到Webpack中,我们只需要简单配置即可实现图片资源的处理。本文还介绍了一些常用的配置方法,如匹配不同类型的图片文件、设置输出路径等。通过本文的学习,读者可以快速掌握Webpack5处理图片资源的方法。 ... [详细]
  • Java序列化对象传给PHP的方法及原理解析
    本文介绍了Java序列化对象传给PHP的方法及原理,包括Java对象传递的方式、序列化的方式、PHP中的序列化用法介绍、Java是否能反序列化PHP的数据、Java序列化的原理以及解决Java序列化中的问题。同时还解释了序列化的概念和作用,以及代码执行序列化所需要的权限。最后指出,序列化会将对象实例的所有字段都进行序列化,使得数据能够被表示为实例的序列化数据,但只有能够解释该格式的代码才能够确定数据的内容。 ... [详细]
  • 本文介绍了如何使用php限制数据库插入的条数并显示每次插入数据库之间的数据数目,以及避免重复提交的方法。同时还介绍了如何限制某一个数据库用户的并发连接数,以及设置数据库的连接数和连接超时时间的方法。最后提供了一些关于浏览器在线用户数和数据库连接数量比例的参考值。 ... [详细]
  • 禁止程序接收鼠标事件的工具_VNC Viewer for Mac(远程桌面工具)免费版
    VNCViewerforMac是一款运行在Mac平台上的远程桌面工具,vncviewermac版可以帮助您使用Mac的键盘和鼠标来控制远程计算机,操作简 ... [详细]
  • 本文主要解析了Open judge C16H问题中涉及到的Magical Balls的快速幂和逆元算法,并给出了问题的解析和解决方法。详细介绍了问题的背景和规则,并给出了相应的算法解析和实现步骤。通过本文的解析,读者可以更好地理解和解决Open judge C16H问题中的Magical Balls部分。 ... [详细]
  • 本文讨论了使用差分约束系统求解House Man跳跃问题的思路与方法。给定一组不同高度,要求从最低点跳跃到最高点,每次跳跃的距离不超过D,并且不能改变给定的顺序。通过建立差分约束系统,将问题转化为图的建立和查询距离的问题。文章详细介绍了建立约束条件的方法,并使用SPFA算法判环并输出结果。同时还讨论了建边方向和跳跃顺序的关系。 ... [详细]
  • 本文介绍了一种划分和计数油田地块的方法。根据给定的条件,通过遍历和DFS算法,将符合条件的地块标记为不符合条件的地块,并进行计数。同时,还介绍了如何判断点是否在给定范围内的方法。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • 本文介绍了Java高并发程序设计中线程安全的概念与synchronized关键字的使用。通过一个计数器的例子,演示了多线程同时对变量进行累加操作时可能出现的问题。最终值会小于预期的原因是因为两个线程同时对变量进行写入时,其中一个线程的结果会覆盖另一个线程的结果。为了解决这个问题,可以使用synchronized关键字来保证线程安全。 ... [详细]
author-avatar
目目目目目目目目目目目目
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有