實(shí)用指南:Linux《線程同步和互斥(下)》
在之前的Linux《線程同步和互斥(上)》當(dāng)中我們已經(jīng)了解了線程同步和互斥的基本概念,并且害學(xué)習(xí)了在pthread庫(kù)當(dāng)中提供的實(shí)現(xiàn)線程同步和互斥的接口,并且還試著封裝了這些接口。除此之外我們還了解到了生產(chǎn)消費(fèi)模型。那么在本篇當(dāng)中我們將基于之前實(shí)現(xiàn)的類來(lái)實(shí)現(xiàn)一個(gè)線程池的設(shè)計(jì),并且還會(huì)了解到一種未單例的設(shè)計(jì)模式,了解線程安全相關(guān)的概念。通過(guò)本篇的學(xué)習(xí)會(huì)讓我們對(duì)線程有更深刻的理解,接下來(lái)久開始本篇的學(xué)習(xí)吧!!!

1.線程池
在此我們要實(shí)現(xiàn)一個(gè)線程池,用戶可以將對(duì)應(yīng)的任務(wù)插入到線程池,之后線程池就會(huì)調(diào)度對(duì)應(yīng)的線程來(lái)執(zhí)行對(duì)應(yīng)的任務(wù),用戶無(wú)需關(guān)心具體的調(diào)度是什么樣的,只需要將任務(wù)傳給線程池即可。在此在實(shí)現(xiàn)線程池需要我們將對(duì)應(yīng)的線程接口進(jìn)行封裝,條件變量和鎖進(jìn)行封裝,還需要實(shí)現(xiàn)對(duì)應(yīng)的日志。以上的封裝的要求在之前的線程學(xué)習(xí)當(dāng)中我已經(jīng)實(shí)現(xiàn)了,但是還有日志的功能為實(shí)現(xiàn),那么接下來(lái)就先來(lái)實(shí)現(xiàn)日志。
1.1 日志和策略模式
IT行業(yè)這么火, 涌入的?很多. 俗話說(shuō)林子大了啥鳥都有。大佬和菜雞們兩極分化的越來(lái)越嚴(yán)重. 為了讓菜雞們不太拖大佬的后腿, 于是?佬們針對(duì)?些經(jīng)典的常見的場(chǎng)景, 給定了?些對(duì)應(yīng)的解決方案, 這個(gè)就是 設(shè)計(jì)模式。
在之前的學(xué)習(xí)當(dāng)中我們了解到的生產(chǎn)者消費(fèi)者模式、責(zé)任鏈模式、建造者模式等都是設(shè)計(jì)模式當(dāng)中的一種,在接下來(lái)日志的學(xué)習(xí)當(dāng)中我將了解到一種新的設(shè)計(jì)模式——策略模式。
在此在設(shè)計(jì)日志之前先來(lái)了解一下日志相關(guān)的概念以及日志的作用:
在計(jì)算機(jī)當(dāng)中日志是進(jìn)行系統(tǒng)和軟件記錄的重要文件,主要作用就是進(jìn)行監(jiān)控運(yùn)行的狀態(tài)、記錄異常的信息,從而讓程序員能更容易地定位到出現(xiàn)地地方,它是進(jìn)行系統(tǒng)維護(hù)、故障排查地重要工具。
一般在日志當(dāng)中需要包含以下的信息:
1.時(shí)間戳
2.日志等級(jí)
3.日志內(nèi)容
除了以上的信息之外還可以包含以下的內(nèi)容:
文件的行號(hào) 進(jìn)程或者線程ID等信息
實(shí)際上日志是有現(xiàn)成的解決方法的,但是面前是我們第一次接觸日志,那么這時(shí)采取的是自定義日志的方案。
在此我們要實(shí)現(xiàn)的日志形式如下所示:
[可讀性很好的時(shí)間] [?志等級(jí)] [進(jìn)程pid] [打印對(duì)應(yīng)?志的?件名][?號(hào)] - 消息內(nèi)容,?持可變參數(shù)
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [16] - hello world
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [17] - hello world
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [18] - hello world
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [20] - hello world
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [21] - hello world
[2024-08-04 12:27:03] [WARNING] [202938] [main.cc] [23] - hello world
以上在了解了日志相關(guān)的概念之后接下來(lái)就試著來(lái)實(shí)現(xiàn)我們自己的日志代碼,一般來(lái)說(shuō)日志的形成之后都是要生成到對(duì)應(yīng)的文件當(dāng)中,但是當(dāng)前在我們的程序當(dāng)中一般默認(rèn)是將日志輸出到顯示器上,只有當(dāng)用戶指定輸出到對(duì)應(yīng)的文件的時(shí)候才會(huì)將日志進(jìn)行文件的保存,實(shí)際上這種將執(zhí)行的方式提供給用戶進(jìn)行選擇的就是策略模式。
策略模式本質(zhì)上就是:提供多種可選的執(zhí)行方式(策略),用戶在運(yùn)行時(shí)選擇其中一種,程序就按對(duì)應(yīng)的策略去執(zhí)行。
那么接下來(lái)就要思考如何實(shí)現(xiàn)以上的要求,實(shí)際上我們只需要實(shí)現(xiàn)一個(gè)日志儲(chǔ)存的基類,之后再生成不同日志存儲(chǔ)的派生類即可。
在此文件的操作我們可以使用原來(lái)系統(tǒng)當(dāng)中提供的系統(tǒng)調(diào)用,但是在此我們使用到了C++17當(dāng)中了文件以及目錄的操作,使用起來(lái)更簡(jiǎn)便一些。
// 每個(gè)日志之間的分隔符
const std::string gsep = "\r\n";
// 日志基類
class LogStrategy
{
public:
LogStrategy() = default;
// 日志儲(chǔ)存到指定的文件當(dāng)中
virtual void Synclog(const std::string &message) = 0;
};
// 將日志儲(chǔ)存到顯示器文件當(dāng)中
class ConsoleLogStrategy : public LogStrategy
{
public:
ConsoleLogStrategy()
{
}
~ConsoleLogStrategy()
{
}
void Synclog(const std::string &message) override
{
// 避免出現(xiàn)不一致問(wèn)題對(duì)當(dāng)前的寫入操作進(jìn)行加鎖
LockGuard lock(_mutex);
std::cout << message << gsep;
}
private:
Mutex _mutex;
};
// 寫入到文件當(dāng)中默認(rèn)寫入的文件
const std::string defaultfile = "my.log";
const std::string defaultpath = "./log";
class FileLogstrategy : public LogStrategy
{
public:
FileLogstrategy(const std::string &path = defaultpath, const std::string &file = defaultfile)
: _path(path),
_file(file)
{
// 多當(dāng)前的操作進(jìn)行加鎖
LockGuard lock(_mutex);
// 判斷當(dāng)前的目錄是否存在
if (std::filesystem::exists(_path))
{
return;
}
// 不存在的時(shí)候就創(chuàng)建對(duì)應(yīng)的目錄
try
{
std::filesystem::create_directories(_path);
}
catch (const std::filesystem::filesystem_error &e)
{
std::cerr << e.what() << "\n";
}
}
~FileLogstrategy()
{
}
// 進(jìn)行日志的寫入
void Synclog(const std::string &message) override
{
LockGuard lock(_mutex);
// 將文件的路徑調(diào)整為./log/my.log的形式
std::string filename = _path + (_path.back() == '/' ? "" : "/") + _file;
// 以追加的方式打開對(duì)應(yīng)的日志文件
std::ofstream f(filename, std::ios::app);
if (!f.is_open())
{
return;
}
// 將日志的寫入到文件當(dāng)中
f << message << "gsep";
f.close();
}
private:
// 日志文件名
std::string _file;
// 日志文件路徑
std::string _path;
// 鎖
Mutex _mutex;
};
實(shí)現(xiàn)了以上日志的儲(chǔ)存代碼之后接下來(lái)就需要來(lái)實(shí)現(xiàn)日志具體的實(shí)現(xiàn),首先來(lái)實(shí)現(xiàn)的就是日志等級(jí)的實(shí)現(xiàn),在此使用枚舉類型來(lái)實(shí)現(xiàn),但是在輸出的時(shí)候我們是要輸出字符串,因此就需要再實(shí)現(xiàn)將對(duì)應(yīng)的枚舉類型轉(zhuǎn)換為字符串的函數(shù)。
//日志等級(jí)
enum class LogLevel
{
DEBUG,//調(diào)試
INFO,//信息
WARNING,//警告
ERROR,//錯(cuò)誤
FATAL//致命錯(cuò)誤
};
//將對(duì)應(yīng)的日志等級(jí)轉(zhuǎn)換為對(duì)應(yīng)的字符串
std::string Leveltostr(LogLevel level)
{
switch (level)
{
case LogLevel::DEBUG:
return "DEBUG";
case LogLevel::ERROR:
return "ERROR";
case LogLevel::FATAL:
return "FATAL";
case LogLevel::INFO:
return "INFO";
case LogLevel::WARNING:
return "WARNING";
default:
return "NONE";
}
}
接下來(lái)再來(lái)實(shí)現(xiàn)日志當(dāng)中的當(dāng)前的時(shí)間,我知道使用time是可以獲得到當(dāng)前的時(shí)間戳的,但是面前的問(wèn)題是我們是需要將在日志輸出 [年-月-日 時(shí):分:秒]的格式,那么這就需要我們進(jìn)行時(shí)間戳到時(shí)間的轉(zhuǎn)換,實(shí)際上在系統(tǒng)當(dāng)中提供了時(shí)間戳到格式化時(shí)間的轉(zhuǎn)換函數(shù),如下所示:
#include
struct tm *localtime_r(const time_t *timep, struct tm *result);
參數(shù):
timep:要進(jìn)行裝換的時(shí)間戳
result:轉(zhuǎn)換之后保存的tm結(jié)構(gòu)體
struct tm內(nèi)成員變量如下所示:
struct tm
{
int tm_sec; /* Seconds. [0-60] (1 leap second) */
int tm_min; /* Minutes. [0-59] */
int tm_hour; /* Hours. [0-23] */
int tm_mday; /* Day. [1-31] */
int tm_mon; /* Month. [0-11] */
int tm_year; /* Year - 1900. */
int tm_wday; /* Day of week. [0-6] */
int tm_yday; /* Days in year.[0-365] */
int tm_isdst; /* DST. [-1/0/1]*/
//……
};
有了以上的函數(shù)之后接下來(lái)就可以將時(shí)間戳裝換為對(duì)應(yīng)的tm結(jié)構(gòu)體,之后再根據(jù)結(jié)構(gòu)體當(dāng)中的變量裝換的為指定格式的字符串,集體實(shí)現(xiàn)的代碼如下所示:
//獲取當(dāng)前的時(shí)間戳,并且裝換為指定的格式 2025-10-04 14:38:16
std::string GetTime()
{
time_t cur = time(nullptr);
//格式化時(shí)間戳的結(jié)構(gòu)體tm
struct tm cur_tm;
//進(jìn)行轉(zhuǎn)換
localtime_r(&cur, &cur_tm);
char timebuffer[128];
//將時(shí)間格式化輸出
snprintf(timebuffer, sizeof(timebuffer), "%4d-%02d-%02d %02d:%02d:%02d",
cur_tm.tm_year + 1900,
cur_tm.tm_mon + 1,
cur_tm.tm_mday,
cur_tm.tm_hour,
cur_tm.tm_min,
cur_tm.tm_sec);
return timebuffer;
}
實(shí)現(xiàn)了以上日志實(shí)現(xiàn)的前提工作之后接下來(lái)就可以試著來(lái)實(shí)現(xiàn)日志類的,實(shí)現(xiàn)的代碼如下所示:
// 日志類
class Logger
{
public:
// 默認(rèn)是將日志寫入到顯示器文件當(dāng)中
Logger()
{
EnableConsoleLogStategy();
}
// 將日志寫入到指定的文件當(dāng)中
void EnableFileLogStategy()
{
_fflush_strategy = std::make_unique();
}
// 將日志寫入到顯示器文件當(dāng)中
void EnableConsoleLogStategy()
{
_fflush_strategy = std::make_unique();
}
// 日志消息類
class LogMessage
{
public:
// 日志消息構(gòu)造函數(shù),將[可讀性很好的時(shí)間] [?志等級(jí)] [進(jìn)程pid] [打印對(duì)應(yīng)?志的?件名][?號(hào)] -寫入到——loginfo字符串當(dāng)中
LogMessage(LogLevel &level, std::string &src_name, int line_number, Logger &logger)
: _curr_time(GetTime()),
_level(level),
_pid(getpid()),
_src_name(src_name),
_line_number(line_number),
_logger(logger)
{
std::stringstream ss;
ss << "[" << _curr_time << "]"
<< "[" << Leveltostr(_level) << "]"
<< "[" << _pid << "]"
<< "[" << _src_name << "]"
<< "[" << _line_number << "]"
<< "- ";
_loginfo = ss.str();
}
// 日志消息當(dāng)中的<<運(yùn)算符重載函數(shù)
template
LogMessage &operator<<(const T &info)
{
// 將info字符串內(nèi)的內(nèi)容追加到_loginfo內(nèi)
std::stringstream ss;
ss << info;
_loginfo += ss.str();
return *this;
}
// 當(dāng)Logmessage析構(gòu)的時(shí)候?qū)loginfo寫入到對(duì)應(yīng)的文件當(dāng)中
~LogMessage()
{
if (_logger._fflush_strategy)
{
_logger._fflush_strategy->Synclog(_loginfo);
}
}
private:
// 日志發(fā)生時(shí)間
std::string _curr_time;
// 日志等級(jí)
LogLevel _level;
// 進(jìn)程pid
pid_t _pid;
// 發(fā)生所處的文件名
std::string _src_name;
// 文件的行號(hào)
int _line_number;
// 保存日志的字符串
std::string _loginfo;
Logger &_logger;
};
// 日志類當(dāng)中的仿函數(shù)
LogMessage operator()(LogLevel level, std::string name, int line)
{
// 將日志的類型、發(fā)生的文件名、文件的行號(hào)等傳給文件消息對(duì)象
return LogMessage(level, name, line, *this);
}
private:
// 日志策略
std::unique_ptr _fflush_strategy;
};
實(shí)際上以上實(shí)現(xiàn)日志基本的思路就是首先先創(chuàng)建出一個(gè)Log的對(duì)象,用戶可以通過(guò)對(duì)應(yīng)的函數(shù)調(diào)用來(lái)選擇日志輸出的目的地,之后用戶通過(guò)仿函數(shù)來(lái)向日志傳入對(duì)應(yīng)的日志等級(jí)等屬性,傳入之后構(gòu)造出對(duì)應(yīng)的LogMessage對(duì)象,接下來(lái)就通過(guò)用戶使用<<將日志的內(nèi)容輸入,最后將格式化之后的字符串通過(guò)調(diào)用Synclog來(lái)實(shí)現(xiàn)日志最后的寫入。
以上我們就實(shí)現(xiàn)的對(duì)應(yīng)的日志代碼,接下來(lái)就進(jìn)行測(cè)試看實(shí)現(xiàn)的是否滿足我們的要求,代碼如下所示:
#include
#include"log.hpp"
using namespace LogModule;
int main()
{
Logger log;
log.EnableConsoleLogStategy();
log(LogLevel::DEBUG,"test.cc",11)<<"日志測(cè)試1";
log(LogLevel::WARNING,"test.cc",12)<<"日志測(cè)試2";
return 0;
}
編譯以上的程序輸出的結(jié)果如下所示:
通過(guò)輸出的結(jié)果可以看出是符合我們的要求的,但是面前問(wèn)題是目前的日志使用起來(lái)還是較為繁瑣,需要我們先創(chuàng)建出對(duì)應(yīng)的Loggerd對(duì)象之后在輸出日志的時(shí)候還需要傳當(dāng)前代碼文件的文件名和行號(hào),這樣使用起來(lái)還是稍顯麻煩,那么在log.hpp當(dāng)中就使用一些宏來(lái)讓日志的使用更為簡(jiǎn)便。
Logger logger;
#define LOG(level) logger(level, __FILE__, __LINE__)
#define Enable_Console_log_Strategy() logger.EnableConsoleLogStategy()
#define Enable_File_log_Strategy() logger.EnableFileLogStategy()
有了以上的之后接下來(lái)就可以按照一下的方式進(jìn)行日志的輸出:
#include
#include"log.hpp"
using namespace LogModule;
int main()
{
Enable_Console_log_Strategy();
LOG(LogLevel::DEBUG)<<"日志測(cè)試1";
LOG(LogLevel::WARNING)<<"日志測(cè)試2";
return 0;
}
日志實(shí)現(xiàn)完整代碼
實(shí)現(xiàn)的日志的代碼如下所示:
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include "Mutex.hpp"
using namespace MutexNamespace;
namespace LogModule
{
// 每個(gè)日志之間的分隔符
const std::string gsep = "\r\n";
// 日志基類
class LogStrategy
{
public:
LogStrategy() = default;
// 日志儲(chǔ)存到指定的文件當(dāng)中
virtual void Synclog(const std::string &message) = 0;
};
// 將日志儲(chǔ)存到顯示器文件當(dāng)中
class ConsoleLogStrategy : public LogStrategy
{
public:
ConsoleLogStrategy()
{
}
~ConsoleLogStrategy()
{
}
void Synclog(const std::string &message) override
{
// 避免出現(xiàn)不一致問(wèn)題對(duì)當(dāng)前的寫入操作進(jìn)行加鎖
LockGuard lock(_mutex);
std::cout << message << gsep;
}
private:
Mutex _mutex;
};
// 寫入到文件當(dāng)中默認(rèn)寫入的文件
const std::string defaultfile = "my.log";
const std::string defaultpath = "./log";
class FileLogstrategy : public LogStrategy
{
public:
FileLogstrategy(const std::string &path = defaultpath, const std::string &file = defaultfile)
: _path(path),
_file(file)
{
// 多當(dāng)前的操作進(jìn)行加鎖
LockGuard lock(_mutex);
// 判斷當(dāng)前的目錄是否存在
if (std::filesystem::exists(_path))
{
return;
}
// 不存在的時(shí)候就創(chuàng)建對(duì)應(yīng)的目錄
try
{
std::filesystem::create_directories(_path);
}
catch (const std::filesystem::filesystem_error &e)
{
std::cerr << e.what() << "\n";
}
}
~FileLogstrategy()
{
}
// 進(jìn)行日志的寫入
void Synclog(const std::string &message) override
{
LockGuard lock(_mutex);
// 將文件的路徑調(diào)整為./log/my.log的形式
std::string filename = _path + (_path.back() == '/' ? "" : "/") + _file;
// 以追加的方式打開對(duì)應(yīng)的日志文件
std::ofstream f(filename, std::ios::app);
if (!f.is_open())
{
return;
}
// 將日志的寫入到文件當(dāng)中
f << message << "gsep";
f.close();
}
private:
// 日志文件名
std::string _file;
// 日志文件路徑
std::string _path;
// 鎖
Mutex _mutex;
};
// 日志等級(jí)
enum class LogLevel
{
DEBUG, // 調(diào)試
INFO, // 信息
WARNING, // 警告
ERROR, // 錯(cuò)誤
FATAL // 致命錯(cuò)誤
};
// 將對(duì)應(yīng)的日志等級(jí)轉(zhuǎn)換為對(duì)應(yīng)的字符串
std::string Leveltostr(LogLevel level)
{
switch (level)
{
case LogLevel::DEBUG:
return "DEBUG";
case LogLevel::ERROR:
return "ERROR";
case LogLevel::FATAL:
return "FATAL";
case LogLevel::INFO:
return "INFO";
case LogLevel::WARNING:
return "WARNING";
default:
return "NONE";
}
}
// 獲取當(dāng)前的時(shí)間戳,并且裝換為指定的格式 2025-10-04 14:38:16
std::string GetTime()
{
time_t cur = time(nullptr);
// 格式化時(shí)間戳的結(jié)構(gòu)體tm
struct tm cur_tm;
// 進(jìn)行轉(zhuǎn)換
localtime_r(&cur, &cur_tm);
char timebuffer[128];
// 將時(shí)間格式化輸出
snprintf(timebuffer, sizeof(timebuffer), "%4d-%02d-%02d %02d:%02d:%02d",
cur_tm.tm_year + 1900,
cur_tm.tm_mon + 1,
cur_tm.tm_mday,
cur_tm.tm_hour,
cur_tm.tm_min,
cur_tm.tm_sec);
return timebuffer;
}
// 日志類
class Logger
{
public:
// 默認(rèn)是將日志寫入到顯示器文件當(dāng)中
Logger()
{
EnableConsoleLogStategy();
}
// 將日志寫入到指定的文件當(dāng)中
void EnableFileLogStategy()
{
_fflush_strategy = std::make_unique();
}
// 將日志寫入到顯示器文件當(dāng)中
void EnableConsoleLogStategy()
{
_fflush_strategy = std::make_unique();
}
// 日志消息類
class LogMessage
{
public:
// 日志消息構(gòu)造函數(shù),將[可讀性很好的時(shí)間] [?志等級(jí)] [進(jìn)程pid] [打印對(duì)應(yīng)?志的?件名][?號(hào)] -寫入到——loginfo字符串當(dāng)中
LogMessage(LogLevel &level, std::string &src_name, int line_number, Logger &logger)
: _curr_time(GetTime()),
_level(level),
_pid(getpid()),
_src_name(src_name),
_line_number(line_number),
_logger(logger)
{
std::stringstream ss;
ss << "[" << _curr_time << "]"
<< "[" << Leveltostr(_level) << "]"
<< "[" << _pid << "]"
<< "[" << _src_name << "]"
<< "[" << _line_number << "]"
<< "- ";
_loginfo = ss.str();
}
// 日志消息當(dāng)中的<<運(yùn)算符重載函數(shù)
template
LogMessage &operator<<(const T &info)
{
// 將info字符串內(nèi)的內(nèi)容追加到_loginfo內(nèi)
std::stringstream ss;
ss << info;
_loginfo += ss.str();
return *this;
}
// 當(dāng)Logmessage析構(gòu)的時(shí)候?qū)loginfo寫入到對(duì)應(yīng)的文件當(dāng)中
~LogMessage()
{
if (_logger._fflush_strategy)
{
_logger._fflush_strategy->Synclog(_loginfo);
}
}
private:
// 日志發(fā)生時(shí)間
std::string _curr_time;
// 日志等級(jí)
LogLevel _level;
// 進(jìn)程pid
pid_t _pid;
// 發(fā)生所處的文件名
std::string _src_name;
// 文件的行號(hào)
int _line_number;
// 保存日志的字符串
std::string _loginfo;
Logger &_logger;
};
// 日志類當(dāng)中的仿函數(shù)
LogMessage operator()(LogLevel level, std::string name, int line)
{
// 將日志的類型、發(fā)生的文件名、文件的行號(hào)等傳給文件消息對(duì)象
return LogMessage(level, name, line, *this);
}
private:
// 日志策略
std::unique_ptr _fflush_strategy;
};
Logger logger;
#define LOG(level) logger(level, __FILE__, __LINE__)
#define Enable_Console_log_Strategy() logger.EnableConsoleLogStategy()
#define Enable_File_log_Strategy() logger.EnableFileLogStategy()
}
1.2 線程池實(shí)現(xiàn)
以上我們實(shí)現(xiàn)了日志之后就可以將線程池之前的準(zhǔn)備工作都完成了,那么接下來(lái)就可以來(lái)實(shí)現(xiàn)線程池了,但是在實(shí)現(xiàn)之前還是先來(lái)了解一下線程池具體的概念。
線程池的概念如下所示:
?種線程使用模式。線程過(guò)多會(huì)帶來(lái)調(diào)度開銷,進(jìn)而影響緩存局部性和整體性能。而線程池維護(hù)著多個(gè)線程,等待著監(jiān)督管理者分配可并發(fā)執(zhí)行的任務(wù)。這避免了在處理短時(shí)間任務(wù)時(shí)創(chuàng)建與銷毀線程的代價(jià)。線程池不僅能夠保證內(nèi)核的充分利?,還能防止過(guò)分調(diào)度。可?線程數(shù)量應(yīng)該取決于可用的并發(fā)處理器、處理器內(nèi)核、內(nèi)存、?絡(luò)sockets等的數(shù)量。
線程池適用的場(chǎng)景如下所示:
1.需要大量的線程來(lái)完成任務(wù),且完成任務(wù)的時(shí)間比較短。 比如WEB服務(wù)器完成網(wǎng)頁(yè)請(qǐng)求這樣的任務(wù),使?線程池技術(shù)是非常合適的。因?yàn)閱蝹€(gè)任務(wù)小,而任務(wù)數(shù)量巨大,你可以想象?個(gè)熱門?站的點(diǎn)擊次數(shù)。 但對(duì)于長(zhǎng)時(shí)間的任務(wù),比如?個(gè)Telnet連接請(qǐng)求,線程池的優(yōu)點(diǎn)就不明顯了。因?yàn)門elnet會(huì)話時(shí)間?線程的創(chuàng)建時(shí)間?多了。2.對(duì)性能要求苛刻的應(yīng)用,比如要求服務(wù)器迅速響應(yīng)客戶請(qǐng)求。
3.接受突發(fā)性的大量請(qǐng)求,但不?于使服務(wù)器因此產(chǎn)生大量線程的應(yīng)?。突發(fā)性大量客戶請(qǐng)求,在沒(méi)有線程池情況下,將產(chǎn)?大量線程,雖然理論上大部分操作系統(tǒng)線程數(shù)?最?值不是問(wèn)題,短時(shí)間內(nèi)產(chǎn)?大量線程可能使內(nèi)存到達(dá)極限,出現(xiàn)錯(cuò)誤。
線程池的種類
a. 創(chuàng)建固定數(shù)量線程池,循環(huán)從任務(wù)隊(duì)列中獲取任務(wù)對(duì)象,獲取到任務(wù)對(duì)象后,執(zhí)行任務(wù)對(duì)象中的任務(wù)接口。
b. 浮動(dòng)線程池,除了線程吃當(dāng)中的線程個(gè)數(shù)不是固定的其他和固定線程池一樣。
實(shí)際上線程池也是一個(gè)生產(chǎn)者消費(fèi)者模型,線程池當(dāng)中的任務(wù)隊(duì)列就是交易場(chǎng)所,各個(gè)線程就是消費(fèi)者,將任務(wù)插入到隊(duì)列當(dāng)中的用戶就是消費(fèi)者。

