1.线程概念
1.什么是线程
2.线程 vs 进程
不同的操作系统有不同的实现方式:
- linux :直接使用pcb的功能来模拟线程,不创建新的数据结构
- windows: 使用新的数据结构TCB,来进行实现,一个PCB里有很多个TCB
3.资源划分
详情可见操作系统书籍中的存储器管理和虚拟存储器管理章节!!!!
4.线程理解
2.线程和进程的区别
- 黄字代表线程特有的私有数据
- 一组寄存器和上下文数据---------------证明线程是可以被独立调用
- 栈--------------证明线程是动态的
1.进程的线程共享!!!!!!!!!!
同⼀地址空间
,因此 Text Segment 、 Data Segment 都是共享的,如果定义⼀个函数,在各线程中都可以调⽤ ,如果定义⼀个全局变量,在各线程中都可以访问到, 除此之外,各线程还共享以下进程资源和环境:
⽂件描述符表 (fd)
- 每种信号的处理⽅式(SIG_IGN、SIG_DFL或者⾃定义的信号处理函数)
当前⼯作⽬录
⽤⼾id和组id
1.父子进程只有代码段是共享的,但主线程和子线程连地址空间都是共享的,所以他们可以使用共享的函数和全局变量 (
意思就是如果子线程的全局变量被修改了,主线程看到的是同一个全局变量,也会变化
)轻松实现类似进程间通信!!!!!!!
2.而全局变量在父子进程中是写实拷贝,子变父不变!!!!!!!!!
3.linux的线程控制
1.线程创建
创建函数:
运行后使用 ps - aL指令查看线程
2.pthread库的引入----为什么需要有线程库?
4.pthread库的使用
- 与线程有关的函数构成了⼀个完整的系列,绝⼤多数函数的名字都是“
pthread_”
打头的
• 要使⽤这些函数库,要通过引⼊头⽂件<pthread.h>
• 链接这些线程函数库时要使⽤编译器命令的“-lpthread”
选项
1.线程创建—pthread_create()
- thread是新线程的标识符,是输出型参数(让主线程获取,便于调用其他函数)
2.线程等待—pthread_join()
- 这里retval拿到的是子线程的退出码,即子线程函数的返回值,但返回值是void *
- 所以retval的类型应当是void* 的地址类型即void**
- 其中,routine是子线程的入口函数,routine函数结束的话子线程也就结束了
3.线程取消或终止—pthread_exit()/pthread_cancel()
1-----------------pthread_exit()
2-----------------pthread_cancel()
- 如果线程被主线程或其他线程取消,那么主线程join函数得到的返回值固定为-1
4.线程分离—int pthread_detach(pthread_t thread);
5.线程ID及进程地址空间布局
1.--------------------------pthread_self函数获取id
2.--------------------------pthread库的动态链接
- 所有的线程都是在thread库中建立的,线程的管理块都存储在库中,具体的pcb由用户使用系统调用在内核中建立
- 创建线程的具体图例
6.线程互斥
来看一个买票的例子:
/ 操作共享变量会有问题的售票系统代码
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int ticket = 100;
void* route(void* arg) {
char* id = (char*)arg;
while (1) {
if (ticket > 0) {
usleep(1000);
printf("%s sells ticket:%d\n", id, ticket);
ticket--;
} else {
break;
}
}
}
int main(void) {
pthread_t t1, t2, t3, t4;
pthread_create(&t1, NULL, route, (void*)"thread 1");
pthread_create(&t2, NULL, route, (void*)"thread 2");
pthread_create(&t3, NULL, route, (void*)"thread 3");
pthread_create(&t4, NULL, route, (void*)"thread 4");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
}
- 为了避免买票变成负数,所以要使用
锁
来保证线程间的互斥
为什么会变成负数??????
- 因为ticket–操作并不是原子的----------即无法一步完成------要先把ticket大小传入cpu,再在cpu中进行运算,最后再写回ticket全局变量中--------一共有三步
- 可能某一个线程计算完后ticket为0,但还没写回ticket,另一个线程就又开始运行,这个时候ticket是1,
还能通过if条件语句
,最后两个线程都执行ticket–,全部写回后,ticket变成了-1!!!!
1.锁的使用
#include <pthread.h>
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int ticket = 100;
pthread_mutex_t mutex;//设置锁----这里是全局锁,用完后会自动销毁
void* route(void* arg)
{
char* id = (char*)arg;
while (1)
{
pthread_mutex_lock(&mutex);//pthread_mutex_lock--------上锁
if (ticket > 0) {
usleep(1000);
printf("%s sells ticket:%d\n", id, ticket);
ticket--;
pthread_mutex_unlock(&mutex);// pthread_mutex_unlock--------解锁
} else {
pthread_mutex_unlock(&mutex);// pthread_mutex_unlock--------解锁
break;
}
}
return nullptr;
}
int main(void) {
pthread_t t1, t2, t3, t4;
pthread_mutex_init(&mutex, NULL);// pthread_mutex_init------初始化锁
pthread_create(&t1, NULL, route, (void*)"thread 1");
pthread_create(&t2, NULL, route, (void*)"thread 2");
pthread_create(&t3, NULL, route, (void*)"thread 3");
pthread_create(&t4, NULL, route, (void*)"thread 4");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
pthread_mutex_destroy(&mutex);//pthread_mutex_destroy-------删除锁
}
pthread
库是 POSIX 线程库,提供了多线程编程的 API,其中包括用于线程同步的锁机制。以下是 pthread
中常见的锁函数及其解释:
1. 互斥锁(Mutex)
互斥锁用于保护临界区,确保同一时间只有一个线程可以访问共享资源。
相关函数:
-
pthread_mutex_init
初始化互斥锁。int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
mutex
:指向互斥锁对象的指针。attr
:属性对象(通常设为NULL
使用默认属性)。- 成功返回
0
,失败返回错误码。
-
pthread_mutex_lock
加锁。如果锁已被其他线程持有,则调用线程阻塞直到锁被释放。int pthread_mutex_lock(pthread_mutex_t *mutex);
-
pthread_mutex_trylock
尝试加锁,如果锁已被持有则立即返回错误(非阻塞)。int pthread_mutex_trylock(pthread_mutex_t *mutex);
- 成功返回
0
,锁被持有时返回EBUSY
。
- 成功返回
-
pthread_mutex_unlock
解锁。int pthread_mutex_unlock(pthread_mutex_t *mutex);
-
pthread_mutex_destroy
销毁互斥锁,释放资源。int pthread_mutex_destroy(pthread_mutex_t *mutex);
7.线程同步
1.条件变量函数介绍
-------------------为了避免加锁后导致线程饥饿而设置的变量
条件变量用于线程间通信,通常与互斥锁配合使用,实现线程的等待和唤醒机制。
相关函数:
-
pthread_cond_init
//第二个变量基本是设置为nullptr
初始化条件变量。int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
-
pthread_cond_wait
// 第二个参数是上文的互斥锁
等待条件变量,并释放关联的互斥锁(原子操作)。int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
-
pthread_cond_signal
唤醒一个等待该条件变量的线程。int pthread_cond_signal(pthread_cond_t *cond);
-
pthread_cond_broadcast
唤醒所有等待该条件变量的线程。int pthread_cond_broadcast(pthread_cond_t *cond);
-
pthread_cond_destroy
销毁条件变量。int pthread_cond_destroy(pthread_cond_t *cond);
#include <iostream>
#include <vector>
#include <string>
#include <unistd.h>
#include <pthread.h>
#define NUM 5
int cnt = 1000;
pthread_mutex_t glock = PTHREAD_MUTEX_INITIALIZER; // 定义锁
pthread_cond_t gcond = PTHREAD_COND_INITIALIZER; // 定义条件变量
// 等待是需要等,什么条件才会等呢?票数为0,等待之前,就要对资源的数量进行判定。
// 判定本身就是访问临界资源!,判断一定是在临界区内部的.
// 判定结果,也一定在临界资源内部。所以,条件不满足要休眠,一定是在临界区内休眠的!
// 证明一件事情:条件变量,可以允许线程等待
// 可以允许一个线程唤醒在cond等待的其他线程, 实现同步过程
void *threadrun(void *args)
{
std::string name = static_cast<const char *>(args);
while (true)
{
pthread_mutex_lock(&glock);
// 直接让对用的线程进行等待?? 临界资源不满足导致我们等待的!
pthread_cond_wait(&gcond, &glock); // glock在pthread_cond_wait之前,会被自动释放掉
std::cout << name << " 计算: " << cnt << std::endl;
cnt++;
pthread_mutex_unlock(&glock);
}
}
int main()
{
std::vector<pthread_t> threads;
for (int i = 0; i < NUM; i++)
{
pthread_t tid;
char *name = new char[64];
snprintf(name, 64, "thread-%d", i);//snprintf函数往name中打印字符串
int n = pthread_create(&tid, nullptr, threadrun, name);
if (n != 0)
continue;
threads.push_back(tid);
sleep(1);
}
sleep(3);
// 每隔1s唤醒一个线程
while(true)
{
std::cout << "唤醒所有线程... " << std::endl;
pthread_cond_broadcast(&gcond);
// std::cout << "唤醒一个线程... " << std::endl;
// pthread_cond_signal(&gcond);
sleep(1);
}
for (auto &id : threads)
{
int m = pthread_join(id, nullptr);
(void)m;
}
return 0;
}
8.生产者消费者模型
- 生产者和消费者之间要有顺序地进行工作,所以是同步的
1.基于阻塞队列实现生产者消费者模型:
----------------什么是阻塞队列?
blockqueue.hpp:
// 阻塞队列的实现
#pragma once
#include <iostream>
#include <string>
#include <queue>
#include <pthread.h>
const int defaultcap = 5; // for test 默认队列容量,用于测试
// 模板类定义,T为队列元素类型
template <typename T>
class BlockQueue
{
private:
// 判断队列是否已满
bool IsFull() { return _q.size() >= _cap; }
// 判断队列是否为空
bool IsEmpty() { return _q.empty(); }
public:
// 构造函数,初始化队列容量和同步变量
BlockQueue(int cap = defaultcap)
: _cap(cap), _csleep_num(0), _psleep_num(0)
{
pthread_mutex_init(&_mutex, nullptr); // 初始化互斥锁
pthread_cond_init(&_full_cond, nullptr); // 初始化"满"条件变量
pthread_cond_init(&_empty_cond, nullptr); // 初始化"空"条件变量
}
// 入队操作,生产者调用
void Equeue(const T &in)
{
pthread_mutex_lock(&_mutex); // 加锁保护临界区
// 生产者调用
while (IsFull()) // 循环检查队列是否已满(防止虚假唤醒),这里一定要使用while,不能使用if
{
// 队列已满,生产者需要等待
// 应该让生产者线程进行等待
// 重点1:pthread_cond_wait调用成功,挂起当前线程之前,要先自动释放锁!!
// 重点2:当线程被唤醒的时候,默认就在临界区内唤醒!要从pthread_cond_wait
// 成功返回,需要当前线程,重新申请_mutex锁!!!
// 重点3:如果我被唤醒,但是申请锁失败了??我就会在锁上阻塞等待!!!
_psleep_num++; // 增加休眠生产者计数
std::cout << "生产者,进入休眠了: _psleep_num" << _psleep_num << std::endl;
// 等待队列不满的信号
// 1. 调用时会自动释放锁
// 2. 被唤醒时会重新获取锁
// 3. 可能因为虚假唤醒或获取锁失败而阻塞
pthread_cond_wait(&_full_cond, &_mutex);
_psleep_num--; // 生产者被唤醒,减少计数
}
// 队列有空间,执行入队操作
_q.push(in);
// v2优化: 如果有消费者在等待,则唤醒一个消费者
if(_csleep_num>0)
{
pthread_cond_signal(&_empty_cond);
std::cout << "唤醒消费者..." << std::endl;
}
pthread_mutex_unlock(&_mutex); // 释放锁
}
// 出队操作,消费者调用
T Pop()
{
pthread_mutex_lock(&_mutex); // 加锁保护临界区
// 消费者调用
while (IsEmpty()) // 循环检查队列是否为空(防止虚假唤醒)
{
_csleep_num++; // 增加休眠消费者计数
pthread_cond_wait(&_empty_cond, &_mutex); // 等待队列不空的信号
_csleep_num--; // 消费者被唤醒,减少计数
}
// 队列不空,执行出队操作
T data = _q.front();
_q.pop();
// 如果有生产者在等待,则唤醒一个生产者
if(_psleep_num > 0)
{
pthread_cond_signal(&_full_cond);
std::cout << "唤醒生产者" << std::endl;
}
pthread_mutex_unlock(&_mutex); // 释放锁
return data; // 返回出队元素
}
// 析构函数,释放资源
~BlockQueue()
{
pthread_mutex_destroy(&_mutex); // 销毁互斥锁
pthread_cond_destroy(&_full_cond); // 销毁"满"条件变量
pthread_cond_destroy(&_empty_cond); // 销毁"空"条件变量
}
private:
std::queue<T> _q; // 底层队列容器,存储实际数据(临界资源)
int _cap; // 队列容量大小
pthread_mutex_t _mutex; // 互斥锁,保护队列操作
pthread_cond_t _full_cond; // 条件变量,队列满时生产者等待
pthread_cond_t _empty_cond; // 条件变量,队列空时消费者等待
int _csleep_num; // 当前休眠的消费者线程数量
int _psleep_num; // 当前休眠的生产者线程数量
};
Task.hpp:
// 防止头文件重复包含
#pragma once
// 包含标准输入输出库
#include <iostream>
// 包含Unix标准库,提供sleep等函数
#include <unistd.h>
// 包含函数对象库,用于std::function
#include <functional>
// 任务形式2
// 我们定义了一个任务类型,返回值void,参数为空
using task_t = std::function<void()>;
// 下载任务示例函数
void Download()
{
// 打印任务开始信息
std::cout << "我是一个下载任务..." << std::endl;
// 模拟耗时操作,暂停3秒
sleep(3); // 假设处理任务比较耗时
}
// 任务形式1
// 任务类定义
class Task
{
public:
// 默认构造函数
Task(){}
// 带参数的构造函数,初始化x和y
Task(int x, int y):_x(x), _y(y)
{
}
// 执行任务的方法
void Execute()
{
// 计算x和y的和
_result = _x + _y;
}
// 获取x值的方法
int X() { return _x; }
// 获取y值的方法
int Y() { return _y; }
// 获取计算结果的方法
int Result()
{
return _result;
}
private:
int _x; // 第一个操作数
int _y; // 第二个操作数
int _result; // 计算结果
};
Main.cc:
// 包含阻塞队列头文件
#include "BlockQueue.hpp"
// 包含任务定义头文件
#include "Task.hpp"
// 标准输入输出库
#include <iostream>
// POSIX线程库
#include <pthread.h>
// Unix标准库,提供sleep等函数
#include <unistd.h>
// 消费者线程函数
void *consumer(void *args)
{
// 将传入参数转换为阻塞队列指针
BlockQueue<task_t> *bq = static_cast<BlockQueue<task_t> *>(args);
// 消费者持续运行
while (true)
{
sleep(10); // 模拟消费者处理速度较慢
// 1. 从阻塞队列中取出任务
task_t t = bq->Pop();
// 2. 执行任务
// 注意:此时任务已经从队列中移除,在消费者线程上下文中执行
t();
}
return nullptr; // 线程函数需要返回指针
}
// 生产者线程函数
void *productor(void *args)
{
// 将传入参数转换为阻塞队列指针
BlockQueue<task_t> *bq = static_cast<BlockQueue<task_t> *>(args);
// 生产者持续运行
while (true)
{
// 1. 生产任务前的提示信息
std::cout << "生产了一个任务: " << std::endl;
// 2. 将任务放入阻塞队列
// 这里使用Download函数作为示例任务
bq->Equeue(Download);
}
return nullptr; // 线程函数需要返回指针
}
// 主函数
int main()
{
// 创建阻塞队列实例
// 队列中存储的是task_t类型的任务(std::function<void()>)
BlockQueue<task_t> *bq = new BlockQueue<task_t>();
// 定义线程ID数组
pthread_t c[2]; // 2个消费者线程
pthread_t p[3]; // 3个生产者线程
// 创建消费者线程
pthread_create(c, nullptr, consumer, bq);
pthread_create(c+1, nullptr, consumer, bq);
// 创建生产者线程
pthread_create(p, nullptr, productor, bq);
pthread_create(p+1, nullptr, productor, bq);
pthread_create(p+2, nullptr, productor, bq);
// 等待所有线程结束(实际上由于是无限循环,不会结束)
pthread_join(c[0], nullptr);
pthread_join(c[1], nullptr);
pthread_join(p[0], nullptr);
pthread_join(p[1], nullptr);
pthread_join(p[2], nullptr);
// 释放阻塞队列内存(实际上由于线程无限循环,不会执行到这里)
delete bq;
return 0;
}
2.基于环形队列
实现生产者消费者模型:
-------POSIX信号量:
POSIX信号量和SystemV信号量作⽤相同,都是⽤于同步操作,达到⽆冲突的访问共享资源⽬的。但POSIX可以⽤于线程间同步
。
- 上文写的基于阻塞队列的生产消费模型就是第一种,因为无论是消费者还是生产者,同一个时间下
只能有一个线程访问阻塞队列这个临界资源
!!!!!!-------------------所以说是整体使用的,所以要加mutex锁!!!! - 下面的可以对资源进行切片,实现多个线程共享资源(不同块),需要使用信号量------pv操作--------
p--,v++
POSIX信号量的调用接口
:
初始化信号量
:
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
pshared:0表⽰线程间共享,⾮零表⽰进程间共享//线程间共享直接传nullptr
value:信号量初始值
销毁信号量
:
int sem_destroy(sem_t *sem);
等待信号量
:**p()------说白了就是-- **
功能:等待信号量,会将信号量的值减1
int sem_wait(sem_t *sem); //P()----说白了就是--
发布信号量
:v()------说白了就是++
功能:发布信号量,表⽰资源使⽤完毕,可以归还资源了。将信号量值加1。
int sem_post(sem_t *sem);//V()
Mutex.hpp------自己写的互斥锁:
// 防止头文件重复包含
#pragma once
// 标准输入输出库
#include <iostream>
// POSIX线程库
#include <pthread.h>
// 定义互斥锁模块命名空间
namespace MutexModule
{
// 互斥锁类封装
class Mutex
{
public:
// 构造函数:初始化互斥锁
Mutex()
{
pthread_mutex_init(&_mutex, nullptr); // 使用默认属性初始化互斥锁
}
// 加锁方法
void Lock()
{
int n = pthread_mutex_lock(&_mutex); // 阻塞直到获取锁
(void)n; // 显式忽略返回值,避免编译器警告
}
// 解锁方法
void Unlock()
{
int n = pthread_mutex_unlock(&_mutex); // 释放锁
(void)n; // 显式忽略返回值,避免编译器警告
}
// 析构函数:销毁互斥锁
~Mutex()
{
pthread_mutex_destroy(&_mutex); // 释放互斥锁资源
}
// 获取底层pthread_mutex_t指针
pthread_mutex_t *Get()
{
return &_mutex; // 暴露底层POSIX互斥锁,用于需要原生接口的场景
}
private:
pthread_mutex_t _mutex; // POSIX原生互斥锁对象
};
// 锁守卫类(RAII封装)
class LockGuard
{
public:
// 构造函数:接收Mutex引用并立即加锁!!!!!!
LockGuard(Mutex &mutex):_mutex(mutex)
{
_mutex.Lock(); // 构造时自动加锁
}
// 析构函数:自动解锁----------析构时自动解锁
~LockGuard()
{
_mutex.Unlock(); // 析构时自动解锁
}
private:
Mutex &_mutex; // 引用管理的互斥锁对象
};
}
Sem.hpp---------自己写的条件变量
// 包含标准输入输出库
#include <iostream>
// 包含POSIX信号量库
#include <semaphore.h>
// 包含POSIX线程库
#include <pthread.h>
// 定义信号量模块命名空间
namespace SemModule
{
// 默认信号量初始值
const int defaultvalue = 1;
// 信号量类封装
class Sem
{
public:
// 构造函数:初始化信号量
// 参数sem_value: 信号量初始值,默认为defaultvalue(1)
Sem(unsigned int sem_value = defaultvalue)
{
// 初始化无名信号量
// 参数1: 信号量对象
// 参数2: pshared标志(0表示线程间共享,非0表示进程间共享)
// 参数3: 信号量初始值
sem_init(&_sem, 0, sem_value);
}
// P操作(等待/获取信号量)--
void P()
{
// 原子操作,信号量值减1
// 如果信号量值为0,则阻塞直到信号量值大于0
int n = sem_wait(&_sem);
(void)n; // 显式忽略返回值,避免编译器警告
}
// V操作(释放/增加信号量)++
void V()
{
// 原子操作,信号量值加1
// 如果有线程在等待信号量,则唤醒其中一个
int n = sem_post(&_sem);
(void)n; // 显式忽略返回值,避免编译器警告
}
// 析构函数:销毁信号量
~Sem()
{
// 销毁信号量,释放相关资源
sem_destroy(&_sem);
}
private:
sem_t _sem; // POSIX信号量对象
};
}
RingQueue.hpp:循环队列
// 防止头文件重复包含
#pragma once
// 标准输入输出库
#include <iostream>
// 动态数组容器
#include <vector>
// 包含信号量实现
#include "Sem.hpp"
// 包含互斥锁实现
#include "Mutex.hpp"
// 调试用的默认队列容量
static const int gcap = 5; // for debug
// 使用信号量和互斥锁的命名空间
using namespace SemModule;
using namespace MutexModule;
// 环形队列模板类
template <typename T>
class RingQueue
{
public:
// 构造函数,初始化队列容量和同步机制
RingQueue(int cap = gcap)
: _cap(cap), // 设置队列容量
_rq(cap), // 初始化存储容器
_blank_sem(cap), // 初始化空位信号量(初始值为容量)----N
_p_step(0), // 生产者下标初始为0
_data_sem(0), // 初始化数据信号量(初始为0,表示无数据)---0
_c_step(0) // 消费者下标初始为0
{
}
// 生产者入队方法
void Equeue(const T &in)
{
// 1. 申请空位信号量(如果没有空位会阻塞)
_blank_sem.P();
{//!!!!!!!设置一个作用域,目的是lockguard出作用域时析构直接调用解锁!!!!!!!!!!
// 使用RAII锁保护生产者临界区
LockGuard lockguard(_pmutex);//初始化时会自动上锁,出作用域时会在析构时自动解锁
// 2. 生产数据到当前生产者位置
_rq[_p_step] = in;
// 3. 更新生产者下标
++_p_step;
// 4. 维持环形特性(下标超过容量时回到0)
_p_step %= _cap;
}
// 释放数据信号量(表示有新数据可用)
_data_sem.V();
}
// 消费者出队方法
void Pop(T *out)
{
// 1. 申请数据信号量(如果没有数据会阻塞)
_data_sem.P();
{
// 使用RAII锁保护消费者临界区
LockGuard lockguard(_cmutex);
// 2. 消费当前消费者位置的数据
*out = _rq[_c_step];
// 3. 更新消费者下标
++_c_step;
// 4. 维持环形特性(下标超过容量时回到0)
_c_step %= _cap;
}
// 释放空位信号量(表示有空位可用)
_blank_sem.V();
}
private:
std::vector<T> _rq; // 底层存储容器
int _cap; // 队列容量
// 生产者相关成员
Sem _blank_sem; // 空位信号量(控制生产者)
int _p_step; // 生产者当前位置下标
// 消费者相关成员
Sem _data_sem; // 数据信号量(控制消费者)
int _c_step; // 消费者当前位置下标
// 多生产者多消费者同步锁
Mutex _cmutex; // 消费者互斥锁
Mutex _pmutex; // 生产者互斥锁
};
Main.cc:
#include <iostream> // 标准输入输出库
#include <pthread.h> // POSIX线程库
#include <unistd.h> // Unix标准库(提供sleep等函数)
#include "RingQueue.hpp" // 环形队列实现
// 线程数据结构体(用于传递参数)
struct threaddata
{
RingQueue<int> *rq; // 指向共享的环形队列
std::string name; // 线程名称(用于标识)
};
// 消费者线程函数
void *consumer(void *args)
{
// 转换参数类型
threaddata *td = static_cast<threaddata*>(args);
// 消费者持续运行
while (true)
{
sleep(3); // 模拟消费速度较慢
// 1. 从队列中消费数据
int t = 0;
td->rq->Pop(&t);
// 2. 处理数据(此时数据已从队列移除)
std::cout << td->name << " 消费者拿到了一个数据: " << t << std::endl;
}
return nullptr;
}
// 全局生产数据(简单示例)
int data = 1;
// 生产者线程函数
void *productor(void *args)
{
// 转换参数类型
threaddata *td = static_cast<threaddata*>(args);
// 生产者持续运行
while (true)
{
sleep(1); // 模拟生产速度较快
// 1. 生产数据前的提示信息
std::cout << td->name << " 生产了一个任务: " << data << std::endl;
// 2. 将数据放入队列
td->rq->Equeue(data);
data++; // 全局数据递增
}
return nullptr;
}
int main()
{
// 创建环形队列实例(容量使用默认值)
RingQueue<int> *rq = new RingQueue<int>();
// 定义线程ID数组
pthread_t c[2]; // 2个消费者线程
pthread_t p[3]; // 3个生产者线程
// 创建消费者线程1
threaddata *td = new threaddata();
td->name = "cthread-1";
td->rq = rq;
pthread_create(c, nullptr, consumer, td);
// 创建消费者线程2
threaddata *td2 = new threaddata();
td2->name = "cthread-2";
td2->rq = rq;
pthread_create(c + 1, nullptr, consumer, td2);
// 创建生产者线程3
threaddata *td3 = new threaddata();
td3->name = "pthread-3";
td3->rq = rq;
pthread_create(p, nullptr, productor, td3);
// 创建生产者线程4
threaddata *td4 = new threaddata();
td4->name = "pthread-4";
td4->rq = rq;
pthread_create(p + 1, nullptr, productor, td4);
// 创建生产者线程5
threaddata *td5 = new threaddata();
td5->name = "pthread-5";
td5->rq = rq;
pthread_create(p + 2, nullptr, productor, td5);
// 等待所有线程结束(实际上不会结束)
pthread_join(c[0], nullptr);
pthread_join(c[1], nullptr);
pthread_join(p[0], nullptr);
pthread_join(p[1], nullptr);
pthread_join(p[2], nullptr);
// 内存释放(实际不会执行到这里)
delete rq;
delete td;
delete td2;
delete td3;
delete td4;
delete td5;
return 0;
}
makefile:
ring_cp:Main.cc
g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -f ring_cp
7.线程池
1.日志
diary.hpp:
#ifndef __LOG_HPP__
#define __LOG_HPP__
// 头文件保护宏,防止重复包含
// 标准库头文件包含
#include <iostream> // 标准输入输出流
#include <cstdio> // C标准IO
#include <string> // 字符串类
#include <filesystem> // 文件系统操作(C++17)
#include <sstream> // 字符串流
#include <fstream> // 文件流
#include <memory> // 智能指针
#include <ctime> // 时间处理
#include <unistd.h> // POSIX API(获取进程ID)
#include <mutex> // 标准库互斥锁//这次不使用pthread.h库
namespace LogModule {
// 全局行分隔符(Windows风格)
const std::string gsep = "\r\n";
/* 日志策略基类(抽象接口) */
class LogStrategy {
public:
virtual ~LogStrategy() = default; // 虚析构函数
// 纯虚函数:同步日志消息,下文需要重写
virtual void SyncLog(const std::string &message) = 0;
};
/* 控制台日志策略(具体实现) */
class ConsoleLogStrategy : public LogStrategy {
public:
ConsoleLogStrategy() = default; // 默认构造函数
// 实现基类的日志同步接口
void SyncLog(const std::string &message) override {
// 使用lock_guard自动加锁/解锁
std::lock_guard<std::mutex> lock(_mutex);
// 输出到标准输出,直接打印到显示器上
std::cout << message << gsep;
}
~ConsoleLogStrategy() = default; // 默认析构函数
private:
std::mutex _mutex; // 互斥锁保证线程安全
};
/* 文件日志策略(具体实现) */
const std::string defaultpath = "./log"; // 默认日志目录
const std::string defaultfile = "my.log"; // 默认日志文件名
class FileLogStrategy : public LogStrategy {
public:
// 构造函数,可自定义路径和文件名
FileLogStrategy(const std::string &path = defaultpath,
const std::string &file = defaultfile)
: _path(path), _file(file) {
std::lock_guard<std::mutex> lock(_mutex);//如果使用pthread库的pthread_mutex_lock(&_mutex),那么记得初始化pthread_mutex_t _mutex = PTHREAD_MUTEX_INITIALIZER;
// 检查目录是否存在 //,还有记得要加unlock解锁,
if (std::filesystem::exists(_path)) { //判断路径是否存在,因为文件可以自动新建,但是路径不行
return;
}
try {
// 递归创建目录
std::filesystem::create_directories(_path); //创建对应的路径
} catch (const std::filesystem::filesystem_error &e) {
// 异常处理
std::cerr << e.what() << '\n';
}
}
// 实现基类的日志同步接口
void SyncLog(const std::string &message) override {
std::lock_guard<std::mutex> lock(_mutex);
// 构造完整文件路径,这样才能打印到文件中!!!!!!!!
std::string filename = _path +
(_path.back() == '/' ? "" : "/") + _file;
// 以追加模式打开文件
std::ofstream out(filename, std::ios::app); //!!!!!!!记得使用追加!!!
if (!out.is_open()) {
return; // 文件打开失败则返回
}
// 写入日志内容
out << message << gsep;
out.close(); // 关闭文件
}
~FileLogStrategy() = default; // 默认析构函数
private:
std::string _path; // 日志存储路径
std::string _file; // 日志文件名
std::mutex _mutex; // 互斥锁保证线程安全
};
/* 日志等级枚举 */
enum class LogLevel {
DEBUG, // 调试信息
INFO, // 普通信息
WARNING, // 警告信息
ERROR, // 错误信息
FATAL // 致命错误
};
/* 日志等级枚举转字符串 */
std::string Level2Str(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";
default: return "UNKNOWN";
}
}
/* 获取当前时间戳 */
std::string GetTimeStamp() {
time_t curr = time(nullptr); // 获取当前时间
struct tm curr_tm;
localtime_r(&curr, &curr_tm); // 转换为本地时间
char timebuffer[128];
// 格式化时间为字符串
snprintf(timebuffer, sizeof(timebuffer),
"%4d-%02d-%02d %02d:%02d:%02d",
curr_tm.tm_year+1900, // 年(从1900开始)
curr_tm.tm_mon+1, // 月(0-11)
curr_tm.tm_mday, // 日
curr_tm.tm_hour, // 时
curr_tm.tm_min, // 分
curr_tm.tm_sec); // 秒
return timebuffer;
}
/* 日志器主类 */
class Logger {
public:
Logger() {
// 默认使用控制台输出策略
EnableConsoleLogStrategy();
}
// 启用文件日志策略
void EnableFileLogStrategy() {
std::lock_guard<std::mutex> lock(_mutex);
// 创建文件策略实例
_fflush_strategy = std::make_unique<FileLogStrategy>(); //类型转换
}
// 启用控制台日志策略
void EnableConsoleLogStrategy() {
std::lock_guard<std::mutex> lock(_mutex);
// 创建控制台策略实例
_fflush_strategy = std::make_unique<ConsoleLogStrategy>(); //一样的类型转换
}
/* 日志消息类(RAII实现) */
class LogMessage {
public:
// 构造函数,收集日志元信息
LogMessage(LogLevel level, const std::string &src_name,
int line_number, Logger &logger)
: _curr_time(GetTimeStamp()), // 当前时间
_level(level), // 日志等级
_pid(getpid()), // 进程ID
_src_name(src_name), // 源文件名
_line_number(line_number), // 行号
_logger(logger) { // 所属日志器
// 构造日志前缀信息
std::stringstream ss;
ss << "[" << _curr_time << "] "
<< "[" << Level2Str(_level) << "] "
<< "[" << _pid << "] "
<< "[" << _src_name << "] "
<< "[" << _line_number << "] "
<< "- ";
_loginfo = ss.str(); // 保存前缀
}
// 重载<<操作符,支持链式调用
template <typename T>
LogMessage &operator<<(const T &info) {
std::stringstream ss;
ss << info;
_loginfo += ss.str(); // 追加日志内容
return *this;
}
// 析构函数,实际执行日志写入
~LogMessage() {
if (_logger._fflush_strategy) {
// 调用策略写入日志
_logger._fflush_strategy->SyncLog(_loginfo);//message析构之后会写入
}
}
private:
std::string _curr_time; // 日志时间
LogLevel _level; // 日志等级
pid_t _pid; // 进程ID
std::string _src_name; // 源文件名
int _line_number; // 行号
std::string _loginfo; // 完整日志信息
Logger &_logger; // 所属日志器引用
};
// 重载()操作符创建日志消息
LogMessage operator()(LogLevel level, const std::string &name, int line) {
return LogMessage(level, name, line, *this);
}
~Logger() = default; // 默认析构函数
private:
std::unique_ptr<LogStrategy> _fflush_strategy; // 当前日志策略
std::mutex _mutex; // 保护策略切换的互斥锁
};
// 全局日志器实例
Logger logger;
/* 用户接口宏 */
#define LOG(level) logger(level, __FILE__, __LINE__) // 创建日志条目
#define Enable_Console_Log_Strategy() logger.EnableConsoleLogStrategy() // 切换控制台输出
#define Enable_File_Log_Strategy() logger.EnableFileLogStrategy() // 切换文件输出
}
#endif // __LOG_HPP__
测试:main.cc:
#include "diary.hpp"
#include <memory>
using namespace LogModule;
int main()
{
//Enable_Console_Log_Strategy();
//LOG(LogLevel::DEBUG) << "hello world" << 3.141;
//LOG(LogLevel::DEBUG) << "hello world" << 3.142;
//std::unique_ptr<LogStrategy> strategy = std::make_unique<ConsoleLogStrategy>(); // C++14 控制台打印
//std::unique_ptr<LogStrategy> strategy = std::make_unique<FileLogStrategy>(); // C++14 文件打印
//strategy->SyncLog("hello log!");
logger(LogLevel::DEBUG, "main.cc", 10) << "hello world," << 3.14 << " " << 8899 << "aaaa";//这个是使用默认的控制台打印
//logger(DEBUG, "main.cc", 10) --> 创建 LogMessage 对象
///|
///v
// 多次 operator<< 调用 --> 逐步拼接字符串
///|
///v
// LogMessage 析构 --> 调用 SyncLog
///|
///v
//策略处理(控制台/文件) --> 加锁 -> 写入 -> 解锁
logger(LogLevel::DEBUG, "main.cc", 10) << "hello world";
logger(LogLevel::DEBUG, "main.cc", 10) << "hello world";
logger(LogLevel::DEBUG, "main.cc", 10) << "hello world";
logger(LogLevel::DEBUG, "main.cc", 10) << "hello world";
logger(LogLevel::DEBUG, "main.cc", 10) << "hello world";
logger(LogLevel::DEBUG, "main.cc", 10) << "hello world";
return 0;
}
makefile:
diary:main.cc
g++ -o $@ $^ -std=c++17 -lpthread
.PHONY:clean
clean:
rm -f diary
9.线程池实现
Log.hpp:
#ifndef __LOG_HPP__
#define __LOG_HPP__
// 头文件保护宏,防止重复包含
// 标准库头文件包含
#include <iostream> // 标准输入输出流
#include <cstdio> // C标准IO
#include <string> // 字符串类
#include <filesystem> // 文件系统操作(C++17)
#include <sstream> // 字符串流
#include <fstream> // 文件流
#include <memory> // 智能指针
#include <ctime> // 时间处理
#include <unistd.h> // POSIX API(获取进程ID)
#include <mutex> // 标准库互斥锁//这次不使用pthread.h库
namespace LogModule {
// 全局行分隔符(Windows风格)
const std::string gsep = "\r\n";
/* 日志策略基类(抽象接口) */
class LogStrategy {
public:
virtual ~LogStrategy() = default; // 虚析构函数
// 纯虚函数:同步日志消息,下文需要重写
virtual void SyncLog(const std::string &message) = 0;
};
/* 控制台日志策略(具体实现) */
class ConsoleLogStrategy : public LogStrategy {
public:
ConsoleLogStrategy() = default; // 默认构造函数
// 实现基类的日志同步接口
void SyncLog(const std::string &message) override {
// 使用lock_guard自动加锁/解锁
std::lock_guard<std::mutex> lock(_mutex);
// 输出到标准输出,直接打印到显示器上
std::cout << message << gsep;
}
~ConsoleLogStrategy() = default; // 默认析构函数
private:
std::mutex _mutex; // 互斥锁保证线程安全
};
/* 文件日志策略(具体实现) */
const std::string defaultpath = "./log"; // 默认日志目录
const std::string defaultfile = "my.log"; // 默认日志文件名
class FileLogStrategy : public LogStrategy {
public:
// 构造函数,可自定义路径和文件名
FileLogStrategy(const std::string &path = defaultpath,
const std::string &file = defaultfile)
: _path(path), _file(file) {
std::lock_guard<std::mutex> lock(_mutex);//如果使用pthread库的pthread_mutex_lock(&_mutex),那么记得初始化pthread_mutex_t _mutex = PTHREAD_MUTEX_INITIALIZER;
// 检查目录是否存在 //,还有记得要加unlock解锁,
if (std::filesystem::exists(_path)) { //判断路径是否存在,因为文件可以自动新建,但是路径不行
return;
}
try {
// 递归创建目录
std::filesystem::create_directories(_path); //创建对应的路径
} catch (const std::filesystem::filesystem_error &e) {
// 异常处理
std::cerr << e.what() << '\n';
}
}
// 实现基类的日志同步接口
void SyncLog(const std::string &message) override {
std::lock_guard<std::mutex> lock(_mutex);
// 构造完整文件路径,这样才能打印到文件中!!!!!!!!
std::string filename = _path +
(_path.back() == '/' ? "" : "/") + _file;
// 以追加模式打开文件
std::ofstream out(filename, std::ios::app); //!!!!!!!记得使用追加!!!
if (!out.is_open()) {
return; // 文件打开失败则返回
}
// 写入日志内容
out << message << gsep;
out.close(); // 关闭文件
}
~FileLogStrategy() = default; // 默认析构函数
private:
std::string _path; // 日志存储路径
std::string _file; // 日志文件名
std::mutex _mutex; // 互斥锁保证线程安全
};
/* 日志等级枚举 */
enum class LogLevel {
DEBUG, // 调试信息
INFO, // 普通信息
WARNING, // 警告信息
ERROR, // 错误信息
FATAL // 致命错误
};
/* 日志等级枚举转字符串 */
std::string Level2Str(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";
default: return "UNKNOWN";
}
}
/* 获取当前时间戳 */
std::string GetTimeStamp() {
time_t curr = time(nullptr); // 获取当前时间
struct tm curr_tm;
localtime_r(&curr, &curr_tm); // 转换为本地时间
char timebuffer[128];
// 格式化时间为字符串
snprintf(timebuffer, sizeof(timebuffer),
"%4d-%02d-%02d %02d:%02d:%02d",
curr_tm.tm_year+1900, // 年(从1900开始)
curr_tm.tm_mon+1, // 月(0-11)
curr_tm.tm_mday, // 日
curr_tm.tm_hour, // 时
curr_tm.tm_min, // 分
curr_tm.tm_sec); // 秒
return timebuffer;
}
/* 日志器主类 */
class Logger {
public:
Logger() {
// 默认使用控制台输出策略
EnableConsoleLogStrategy();
}
// 启用文件日志策略
void EnableFileLogStrategy() {
std::lock_guard<std::mutex> lock(_mutex);
// 创建文件策略实例
_fflush_strategy = std::make_unique<FileLogStrategy>(); //类型转换
}
// 启用控制台日志策略
void EnableConsoleLogStrategy() {
std::lock_guard<std::mutex> lock(_mutex);
// 创建控制台策略实例
_fflush_strategy = std::make_unique<ConsoleLogStrategy>(); //一样的类型转换
}
/* 日志消息类(RAII实现) */
class LogMessage {
public:
// 构造函数,收集日志元信息--前缀建立
LogMessage(LogLevel level, const std::string &src_name,
int line_number, Logger &logger)
: _curr_time(GetTimeStamp()), // 当前时间
_level(level), // 日志等级
_pid(getpid()), // 进程ID
_src_name(src_name), // 源文件名
_line_number(line_number), // 行号
_logger(logger) { // 所属日志器
// 构造日志前缀信息
std::stringstream ss;
ss << "[" << _curr_time << "] "
<< "[" << Level2Str(_level) << "] "
<< "[" << _pid << "] "
<< "[" << _src_name << "] "
<< "[" << _line_number << "] "
<< "- ";
_loginfo = ss.str(); // 保存前缀
}
// 重载<<操作符,支持链式调用
template <typename T>
LogMessage &operator<<(const T &info) {
std::stringstream ss;
ss << info;
_loginfo += ss.str(); // 追加日志内容
return *this;
}
// 析构函数,实际执行日志写入
~LogMessage() {
if (_logger._fflush_strategy) {
// 调用策略写入日志
_logger._fflush_strategy->SyncLog(_loginfo);//message析构之后会写入
}
}
private:
std::string _curr_time; // 日志时间
LogLevel _level; // 日志等级
pid_t _pid; // 进程ID
std::string _src_name; // 源文件名
int _line_number; // 行号
std::string _loginfo; // 完整日志信息
Logger &_logger; // 所属日志器引用
};
// 重载()操作符创建日志消息
LogMessage operator()(LogLevel level, const std::string &name, int line) {
return LogMessage(level, name, line, *this);
}
~Logger() = default; // 默认析构函数
private:
std::unique_ptr<LogStrategy> _fflush_strategy; // 当前日志策略
std::mutex _mutex; // 保护策略切换的互斥锁
};
// 全局日志器实例
Logger logger;
/* 用户接口宏 */
#define LOG(level) logger(level, __FILE__, __LINE__) // 创建日志条目
#define Enable_Console_Log_Strategy() logger.EnableConsoleLogStrategy() // 切换控制台输出
#define Enable_File_Log_Strategy() logger.EnableFileLogStrategy() // 切换文件输出
}
#endif // __LOG_HPP__
- 日志和上文一致!!!!!!
Makefile:
threadpool:Main.cc
g++ -o $@ $^ -std=c++17 -lpthread
.PHONY:clean
clean:
rm -f threadpool
Task.hpp:
#pragma once
#include <iostream>
#include <unistd.h>
#include <functional>
#include "Log.hpp"
using namespace LogModule;
// 任务形式2
// 我们定义了一个任务类型,返回值void,参数为空
using task_t = std::function<void()>;
void Download()
{
LOG(LogLevel::DEBUG) << "我是一个下载任务...";
}
- 这个task_t就是到时候要传递给线程的任务函数!!!!
Thread.hpp:
#ifndef _THREAD_H_ // 防止头文件重复包含的宏定义
#define _THREAD_H_ // 定义头文件标识符
#include <iostream> // 标准输入输出流
#include <string> // 字符串操作
#include <pthread.h> // POSIX线程库
#include <cstdio> // C标准IO
#include <cstring> // C字符串操作
#include <functional> // 函数对象包装器
#include "Log.hpp" // 日志系统头文件
namespace ThreadModlue // 线程模块命名空间
{
using namespace LogModule; // 使用日志模块命名空间
static uint32_t number = 1; // 线程计数器(注意:多线程下非原子操作,有并发问题),纪录由多少个线程
class Thread
{
using func_t = std::function<void()>; // 定义线程任务类型-----即线程建立后要做什么!!!!(无参无返回值的可调用对象)
private:
// 设置线程为分离状态
void EnableDetach()
{
_isdetach = true;
}
// 标记线程为运行状态
void EnableRunning()
{
_isrunning = true;
}
// 静态线程入口函数(必须静态,因为pthread_create需要C风格函数)
static void *Routine(void *args)
{
Thread *self = static_cast<Thread *>(args); // 转换参数为Thread对象指针
self->EnableRunning(); // 标记线程运行状态
if (self->_isdetach) // 如果设置了分离属性
self->Detach(); // 执行线程分离
pthread_setname_np(self->_tid, self->_name.c_str()); // 设置线程名称------在内核中改名!!!!!!!!!!!!!!!!!!不是在我们创建的类的——name中改
self->_func(); // 执行用户任务函数
return nullptr; // 线程退出
}
public:
// 构造函数:初始化线程属性
Thread(func_t func)
: _tid(0), // 初始化线程ID为0
_isdetach(false), // 默认非分离状态
_isrunning(false), // 默认非运行状态
res(nullptr), // 线程返回值指针初始化为空
_func(func) // 保存用户任务函数
{
_name = "thread-" + std::to_string(number++); // 生成线程名称(如thread-1)number是域内的全局变量
}
// 设置线程为分离状态
void Detach()
{
if (_isdetach) // 如果已经是分离状态则直接返回
return;
if (_isrunning) // 如果线程正在运行
pthread_detach(_tid); // 调用系统分离函数---------pthread_detach
EnableDetach(); // 更新状态标记
}
// 获取线程名称
std::string Name()
{
return _name;
}
// 启动线程
bool Start()
{
if (_isrunning) // 防止重复启动
return false;
int n = pthread_create(&_tid, nullptr, Routine, this); // 创建线程---创建成功后直接走routine函数!!!
if (n != 0) // 创建失败处理
{
return false;
}
else
{
return true;
}
}
// 停止线程(强制取消)
bool Stop()
{
if (_isrunning) // 只有运行中的线程才能停止
{
int n = pthread_cancel(_tid); // 发送取消请求
if (n != 0) // 取消失败处理
{
return false;
}
else
{
_isrunning = false; // 更新状态标记
return true;
}
}
return false; // 未运行的线程直接返回失败
}
// 等待线程结束
void Join()
{
if (_isdetach) // 分离线程不可join
{
return;
}
int n = pthread_join(_tid, &res); // 阻塞等待线程结束
if (n != 0) // 失败日志
{
LOG(LogLevel::DEBUG) << "Join线程失败";
}
else
{
LOG(LogLevel::DEBUG) << "Join线程成功";
}
}
// 析构函数(空实现,资源需用户显式管理)
~Thread()
{
}
private:
pthread_t _tid; // 线程ID
std::string _name; // 线程名称
bool _isdetach; // 是否分离状态标记
bool _isrunning; // 是否运行中标记
void *res; // 线程返回值指针
func_t _func; // 用户任务函数对象
};
}
#endif // _THREAD_H_ // 结束头文件保护
- 封装线程的等待,创建,命名(内核),停止(结束)和入口函数
ThreadPool.hpp:
#pragma once // 防止头文件重复包含
#include <iostream>
#include <string>
#include <vector>
#include <queue>
#include <pthread.h> // POSIX线程库
#include "Log.hpp" // 日志系统
#include "Thread.hpp" // 线程封装类
// .hpp header only // 提示这是头文件-only的实现
namespace ThreadPoolModule // 线程池模块命名空间
{
using namespace ThreadModlue; // 使用线程模块
using namespace LogModule; // 使用日志模块
static const int gnum = 5; // 默认线程数量
template <typename T> // 模板类,T表示任务类型
class ThreadPool
{
private:
// 唤醒所有休眠线程
void WakeUpAllThread()
{
pthread_mutex_lock(&_mutex); // 加锁
if (_sleepernum) // 如果有休眠线程-------—_sleepernum是指休眠的线程个数
pthread_cond_broadcast(&_cond); // pthread_cond_broadcast广播唤醒所有等待条件变量的线程,---------
pthread_mutex_unlock(&_mutex); // 解锁
LOG(LogLevel::INFO) << "唤醒所有的休眠线程"; // 记录日志
}
// 唤醒单个休眠线程
void WakeUpOne()
{
pthread_cond_signal(&_cond); // pthread_cond_signal发送信号唤醒一个等待线程
LOG(LogLevel::INFO) << "唤醒一个休眠线程"; // 记录日志
}
// 构造函数
ThreadPool(int num = gnum) : _num(num), _isrunning(false), _sleepernum(0)//num是要建立的线程个数,初始设置没有线程在运行,也没有休眠,因为一开始没建立线程!!!
{
pthread_mutex_init(&_mutex, nullptr); // 初始化互斥锁
pthread_cond_init(&_cond, nullptr); // 初始化条件变量
// 创建线程池中的工作线程
for (int i = 0; i < num; i++)
{
_threads.emplace_back( // 向vector中添加Thread对象--------emplace_back(Args&&... args)函数会直接构造对象,(Args&&... args)是参数,emplace_back 在构造 std::vector<Thread> 中的元素时,会直接调用 Thread 类的构造函数。
[this]() //lambda表达式作为线程函数------
{
HandlerTask(); // 线程执行的任务处理函数----[this](){ HandlerTask(); }-----//1.[this] 这里的 this 是指向当前 ThreadPool 对象的指针。因为HandlerTask() 是 ThreadPool 类的成员方法,必须通过 this 指针调用。
}); //2.()表示声明 lambda 的参数列表(此处为空,因为不需要参数)。适配 Thread 类构造函数所需的 std::function<void()> 类型
} //3.{ HandlerTask(); } 定义 lambda 的实际行为:调用 ThreadPool::HandlerTask() 方法。
//4.thread的函数变量是这个lambda表达式,不是HandlerTask()这个函数,但thread调用lambda表达式时会直接调用this->HandlerTask()
}
// 启动线程池
void Start()
{
if (_isrunning) // 如果已经在运行则返回
return;
_isrunning = true; // 设置运行标志
// 启动所有线程
for (auto &thread : _threads)
{
thread.Start(); // 启动线程
LOG(LogLevel::INFO) << "start new thread success: " << thread.Name(); // 记录日志
}
}
// 禁止拷贝构造和赋值
ThreadPool(const ThreadPool<T> &) = delete;
ThreadPool<T> &operator=(const ThreadPool<T> &) = delete;
public:
// 获取单例实例
static ThreadPool<T> *GetInstance() //----------确保整个程序中只有一个 ThreadPool 实例,并提供全局访问点
{
if (inc == nullptr) // 第一次检查
{
pthread_mutex_lock(&_lock); // 加锁
LOG(LogLevel::DEBUG) << "获取单例....";
if (inc == nullptr) // 第二次检查(双检锁)
{
LOG(LogLevel::DEBUG) << "首次使用单例, 创建之....";
inc = new ThreadPool<T>(); // 创建实例
inc->Start(); // 启动线程池
}
pthread_mutex_unlock(&_lock); // 解锁
}
return inc; // 返回单例
}
// 停止线程池
void Stop()
{
if (!_isrunning) // 如果已经停止则返回
return;
_isrunning = false; // 设置停止标志
// 唤醒所有的线程
WakeUpAllThread();
}
// 等待所有线程结束
void Join()
{
for (auto &thread : _threads)
{
thread.Join(); // 等待每个线程结束
}
}
// 任务处理函数(工作线程执行)
void HandlerTask()
{
char name[128];
pthread_getname_np(pthread_self(), name, sizeof(name)); // 获取线程名称
while (true) // 工作循环
{
T t; // 任务对象
{
pthread_mutex_lock(&_mutex); // 加锁
// 等待条件:1.队列为空 2.线程池在运行
while (_taskq.empty() && _isrunning)
{
_sleepernum++; // 增加休眠线程计数
pthread_cond_wait(&_cond, &_mutex); // 等待条件变量
_sleepernum--; // 减少休眠线程计数
}
// 检查是否应该退出
if (!_isrunning && _taskq.empty())
{
pthread_mutex_unlock(&_mutex); // 解锁
LOG(LogLevel::INFO) << name << " 退出了, 线程池退出&&任务队列为空";
break; // 退出循环
}
// 获取任务
t = _taskq.front(); // 从队列头部获取任务
_taskq.pop(); // 移除队列头部任务
pthread_mutex_unlock(&_mutex); // 解锁
}
t(); // 执行任务(在锁外执行)--------线程被唤醒后拿到download()函数并运行!!!!
}
}
// 向任务队列添加任务
bool Enqueue(const T &in)
{
if (_isrunning) // 只有运行状态才能添加任务
{
pthread_mutex_lock(&_mutex); // 加锁
_taskq.push(in); // 添加任务到队列
// 如果所有线程都在休眠,则唤醒一个
if (_threads.size() == _sleepernum)
WakeUpOne();
pthread_mutex_unlock(&_mutex); // 解锁
return true;
}
return false; // 线程池未运行,添加失败
}
// 析构函数
~ThreadPool()
{
pthread_mutex_destroy(&_mutex); // 销毁互斥锁
pthread_cond_destroy(&_cond); // 销毁条件变量
}
private:
std::vector<Thread> _threads; // 工作线程集合
int _num; // 线程数量
std::queue<T> _taskq; // 任务队列
pthread_cond_t _cond; // 条件变量
pthread_mutex_t _mutex; // 互斥锁
bool _isrunning; // 线程池运行状态
int _sleepernum; // 当前休眠线程数
static ThreadPool<T> *inc; // 单例指针---保证只有一个线程池
static pthread_mutex_t _lock; // 单例锁
};
// 静态成员初始化----要在类外面进行初始化
template <typename T>
ThreadPool<T> *ThreadPool<T>::inc = nullptr;
template <typename T>
pthread_mutex_t ThreadPool<T>::_lock = PTHREAD_MUTEX_INITIALIZER; // 静态锁初始化
}
Main.cc:
#include "Log.hpp"
#include "ThreadPool.hpp"
#include "Task.hpp"
#include <memory>
using namespace LogModule;
using namespace ThreadPoolModule;
int main()
{
Enable_Console_Log_Strategy();
// ThreadPool<task_t> tp;
// ThreadPool<task_t> tp1 = tp;
// ThreadPool<task_t> *tp = new ThreadPool<task_t>();
// tp->Start();
// 有一个单例了! 如果线程池本身,会被多线程获取呢??
int count = 10;
while (count)
{
sleep(1);
ThreadPool<task_t>::GetInstance()->Enqueue(Download);//ThreadPool<task_t>::GetInstance() 的调用并不依赖于实例化的对象,而是通过 静态成员函数 和 模板类的特性 实现的。
count--; //且threadpool<T>*inc 是静态stastic变量,所以可以不用实例化直接获取,一定是唯一的!!!!!!!
}
ThreadPool<task_t>::GetInstance()->Stop();
ThreadPool<task_t>::GetInstance()->Join();
return 0;
}
线程池运行流程与输出结果分析
代码概览
int main() {
Enable_Console_Log_Strategy();
int count = 10;
while (count--) {
sleep(1);
ThreadPool<task_t>::GetInstance()->Enqueue(Download);
}
ThreadPool<task_t>::GetInstance()->Stop();
ThreadPool<task_t>::GetInstance()->Join();
return 0;
}
完整运行流程
阶段1:线程池初始化
步骤 | 函数调用栈 | 关键操作 | 输出日志 |
---|---|---|---|
1 | main() → GetInstance() | 首次调用单例获取 | [DEBUG] 获取单例.... |
2 | ThreadPool<task_t>() | 构造线程池实例 | [DEBUG] 首次使用单例,创建之.... |
3 | Start() | 启动5个工作线程 | [INFO] start new thread success: thread-1 [INFO] start new thread success: thread-2 … [INFO] start new thread success: thread-5 |
阶段2:任务提交与处理(循环10次)
步骤 | 函数调用栈 | 关键操作 | 输出日志 |
---|---|---|---|
1 | Enqueue(Download) | 任务入队并唤醒线程 | [INFO] 唤醒一个休眠线程 |
2 | HandlerTask() | 工作线程获取任务 | [INFO] thread-[X] 处理下载任务... |
3 | Download() | 执行具体任务 | (假设Download函数输出) |
注:X为线程ID(1-5),任务处理顺序可能随机
阶段3:线程池关闭
步骤 | 函数调用栈 | 关键操作 | 输出日志 |
---|---|---|---|
1 | Stop() | 设置停止标志并唤醒所有线程 | [INFO] 唤醒所有的休眠线程 |
2 | HandlerTask() | 线程检查退出条件 | [INFO] thread-1 退出了,线程池退出&&任务队列为空 … [INFO] thread-5 退出了,线程池退出&&任务队列为空 |
3 | Join() | 等待线程结束 | [DEBUG] Join线程成功 |
完整输出示例
[DEBUG] 获取单例....
[DEBUG] 首次使用单例,创建之....
[INFO] start new thread success: thread-1
[INFO] start new thread success: thread-2
[INFO] start new thread success: thread-3
[INFO] start new thread success: thread-4
[INFO] start new thread success: thread-5
[INFO] 唤醒一个休眠线程
[INFO] thread-1 处理下载任务...
[INFO] 唤醒一个休眠线程
[INFO] thread-2 处理下载任务...
(...共10次任务提交...)
[INFO] 唤醒所有的休眠线程
[INFO] thread-1 退出了,线程池退出&&任务队列为空
[INFO] thread-2 退出了,线程池退出&&任务队列为空
[INFO] thread-3 退出了,线程池退出&&任务队列为空
[INFO] thread-4 退出了,线程池退出&&任务队列为空
[INFO] thread-5 退出了,线程池退出&&任务队列为空
[DEBUG] Join线程成功