Linux多线程
为什么要有多线程(实现多线程的目的)
-
提高性能:多线程允许程序同时执行多个任务,充分利用多核CPU,提升计算效率和响应速度。
-
增强响应性:在图形界面应用中,多线程可以避免主线程被长时间任务阻塞,保持界面的流畅响应。
-
简化设计:多线程可以将复杂任务分解为多个独立线程,简化程序结构,便于维护。
-
资源利用:多线程能更高效地利用系统资源,如CPU和内存,减少空闲时间。
-
并行处理:多线程支持同时处理多个任务,适用于需要并行执行的场景,如服务器处理多个客户端请求。
-
实时处理:多线程适合实时系统,能够及时响应外部事件。
-
任务分离:将不同任务分配到不同线程,降低模块间的耦合,提升代码可读性和可维护性。
-
异步操作:多线程支持异步操作,允许任务在后台执行,主线程继续处理其他任务。
为了引入我们后续的多线程的场景,这里首先应该引入生产者-消费者模型
生产者-消费者模型
生产者-消费者模型是一种经典的多线程同步问题,涉及两类线程:生产者和消费者。生产者生成数据并放入共享缓冲区,消费者从缓冲区取出数据进行处理。该模型的核心在于协调两者的操作,确保缓冲区不会溢出或为空。
关键组件
共享缓冲区:很多时候就是任务队列
用于存储生产者生成的数据,供消费者使用。
通常有固定大小,需防止溢出或空取。
生产者:
生成数据并放入缓冲区。
若缓冲区满,生产者需等待消费者取走数据。
消费者:
从缓冲区取出数据并处理。
若缓冲区空,消费者需等待生产者放入新数据。
同步机制:
用于协调生产者和消费者的操作,常用方法包括:
互斥锁(Mutex):确保同一时间只有一个线程访问缓冲区。
** 信号量(Semaphore)**:控制缓冲区的空位和满位。
** 条件变量(Condition Variable)**:在特定条件下唤醒等待的线程。
问什么会有互斥锁,信号量,条件变量的引入,是因为我们在处理多线程的时候,会涉及到很多的临界区的问题,这三种数据结构就是帮助我们进行合理处理临界区的问题
互斥锁
互斥锁在我的上一篇文章中有着详细的描述这里就不在赘述
Linux线程,锁
条件变量
为什么会有条件变量
想象一下生产者消费者模型,生产者和消费者共用一把锁,当任务队列为空时,消费者和生产者同时争取这把锁进入临界区,但是任务队列为空,消费者争取锁来并没有用,反而这个时候我们需要生产者尽快的争取到这把锁,让任务队列不为空,这样才能更加合理的推动生产者-消费者模型的进行。
当我们的任务队列的size == 0的时候我们就让这个时候获取到锁的消费者线程进行休眠,然后当我们的生产者push了新的任务使得任务队列size > 0,再唤醒我们正在休眠的消费者线程。
这样的设计有着很多的好处
- 消费者进入沉睡,暂时不会参与锁的竞争,这样可以使得生产者再想要push任务的时候,可以尽快的获取到锁。
- 避免消费者无效的竞争锁,这本质也是一种资源的损耗
条件变量的接口
pthread_cond_init() // 初始化条件变量
pthread_cond_signal() // 唤醒该条件变量下的一个线程
pthread_cond_broadcast // 唤醒该条件变量下的所有的线程
pthread_cond_wait() // 控制某个线程沉睡在当前的条件变量下
pthread_cond_destroy() // 对条件变量进行析构
所以这里我们引入了条件变量
生产者-消费者模型的应用场景
试想一下,平时我们单线程写代码的时候,我们都是串行执行程序,每个任务(写入文件,更新日志…)都需要上一个任务完全完成之后才能进行,但是如果我们利用生产者-消费者模型,我们可以将调用封装成任务函数,然后push进入任务队列,这样就能够并行的较快的执行任务。
线程池
线程池是一种利用池化技术思想来实现的线程管理技术,主要是为了复用线程、便利地管理线程和任务、并将线程的创建和任务的执行解耦开来。我们可以创建线程池来复用已经创建的线程来降低频繁创建和销毁线程所带来的资源消耗。
我们可以复用一组线程,频繁利用这一组线程完成一系列的问题
准备知识
- std::thread,或者自己封装(基于pthread_t 的接口)
- std::mutex,或者自己封装(基于pthread_mutex_t的接口)
- std::cond_variable,或者自己封装(基于pthread_cond_t的接口)
线程池的具体实现
下面我使用的是c++11线程库std::thread,std::mutex,std::cond_variable的封装的线程池
#pragma once
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <functional>
#include <memory>
#include <vector>
#include <pthread.h>
#include "/home/hrj/include/Log.hpp" // 用绝对路径更加的安全
namespace stlthreadpool
{
using namespace LogModule;
static const int default_thread_size = 5; // 默认创建五个线程作为线程池
class isRunning
{
public:
void set()
{
std::unique_lock<std::mutex> lock(_lock);
_isrunning = true;
}
void reset()
{
std::unique_lock<std::mutex> lock(_lock);
_isrunning = false;
}
bool isRun()
{
std::unique_lock<std::mutex> lock(_lock);
return _isrunning == true;
}
private:
bool _isrunning = false;
std::mutex _lock;
};
using task_t = std::function<void()>;
class threadpool
{
private:
static void scheduleQueue(threadpool*self,const std::string& name)
{
// 通过生产者消费者模型不断地接取任务
while (true)
{
task_t task;
{
std::unique_lock<std::mutex> lock(self->_lock);
while (self->_tq.size() == 0)
{
self->_wait_num++;
LOG(LogLevel::DEBUG) << name << "begin to sleep";
if(self->_isrunning.isRun() == false) return;
self->_cond.wait(lock);
LOG(LogLevel::DEBUG) << name << "wake up";
self->_wait_num--;
if(self->_isrunning.isRun() == false && self->_tq.size() == 0) return;
}
task = self->_tq.front();
self->_tq.pop();
}
task();
}
LOG(LogLevel::DEBUG) << name << "退出成功";
}
threadpool(int threadsize = default_thread_size)
{
for(int i = 0;i < threadsize;i++)
{
_tp.emplace_back(scheduleQueue, this,"thread" + std::to_string(i));
}
}
public:
// 单例模式
static threadpool* getInstance(int num = default_thread_size)
{
return new threadpool(num);
}
template <class Func, class... Args>
void enterQueue(Func &&func, Args &&...args)
{
task_t newtask = std::bind(std::forward<Func>(func), std::forward<Args>(args)...); // 将我们的新任务进行打
{
std::unique_lock<std::mutex> lock(_lock);
_tq.push(newtask);
_cond.notify_one();
}
}
void Stop()
{
_isrunning.reset();
if(_wait_num)
_cond.notify_all();
for(auto& thread : _tp)
{
thread.join();
}
LOG(LogLevel::DEBUG) << "threads join successfully";
}
private:
std::vector<std::thread> _tp; // 线程池
std::queue<task_t> _tq; // 任务队列
std::mutex _lock;
std::condition_variable _cond;
isRunning _isrunning; // 状态
int _wait_num = 0;
};
}
// 通过c++11的库引入我们的线程池
这里利用的关键的技术
- template <class …Args>实现模版多参数
- std::bind方便对函数的类型进行统一成function<void(void)>
- 线程池利用的时候创建,实现了单例的懒汉模式
- std::thread,std::cond_variable,std::mutex的灵活使用
日志的编写
日志
日志系统是记录系统运行过程中各种重要信息的文件系统。它由各进程创建并记录,主要用于记录系统的运行过程及异常信息,为快速定位系统运行中出现的问题及开发过程中的程序调试问题提供详细信息
由于日志可以多个执行流进行操作,因此这里日志我们要进行加锁和解锁的操作,
并且提供打印到文件或者打印到显示器。
#pragma once
#include <iostream>
#include <string>
#include <unistd.h>
#include <unistd.h>
#include "/home/hrj/include/Mutex.hpp"
#include <fstream>
#include <filesystem>
#include <chrono>
#include <iomanip>
#include <memory>
#include <sstream>
namespace LogModule
{
// 1.构建日志文件
const std::string gpath = "./log/";
const std::string defaultlogname = "log.txt";
// 为了实现向不同的文件进行写入这里我们使用多态的性质
using namespace MutexModule;
class LogPathBase
{
public:
virtual void Write(const std::string &) = 0;
}; // 声明为纯虚函数
class LogPathFile : public LogPathBase
{
public:
LogPathFile(const std::string &path = gpath, const std::string &filename = defaultlogname) : _path(path), _filename(filename)
{
}
virtual void Write(const std::string &message) override
{
LockGuard lock(_mtx);
if (!std::filesystem::exists(_path))
std::filesystem::create_directories(_path);
std::ofstream ofs(_path + _filename, std::ios::app); // 通过追加的方式进行写入
if (ofs.is_open() == false)
return;
ofs << message << std::endl;
}
private:
Mutex _mtx;
const std::string _path;
const std::string _filename;
};
class LogPathScreen : public LogPathBase
{
virtual void Write(const std::string &message) override
{
LockGuard lock(_mtx);
std::cout << message << std::endl;
}
private:
Mutex _mtx;
};
// 2. 日志等级
enum class LogLevel
{
DEBUG,
INFO,
WARNING,
ERROR,
FATAL
}; // 定义日志的等级
std::string LevelToString(LogLevel level)
{
switch(level)
{
case LogLevel::DEBUG: return "Debug";
case LogLevel::INFO: return "Info";
case LogLevel::WARNING: return "Warning";
case LogLevel::ERROR: return "Error";
case LogLevel::FATAL: return "Fatal";
}
return "None";
}
// 首先确定日志的格式
// 确定当前的时间
std::string getCurtime()
{
auto now = std::chrono::system_clock::now();
// 转换为time_t格式
std::time_t now_time = std::chrono::system_clock::to_time_t(now);
// 格式化输出
std::stringstream ssm;
ssm << std::put_time(std::localtime(&now_time), "%Y-%m-%d %H:%M:%S");
return ssm.str();
}
class Log
{
public:
Log() : _src(std::make_unique<LogPathScreen>())
{
}
void ChgToScreen() { _src = std::make_unique<LogPathScreen>(); }
void ChgToFile() { _src = std::make_unique<LogPathFile>(); }
// loginfo中存储的使我们想要输出的数据
class LogInfo
{
public:
LogInfo(LogLevel level,const std::string &filename,int line,Log& self):
_level(level),
_curtime(getCurtime()),
_pid(::getpid()),
_filename(filename),
_line(line),
_self(self)
{
std::stringstream ssm;
ssm << '[' << _curtime << ']'\
<< '[' << LevelToString(_level) << ']'\
<< '[' << _filename << ']'\
<< '[' << _line << ']'\
<< '[' << _pid << ']';
__log_all_info = std::move(ssm.str());
}
template <class Val>
LogInfo &operator<<(const Val &arg)
{
std::stringstream ssm;
ssm << arg;
__log_all_info += ssm.str();// 将我们外部读取到的数据添加进入我们的__log_all_info
return *this;
}
~LogInfo()
{
_self._src->Write(__log_all_info);
}
private:
LogLevel _level;
std::string _curtime;
pid_t _pid;
std::string _filename;
int _line;
std::string __log_all_info;
Log& _self;
};
LogInfo operator()(LogLevel level,std::string filename,int line)
{
return LogInfo(level,filename,line,*this);
}
private:
std::unique_ptr<LogPathBase> _src;
};
// 3. 刷新策略
Log log;
#define LOG(type) LogModule::log(type,__FILE__,__LINE__)
#define ENABLE_CONSOLE_LOG() log.ChgToScreen()
#define ENABLE_FILE_LOG() log.ChgToFile()
}
总结
本篇文章主要介绍了如何通过生产者消费者模型实现线程池,以及如何在Linux通过Linux,libpthread.so的动态库实现线程池,同时还加入了日志的编写。