了解了線程池的相關(guān)的概念之后接下來(lái)就可以來(lái)就可以來(lái)實(shí)現(xiàn)線程池的代碼:
#include
#include
#include
#include
#include "log.hpp"
#include "cond.hpp"
#include "Mutex.hpp"
#include "Thread.hpp"
namespace ThreadPoolModule
{
//引入之前實(shí)現(xiàn)的封裝的互斥鎖、條件變量、線程等
using namespace CondNamespace;
using namespace MutexNamespace;
using namespace ThreadModlue;
using namespace LogModule;
//線程池當(dāng)中默認(rèn)的線程個(gè)數(shù)
static const int gnum = 5;
//線程池類
template
class ThreadPool
{
private:
//喚醒所有的線程
void WakeUpAllThread()
{
LockGuard lock(_mutex);
if (_sleepernum)
_cond.Broadcast();
LOG(LogLevel::INFO) << "喚醒所有休眠線程";
}
//喚醒一個(gè)線程
void WakeUpoOne()
{
_cond.Signal();
LOG(LogLevel::INFO) << "喚醒一個(gè)休眠線程";
}
public:
//線程池構(gòu)造函數(shù)
ThreadPool(int num = gnum)
: _num(num),
_isrunning(false),
_sleepernum(0)
{
for (int i = 0; i < num; i++)
{
_threads.emplace_back(
[this]()
{
HandlerTask();
});
}
}
//啟動(dòng)線程池
void Start()
{
if (_isrunning)
return;
_isrunning = true;
for (auto &x : _threads)
{
x.Start();
LOG(LogLevel::INFO) << "start new thread success";
}
}
//暫停線程池
void Stop()
{
if (!_isrunning)
return;
_isrunning = false;
//喚醒所有線程
WakeUpAllThread();
}
//進(jìn)行線程等待
void Join()
{
for (auto &thrad : _threads)
{
thrad.Join();
}
}
//線程執(zhí)行任務(wù)
void HandlerTask()
{
char name[128];
pthread_getname_np(pthread_self(), name, sizeof(name));
while (true)
{
T t;
{
LockGuard lock(_mutex);
//當(dāng)前任務(wù)隊(duì)列為空并且線程還在運(yùn)行狀態(tài)
while (_taskq.empty() && _isrunning)
{
_sleepernum++;
//進(jìn)行將當(dāng)前線程進(jìn)行休眠
_cond.Wait(_mutex);
_sleepernum--;
}
//當(dāng)前線程池不在運(yùn)行狀態(tài)并且任務(wù)隊(duì)列為空
if (!_isrunning && _taskq.empty())
{
//讓當(dāng)前的線程結(jié)束運(yùn)行
LOG(LogLevel::INFO) << name << "退出了,線程池退出或者任務(wù)隊(duì)列為空";
break;
}
//能走到這里表明任務(wù)隊(duì)列當(dāng)中一定有剩余沒(méi)有取出的任務(wù)
//從任務(wù)隊(duì)列當(dāng)中當(dāng)中獲取任務(wù)
t = _taskq.front();
_taskq.pop();
}
//執(zhí)行任務(wù),在執(zhí)行任務(wù)過(guò)程中線程之間不存在互斥的關(guān)系,不需要進(jìn)行加鎖
t();
}
}
//向線程池當(dāng)中插入任務(wù)
bool Enqueue(const T &in)
{
if (_isrunning)
{
//將任務(wù)添加到任務(wù)隊(duì)列當(dāng)中
LockGuard lock(_mutex);
_taskq.push(in);
//當(dāng)前存在休眠的線程,就喚醒一個(gè)線程
if ( _sleepernum>0)
{
WakeUpoOne();
}
return true;
}
return false;
}
~ThreadPool()
{
}
private:
//存儲(chǔ)線程的數(shù)組
std::vector _threads;
//線程池當(dāng)中的線程個(gè)數(shù)
int _num;
//任務(wù)隊(duì)列
std::queue _taskq;
//條件變量
Cond _cond;
//互斥鎖
Mutex _mutex;
//當(dāng)前線程池是否運(yùn)行標(biāo)志位
bool _isrunning;
//線程池當(dāng)中處于休眠的線程個(gè)數(shù)
int _sleepernum;
};
}
以上我們實(shí)現(xiàn)出了線程池對(duì)應(yīng)的代碼,那么接下來(lái)就使用以上的線程池來(lái)進(jìn)行一些任務(wù)的處理,在此創(chuàng)建一個(gè)Task.hpp的類,在該類當(dāng)中實(shí)現(xiàn)對(duì)應(yīng)的任務(wù):
#pragma once
#include"log.hpp"
#include
using namespace LogModule;
using func_t =std::function;
void Download()
{
LOG(LogLevel::DEBUG)<<"下載任務(wù)正在進(jìn)行";
}
在mian.cc當(dāng)中向線程池當(dāng)中插入對(duì)應(yīng)的Download任務(wù)。
#include"ThreadPool.hpp"
#include"Task.hpp"
using namespace ThreadPoolModule;
int main()
{
ThreadPool* t=new ThreadPool();
t->Start();
int count=10;
while(count)
{
t->Enqueue(Download);
sleep(1);
count--;
}
sleep(1);
t->Stop();
t->Join();
return 0;
}
通過(guò)輸出的結(jié)果就可以看出實(shí)現(xiàn)的線程池是沒(méi)有問(wèn)題的。

