C++实现线程池、内存池

线程池

代码实现

#include <iostream>
#include <vector>
#include <queue>
#include <thread>
#include <functional>
#include <mutex>
#include <condition_variable>

class ThreadPool {
public:
    // 构造函数,创建指定数量的工作线程
    ThreadPool(size_t numThreads) : stop(false) {
        for (size_t i = 0; i < numThreads; ++i) {
            // 创建工作线程,并使用 Lambda 表达式定义线程函数
            workers.emplace_back([this] {
                // 工作线程主循环
                while (true) {
                    std::function<void()> task;
                    {      // 获取任务前先加锁,保证线程安全,
                        std::unique_lock<std::mutex> lock(queueMutex);
                        // 等待任务队列非空或线程池停止信号,等待的时候,会将锁释放掉
                        condition.wait(lock, [this] { return stop || !tasks.empty(); });
                        // 如果线程池停止并且任务队列为空,则退出线程
                        if (stop && tasks.empty()) {
                            return;
                        }
                        // 获取任务并从队列中弹出
                        task = tasks.front();
                        tasks.pop();
                    }
                    //离开作用域后,lock 会被销毁,从而释放互斥锁
                    //此时任务已经成功存储在 task 中,并且工作线程可以在不持有锁的情况下执行该任务。
                    // 释放锁后执行任务
                    task();
                }
            });
        }
    }

    // 将任务加入线程池
    template <class F, class... Args>
    void enqueue(F&& f, Args&&... args) {
        {
            // 加锁保护任务队列
            std::unique_lock<std::mutex> lock(queueMutex);
            // 将任务和参数绑定为可调用对象并加入队列
            tasks.emplace(std::bind(std::forward<F>(f), std::forward<Args>(args)...));
        }
        // 通知一个工作线程有任务可执行,条件满足会加锁
        condition.notify_one();
    }
    // 析构函数,等待所有工作线程执行完成,
    ~ThreadPool() {
        {
            std::unique_lock<std::mutex> lock(queueMutex);
            stop = true; // 设置停止信号,通知工作线程退出
        }
        //离开作用域,锁被释放掉
        condition.notify_all(); // 通知所有工作线程退出
        // 等待所有工作线程执行完成
        for (std::thread& worker : workers) {
            worker.join();
        }
    }

private:
    std::vector<std::thread> workers; // 工作线程数组
    std::queue<std::function<void()>> tasks; // 任务队列

    std::mutex queueMutex; // 任务队列锁
    std::condition_variable condition; // 条件变量,用于通知工作线程
    bool stop; // 停止信号,用于通知工作线程退出
};

// 示例用法
void task1(int x) {
    std::cout << "Task 1 executed with argument " << x << std::endl;
}

void task2(const std::string& str) {
    std::cout << "Task 2 executed with argument " << str << std::endl;
}

int main() {
    ThreadPool pool(4); // 创建一个包含4个工作线程的线程池

    // 将任务加入线程池并执行
    pool.enqueue(task1, 42);
    pool.enqueue(task2, "Hello, ThreadPool!");

    // 主线程等待所有任务完成
    std::this_thread::sleep_for(std::chrono::seconds(1));

    return 0;
}

代码中知识点

  • Lambda
    lambda表达式,使用[]代替了函数名,成为一个函数对象,可以直接当作参数