3.3 單例模式
某些類在實(shí)例化的過(guò)程當(dāng)中只能有一個(gè)對(duì)象,那么就將這種模式稱為單例模式。
那么這時(shí)候就要思考為什么在實(shí)例化的過(guò)程當(dāng)中只能有一個(gè)對(duì)象?
其實(shí)一些數(shù)據(jù)的內(nèi)容是非常多的,創(chuàng)建一次對(duì)應(yīng)的對(duì)象就需要大量的內(nèi)存以及時(shí)間,這時(shí)候?qū)?shí)例化只能進(jìn)行一次就更加的合理,同時(shí)在此使用單例模式能讓系統(tǒng)的接口更加的規(guī)范,保證只能有一個(gè)接口進(jìn)行訪問(wèn)。
實(shí)際上在單例模式當(dāng)中是可以劃分為兩種模式的,分別是懶漢模式和餓漢模式
以下是兩個(gè)模式具體的區(qū)別:
懶漢模式(Lazy Singleton)
特點(diǎn):實(shí)例在第一次使用時(shí)才創(chuàng)建。
優(yōu)點(diǎn):節(jié)省內(nèi)存資源,如果程序運(yùn)行過(guò)程中根本沒(méi)有用到該對(duì)象,就不會(huì)占用空間。
缺點(diǎn):實(shí)現(xiàn)上要考慮 線程安全,否則在多線程環(huán)境可能會(huì)出現(xiàn)多個(gè)實(shí)例。
就像 點(diǎn)外賣。你餓了才點(diǎn),外賣才開始做。
好處:不浪費(fèi),如果你根本沒(méi)餓,就不用做飯。
壞處:等飯的時(shí)間可能比較久。
餓漢模式(Eager Singleton)
特點(diǎn):實(shí)例在程序啟動(dòng)時(shí)就已經(jīng)創(chuàng)建好。
優(yōu)點(diǎn):實(shí)現(xiàn)簡(jiǎn)單,天然線程安全(因?yàn)轭惣虞d時(shí)就創(chuàng)建完成)。
缺點(diǎn):即使程序運(yùn)行過(guò)程中沒(méi)用到這個(gè)對(duì)象,也會(huì)一直占用內(nèi)存。
就像 提前做好便當(dāng)。你不管今天餓不餓,早上就把飯做好了。
好處:想吃的時(shí)候馬上有。
壞處:可能浪費(fèi)(如果你不吃,這頓飯就浪費(fèi)了)。
在代碼當(dāng)中懶漢模式和餓漢模式的具體的區(qū)別就是:在懶漢當(dāng)中是會(huì)一開始加載的時(shí)候就將對(duì)應(yīng)的對(duì)象實(shí)例化,懶漢模式當(dāng)中一開始在加載的時(shí)候未進(jìn)行實(shí)例化而是等到第一次使用的時(shí)候再進(jìn)行實(shí)例化。
例如以下的示例:
//餓漢模式實(shí)現(xiàn)單例
template
class Singleton
{
static T data;
public:
static T* GetInstance()
{
return &data;
}
};
//懶漢模式實(shí)現(xiàn)單例
template
class Singleton
{
static T* inst;
public:
static T* GetInstance()
{
if (inst == NULL)
{
inst = new T();
}
return inst;
}
};
但是以上懶漢模式實(shí)現(xiàn)實(shí)際上還是存在問(wèn)題的,那么就是當(dāng)前可能會(huì)存在兩個(gè)線程同時(shí)進(jìn)入到臨界區(qū)當(dāng)中,那么這時(shí)候久會(huì)出現(xiàn)創(chuàng)建出兩個(gè)對(duì)象的問(wèn)題。那么這時(shí)候就需要使用鎖來(lái)避免以上的問(wèn)題。
// 懶漢模式, 線程安全
template
class Singleton
{
volatile static T* inst; // 需要設(shè)置 volatile 關(guān)鍵字, 否則可能被編譯器優(yōu)化.
static std::mutex lock;
public:
static T* GetInstance()
{
if (inst == NULL)
{ // 雙重判定空指針, 降低鎖沖突的概率, 提?性能.
lock.lock(); // 使?互斥鎖, 保證多線程情況下也只調(diào)??次 new.
if (inst == NULL)
{
inst = new T();
}
lock.unlock();
}
return inst;
}
}
以上就了解了單例模式的概念,那么接下來(lái)就可以試著將以上外賣實(shí)現(xiàn)的線程池修改為單例模式。
首先我們需要做的是將構(gòu)造函數(shù)私有化,接下來(lái)在實(shí)現(xiàn)一個(gè)靜態(tài)成員函數(shù),讓用戶能得到對(duì)應(yīng)對(duì)象的指針,并且還需要實(shí)現(xiàn)一個(gè)靜態(tài)成員的鎖來(lái)保證用戶在獲取的過(guò)程當(dāng)中是互斥的。
template
class ThreadPool
{
//……
public:
static ThreadPool *GwetInstance()
{
LOG(LogLevel::INFO) << "獲取單例";
if (_pthread == nullptr)
{
LockGuard lock(_lock);
if (_pthread == nullptr)
{
LOG(LogLevel::INFO) << "創(chuàng)建一個(gè)單例";
_pthread = new ThreadPool();
_pthread->Start();
}
}
return _pthread;
}
private:
//……
static ThreadPool *_pthread;
static Mutex _lock;
};
template
ThreadPool *ThreadPool::_pthread = nullptr;
template
Mutex ThreadPool::_lock;
2. 線程安全和重入問(wèn)題
以下是兩個(gè)的概念:
線程安全:就是多個(gè)線程在訪問(wèn)共享資源時(shí),能夠正確地執(zhí)行,不會(huì)相互干擾或破壞彼此的執(zhí)行結(jié)果。?般而言,多個(gè)線程并發(fā)同?段只有局部變量的代碼時(shí),不會(huì)出現(xiàn)不同的結(jié)果。但是對(duì)全局變量或者靜態(tài)變量進(jìn)?操作,并且沒(méi)有鎖保護(hù)的情況下,容易出現(xiàn)該問(wèn)題。
重入:同?個(gè)函數(shù)被不同的執(zhí)行流調(diào)用,當(dāng)前?個(gè)流程還沒(méi)有執(zhí)行完,就有其他的執(zhí)?流再次進(jìn)入,我們稱之為重入。?個(gè)函數(shù)在重入的情況下,運(yùn)行結(jié)果不會(huì)出現(xiàn)任何不同或者任何問(wèn)題,則該函數(shù)被稱為可重?函數(shù),否則,是不可重入函數(shù)。
到目前為止,我們學(xué)習(xí)的重入實(shí)際上分為以下的兩種:
1.多線程重入函數(shù)
2.信號(hào)導(dǎo)致一個(gè)執(zhí)行流重復(fù)進(jìn)入函數(shù)
實(shí)際上對(duì)應(yīng)線程安全和重入只需要理解以下的即可:
可重入與線程安全聯(lián)系
? 函數(shù)是可重入的,那就是線程安全的(其實(shí)知道這?句話就夠了)
? 函數(shù)是不可重入的,那就不能由多個(gè)線程使用,有可能引發(fā)線程安全問(wèn)題
? 如果?個(gè)函數(shù)中有全局變量,那么這個(gè)函數(shù)既不是線程安全也不是可重入的。
可重入與線程安全區(qū)別
可重入函數(shù)是線程安全函數(shù)的?種
線程安全不?定是可重入的,而可重入函數(shù)則?定是線程安全的。
如果將對(duì)臨界資源的訪問(wèn)加上鎖,則這個(gè)函數(shù)是線程安全的,但如果這個(gè)重入函數(shù)若鎖還未釋放則會(huì)產(chǎn)生死鎖,因此是不可重入的。
注意:
? 如果不考慮 信號(hào)導(dǎo)致?個(gè)執(zhí)行流重復(fù)進(jìn)?函數(shù) 這種重入情況,線程安全和重入在安全角度不做區(qū)分
? 但是線程安全側(cè)重說(shuō)明線程訪問(wèn)公共資源的安全情況,表現(xiàn)的是并發(fā)線程的特點(diǎn)
? 可重入描述的是?個(gè)函數(shù)是否能被重復(fù)進(jìn)入,表示的是函數(shù)的特點(diǎn)
3. 死鎖
死鎖是指在一組的線程當(dāng)中各個(gè)進(jìn)程均占有不會(huì)釋放的資源,但互相申請(qǐng)其他進(jìn)程所占用不會(huì)釋放的資源而處于的一種永久的等待狀態(tài)。
例如以下的示例,當(dāng)前有線程A和線程B,線程必須同時(shí)的擁有鎖1和鎖2,才能進(jìn)行之后資源的訪問(wèn)