[this] { return stop || !tasks.empty(); }
用法:[z]代表按值访问变量,[=]按值访问所有的动态变量
还有其它的用法见《CPP》
  • 为啥使用lambda代替函数指针
    主要目的:让程序员能够将类似于函数的表达式用作接受函数指针或函数符的函数的参数。使用lambda,无需翻阅多页的源代码,就可以看到参数的内容,如果修改代码,涉及的内容将都在附近。而函数指针是比较糟糕的,因为不能在函数内部定义其他的函数,内容可能在很远的地方。
  • 包装器function
    function<void()>是C++中的一个通用函数对象封装类模板,它可以用来封装任意可调用对象(例如函数,lambda表达式、成员函数),并提供一种统一的方式来调用这些对象。
    std::function<void()> 来封装任务。std::function<void()> 表示一个不带参数且无返回值的可调用对象。在线程池中,任务是由 std::function<void()> 表示的,这样可以统一封装各种类型的任务,使其可以方便地放入任务队列中并由工作线程执行。
  • emplace
    当我们使用 std::queue 容器时,向队列中添加元素一般有两种方式:使用 push 或 emplace。这两种方式都可以用来将元素添加到队列中,但有一些细微的区别。
    1、使用 push 函数时,我们需要传递具体的元素值作为参数,这会调用元素类型的拷贝构造函数,并将拷贝的副本存储在队列中。
    2、使用 emplace 函数时,我们传递的参数会被直接传递给元素类型的构造函数,并在队列中就地构造一个新的元素,避免了拷贝构造的开销。
    同vector的emplace_back()与push_back()的区别
  • bind()
    在这里插入图片描述
    参数是T&&通用引用。所以要使用forward()
    std::bind 是 C++11 中引入的一个非常强大的函数对象库,它允许我们将函数及其参数绑定为一个可调用对象,从而在稍后的时候调用该函数时,可以携带绑定的参数。
  • forward
    在这里插入图片描述
    forward()返回的是T&&通用引用。它可以接收任意类型的参数,无论传递的是左值还是右值。在目标函数中保持与原始参数类型一致。这就是完美转发的优势,可以灵活地处理传递的参数,并保持其原始类型。所以很适应模板函数。
    1、作用
    std::forward 的作用就是根据形参类型来决定将传入参数转发为左值引用或右值引用,能够避免值类型和引用类型的副本或损失,并确保传递的参数在目标函数中的类型与原始类型保持一致。在函数模板中,std::forward 通常与函数模板参数包(variadic template)一起使用,可以将多个参数以完美转发(T&&)的方式传递给其他函数。
    在上述的线程池实现中,std::forward(f) 和 std::forward(args)… 分别用于将传递给 enqueue 函数的可调用对象和额外参数以完美转发的方式传递给 std::bind 函数,确保 std::bind 可以正确地进行绑定并保留原始参数的值类型和左值/右值属性。
    2、为啥使用forward
    如果直接使用传统的参数传递方式(如按值传递或按引用传递),可能会导致一些问题:
    2.1、 如果使用按值传递,原始参数的值类型和左值/右值属性会丢失,传递给目标函数的是复制的值,而不是原始参数。(我们不想传递副本)
    2.1 、如果使用按引用传递,虽然可以保持原始参数的左值/右值属性,但是传递给目标函数的是引用,而不是原始参数的值。(不想修改数据)
    当使用完美转发技术(std::forward)时,我们希望在传递参数给其他函数时保留参数的值类型和左值/右值属性【可以实现传递原始数据,且不会修改原始数据,这样就和原始数据一致了】。这样,接收参数的函数可以正确地处理原始参数,而不是参数的副本,也不会修改参数的值类型和属性。
    通过完美转发,我们可以实现在不同函数之间传递参数时保持参数类型和属性的一致性,从而确保函数操作的是原始参数,并且可以正确地处理左值引用、右值引用以及普通值类型的参数。这在泛型编程和模板函数中特别有用,可以提高代码的灵活性和性能。

内存池

代码实现

#include<iostream>
#include<vector>
#include<cstring>
using namespace std;
class MemoryPool {
private:
	vector<char*>memoryChunks;//存储内存块的容器
	char*freeList;//空闲内存块链表的头指针
	size_t chunkSize;//内存块的总大小
	size_t blockSize;//单个内存块的大小
	size_t totalBlocks;//每个内存池中的内存块数量
public:
MemoryPool(size_t blockSize, size_t totalBlocks) :blockSize(blockSize), totalBlocks(totalBlocks) {
	chunkSize = blockSize * totalBlocks;
	freeList = nullptr;
}

~MemoryPool() {
	//释放所有内存块
	for (auto chunk : memoryChunks) {
		delete[]chunk;
	}
}
void *allocate() {
	if (!freeList) {
		//如果空闲链表为空,则分配一个新的内存块
		char* newChunk = new char[chunkSize];
		memoryChunks.push_back(newChunk);

		//初始化新分配的内存池的空闲链表
		for (size_t i = 0; i < totalBlocks - 1; i++) {
			//将当前内存块的下一个内存块地址保存在当前内存的起始地址
			//reinterpret_cast<size_t*>(newChunk + i * blockSize)
			//将内存块的起始地址转换为 size_t 类型的指针。
			//这里的目的是为了将起始地址视为一个无符号整数,方便后续链表结构的链接。
			*reinterpret_cast<size_t*>(newChunk + i * blockSize) = reinterpret_cast<size_t>(newChunk + (i + 1) * blockSize);
		}
		//最后一个内存块的起始地址设置为0,表示链表结束
		*reinterpret_cast<size_t*>(newChunk + (totalBlocks - 1) * blockSize) = 0;

		//将新内存块的投作为新的空闲链表头
		freeList = newChunk;//freeList指向的链表起始地址,相当于虚拟节点
	}
	//从空闲链表中取出一个空闲内存块,并更新空闲链表表指针
	void* freeBlock = freeList;
	freeList = reinterpret_cast<char *>(*reinterpret_cast<size_t *>(freeList));
	return freeBlock;
	}

void deallocate(void*ptr) {
	//将释放的内存块加入空闲链表的头部,头插法,不需要变量,减少开销
	char*block = reinterpret_cast<char*>(ptr);
	*reinterpret_cast<size_t*>(block) = reinterpret_cast<size_t>(freeList);
	freeList = block;
	}
};

int main() {
	MemoryPool memoryPool(sizeof(int), 10);//4 * 10;

	//分配两个int型内存块,并分别赋值
	int*num1 = static_cast<int*>(memoryPool.allocate());
	*num1 = 42;
	int*num2 = static_cast<int*>(memoryPool.allocate());
	*num2 = 99;
	// 输出两个内存块中的值
	std::cout << *num1 << std::endl;
	std::cout << *num2 << std::endl;
	//释放内存块
	memoryPool.deallocate(num1);
	memoryPool.deallocate(num2);

	return 0;
}