在此申請(qǐng)一把鎖的時(shí)候是原子的,但是兩把就不是了

這時(shí)就會(huì)造成以下的結(jié)果:
產(chǎn)生死鎖的四個(gè)必要條件
? 互斥條件:?個(gè)資源每次只能被?個(gè)執(zhí)行流使用
? 請(qǐng)求與保持條件:?個(gè)執(zhí)行流因請(qǐng)求資源而阻塞時(shí),對(duì)已獲得的資源保持不放

? 不剝奪條件:?個(gè)執(zhí)行流已獲得的資源,在末使用完之前,不能強(qiáng)行剝奪
? 循環(huán)等待條件:若干執(zhí)行流之間形成?種頭尾相接的循環(huán)等待資源的關(guān)系
4. STL,智能制造和線程安全
STL中的容器是否是線程安全的?
其實(shí)不是的。原因是, STL 的設(shè)計(jì)初衷是將性能挖掘到極致, 而?旦涉及到加鎖保證線程安全, 會(huì)對(duì)性能造成巨大的影響。
而且對(duì)于不同的容器, 加鎖?式的不同, 性能可能也不同(例如hash表的鎖表和鎖桶).
因此 STL 默認(rèn)不是線程安全. 如果需要在多線程環(huán)境下使用, 往往需要調(diào)用者自行保證線程安全.
智能指針是否是線程安全的?
對(duì)于 unique_ptr, 由于只是在當(dāng)前代碼塊范圍內(nèi)生效, 因此不涉及線程安全問(wèn)題.
對(duì)于 shared_ptr, 多個(gè)對(duì)象需要共用?個(gè)引用計(jì)數(shù)變量, 所以會(huì)存在線程安全問(wèn)題. 但是標(biāo)準(zhǔn)庫(kù)實(shí)現(xiàn)的時(shí)候考慮到了這個(gè)問(wèn)題, 基于原子操作(CAS)的方式保證 shared_ptr 能夠搞效,原子的操作引用計(jì)數(shù)。
以上就是本篇的全部?jī)?nèi)容了,在此之后我們將開始Linux當(dāng)中網(wǎng)絡(luò)部分的學(xué)習(xí),未完待續(xù)……

浙公網(wǎng)安備 33010602011771號(hào)