代码中的知识点

  • 为什么使用size_t而不是具体的类型
    主要是为了方便进行内存地址的操作和管理。
    1、size_t是一个无符号整数类型,在不同的平台上都有固定的字节大小,通常为4字节或8字节。使用size_t作为链表节点指针的数据类型可以确保指针在不同平台上具有一致的大小,从而提高代码的可移植性。而使用char*类型作为指针可能会引起一些计算上的困难和错误。
  • 说一下C++中四种强制转换
  • 1、静态转换(static_cast<type_name> expression):
    静态转换是最常见的强制转换,在大多数情况下用于显示转换,比如整数类型之间的转换,浮点数类型之间的转换,以及父子类指针或引用之间的转换。从基类指针到派生类,在不进行显示类型转换的情况下,将无法进行。但是只要满足type_name转换为expression所属的类型或expression可被隐式转换为type_name,就可以实现转换,所以可以通过它进行向下转换。静态转换编译器不会进行类型检查。static_cast中提供了空指针与任何类型指针的互相转换。
// 整数类型之间的转换
int num1 = 10;
double num2 = static_cast<double>(num1);

// 父子类指针之间的转换
class Base {};
class Derived : public Base {};
Base* basePtr = new Derived;
Derived* derivedPtr = static_cast<Derived*>(basePtr);
  • 2、动态转换(dynamic_cast):
    动态转换主要用于处理多态类型的指针或引用之间的转换。它在运行时进行类型检查,如果转换不合法,会返回空指针(对于指针)或抛出std::bad_cast异常(对于引用);dynamic_cast同样支持进行上下行类型转换,但编译器会进行类型检查。当下行转换时,如果父类中不包括虚函数,则编译器会报错,所以必须确保父类中有虚函数。另外,dynamic_cast不支持空指针与一般指针之间的转换,转换前检查指针是否有效,以避免空指针引用。

为什么必须包含虚函数
下行转换是将父类指针或引用转换为子类指针或引用。当父类中包含虚函数时,对象的运行时类型信息会在虚函数表中,这样 dynamic_cast 可以通过运行时类型信息来检查是否可以进行转换,以避免转换到错误的子类类型。
如果父类没有虚函数,则运行时类型信息不会存在于虚函数表中,因此 dynamic_cast 无法进行正确的类型检查,编译器无法确定转换的安全性,因此会报错。

class Animal {
public:
    virtual ~Animal() {}
};
class Dog : public Animal {};

Animal* animalPtr = new Dog;//父指向子类
Dog* dogPtr = dynamic_cast<Dog*>(animalPtr); // 正确的转换,返回非空指针

Animal* anotherAnimalPtr = new Animal;
Dog* anotherDogPtr = dynamic_cast<Dog*>(anotherAnimalPtr); // 错误的转换,返回空指针

为什么Dog* anotherDogPtr = dynamic_cast<Dog*>(anotherAnimalPtr)会错误
在进行 dynamic_cast 的时候,会检查 anotherAnimalPtr 指向的实际对象的类型是否是 Dog 类型或其派生类。由于 anotherAnimalPtr 实际指向的是 Animal 类型的对象,而不是 Dog 类型或其派生类的对象,所以类型转换是不合法的。必须确保指针指向的实际对象类型是派生类或其派生类。

  • 3、重新解释转换(reinterpret_cast):
    重新解释转换用于不同类型之间的二进制位的重新解释。它通常用于低级别的转换,比如将指针转换为整数类型或将整数类型转换为指针。由于重新解释转换不进行类型检查,使用时需谨慎,因为可能导致未定义行为。
int num = 42;
void* voidPtr = reinterpret_cast<void*>(&num); // 将int类型的指针转换为void指针

int* intPtr = reinterpret_cast<int*>(voidPtr); // 将void指针重新转换为int类型的指针
  • 4、常量转换(const_cast):
    常量转换用于去除指针或引用的const或volatile属性。它通常用于函数重载时,用于去除函数参数的const属性,以便调用正确的函数重载。转换前后的变量并无区别,内存中的地址也是相同的。
void func(int* ptr) {
    *ptr = 10;
}

const int num = 5;
func(const_cast<int*>(&num)); // 去除num的const属性,调用非常量版本的func函数

讲一下volatile变量
编译器在优化代码时会尽可能地利用变量的值存储在寄存器中,以提高程序的性能,不是每次访问时都从内存中读取,可能会导致程序出现错误的行为。使用 volatile 关键字告诉编译器不要对该变量进行优化,每次访问该变量都应该从内存中读取。这样可以确保在程序的不同部分对该变量的访问都是一致的,避免出现意外的错误。
需要注意的是,volatile 修饰符只能确保对变量的单线程可见性,而不能保证多线程之间的同步。在多线程编程中,如果要保证多个线程对共享变量的访问是同步的,还需要使用互斥锁或其他同步机制。

写到最后

本人是在准备秋招阶段整理的,为后续方便复习。有错误还请大佬指出,万分谢谢!!!
后续可能会更新进程池!!!有帮助的话,点个赞吧!哈哈~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值