项目2 |内存池1|基于哈希桶的多种定长内存池

Alloc内存分配器

vector类

cpp 复制代码
namespace std {
    // T: 你要存什么类型的数据 (比如 int)
    // Alloc: 你想用什么工具来管理内存 (默认送你一个 std::allocator<T>)
    template < typename T, typename Alloc = std::allocator<T> > 
    class vector {
        // ... vector 的内部实现 ...
    };
}
cpp 复制代码
// 模板类:如果你不传 Alloc,Alloc 默认就是 std::allocator<T>
std::vector<int> v; 
// 等价于 ->
std::vector<int, std::allocator<int>> v;

当我学会了内存池,

于是代码就会变成这样:

cpp 复制代码
// 定义一个使用 MyPoolAllocator 的 vector 类型
std::vector<int, MyPoolAllocator<int>> v;

内存碎片

内部碎片

已分配的内存块未被实际使用的部分

外部碎片

分配不出去的内存。

自旋锁

  1. test_and_set()的作用是检测这个锁是false或true,然后再将锁置为true
  2. std::memory_order_acquire只是配合test_and_set()的一个参数,保证他是原子操作
cpp 复制代码
#include <iostream>
#include <atomic>  // 必须包含这个库
#include <thread>
#include <vector>

class SpinLock {
private:
    // std::atomic_flag 是 C++ 中唯一保证"无锁"的原子类型
    // ATOMIC_FLAG_INIT 初始化为"清除"状态 (也就是 false / 未上锁)
    std::atomic_flag flag = ATOMIC_FLAG_INIT;

public:
    void lock() {
        // test_and_set 做两件事:
        // 1. 把 flag 设置为 true (上锁)。
        // 2. 返回 flag **之前** 的值。
        // 如果之前是 true (别人锁着),返回 true -> while 循环继续转 (Spin)。
        // 如果之前是 false (没人锁),返回 false -> while 循环结束 -> 我拿到了锁!
        
        // 这里用 acquire,保证我拿到锁之后,能看见别人改的数据
        while (flag.test_and_set(std::memory_order_acquire)) {
            // 这里通常放一个 CPU hint,告诉 CPU 我在空转 (可选,但推荐)
            // std::this_thread::yield(); // 或者 _mm_pause();
        }
    }

    void unlock() {
        // 清除标志位 (设为 false)
        // 这里用 release,保证我改完的数据,能被下一个拿锁的人看见
        flag.clear(std::memory_order_release);
    }
};

// --- 测试代码 ---

SpinLock mySpinLock;
int counter = 0; // 共享资源

void work() {
    for (int i = 0; i < 100000; ++i) {
        mySpinLock.lock();   // 1. 加锁 (忙等待)
        counter++;           // 2. 临界区 (很快的操作)
        mySpinLock.unlock(); // 3. 解锁
    }
}

int main() {
    std::thread t1(work);
    std::thread t2(work);

    t1.join();
    t2.join();

    // 如果没有锁,结果肯定小于 200000
    std::cout << "Final counter: " << counter << std::endl;

    return 0;
}

原子操作无锁

  • CAS(比较并交换)
    举一个简单的CAS例子,就是先检查a是否=b,然后交换(a,c)或(b,c)
cpp 复制代码
a.CAS(b,c);
if(a==b) a=c;
if(a!=b) b=c  (下一轮)==> a.CAS(c,c)这样只要a没有被别人改变,这轮就会出去。
这就有点乐观锁的样子。没人动a我就动了,有人动a我就一直循环。

把代码中的互斥锁改进一点

我往freeList中前插一个【结点X】

freeList_ (头指针) -> [节点 A]

freeList_ -> [节点 X] -> [节点 A]

  • 在 C++ 中,它长这样: `现在的值.compare_exchange_weak(预料值, 渴望指)
cpp 复制代码
bool MemoryPool::pushFreeList(Slot* X) {
	while(true){
		 x->next=A;

			//如果不用CAS,没有互斥要求
			//代码只是简单的一局freeList_=X;

		//有了互斥操作 就是if(FreeList_=A)  freeList_=X
		//                if(freeList_!=A)  A=X,再执行下一轮
		 if (freeList_.compare_exchange_weak(A, X)
		 {return true;}
	}
}	

内存池的三层结构

门卫Hash Bucket

前台MemoryPool

房间Real Memory

读源码的思考

神奇的结构体

cpp 复制代码
struct Slot 
{
    Slot* next;
};
  1. 作用1.在申请的内存池块中,前8个字节用来把各个内存池串起来。(前8个B内存池不使用)
  2. 作用2.释放过的槽,前8个字节把这几个槽串起来。(因为这些都是空空的内存,占用8个无所谓)

哈希桶HashBucket

搞了好久,终于是给盘明白了。

  1. 哈希桶就是处理哈希冲突的一种方式,学名叫"链地址法"(Chaining)。

  2. 哈希 是一种思想。y=f(x)这就是哈希!!!

  3. 哈希桶其实就是unordered_map的特列。他的哈希关系f(x)是自己写的,unordered_map是stl规定的。

  4. 一个简单的哈希桶雏形。由一个链表数组组成。

cpp 复制代码
#include <iostream>
#include <vector>
#include <list> // 用标准库的链表来模拟"桶"的深度

// 定义一个哈希桶类
class MyHashMap {
private:
    // 1. 桶的个数:这里我们只准备 10 个桶
    static const int BUCKET_COUNT = 10;

    // 2. 真正的"桶"数组
    // 这是一个数组,数组的每个元素是一个链表(list)
    // 链表里存的是 pair<学号, 分数>
    std::list<std::pair<int, int>> buckets[BUCKET_COUNT];

    // 3. 哈希函数:决定你去哪个桶
    int hashFunction(int key) {
        return key % BUCKET_COUNT; // 取个模,结果一定是 0-9
    }

public:
    // 插入数据
    void insert(int id, int score) {
        int index = hashFunction(id); // 算出桶号
        
        // 把数据"扔"进对应的桶里(链表尾部插入)
        buckets[index].push_back({id, score});
        
        std::cout << "学号 " << id << " -> 放入了 " << index << " 号桶" << std::endl;
    }

    // 查找数据
    int findScore(int id) {
        int index = hashFunction(id); // 先算桶号,直接定位到那个桶
        
        // 然后在这个桶(链表)里挨个找(线性查找)
        // 只要桶里的数据不多,这个过程非常快
        for (auto& pair : buckets[index]) {
            if (pair.first == id) {
                return pair.second; // 找到了!
            }
        }
        return -1; // 没找到
    }
};

int main() {
    MyHashMap myMap;

    // 模拟插入
    myMap.insert(11, 95); // 11 % 10 = 1,去1号桶
    myMap.insert(21, 88); // 21 % 10 = 1,也去1号桶(冲突了,但没关系,桶里能装)
    myMap.insert(35, 60); // 35 % 10 = 5,去5号桶

    // 模拟查找
    std::cout << "查找学号 21 的成绩: " << myMap.findScore(21) << std::endl;

    return 0;
}

单例模式

单实例对象,操作来操作去都是针对一个对象

memoryPool的单例模式为什么不在memoryPool的类中写,要在hashbucket中写?

如果是在memoryPool类中写,就不能体现缓存池的差异化了。而且本身缓存池就是哈希桶中的桶,又哈希桶来进行管理。

static void initMemoryPool()/static MemoryPool& getMemoryPool(int index);这个两个函数的定义为什么不像其他Hashbucket类中的函数一样,在类里面定义。反而是在cpp文件中定义?

首先我们得知道函数定义写在类中,编译器会把这些函数当做内联函数处理。

内联函数就是,在处理函数调用的时候,不会在回调call,而是直接在这里展开函数。

这两个函数使用频率很低,所以就不设置为内联函数喽!

缓存池数组要设计成单例模式,为什么哈希桶不也设计成单例模式呢?哈希桶整个程序也是独一份啊?

  1. 首先HashBucket 其实已经实现了"精神上的单例",但它采用了一种更轻量级、性能更高的手段------全静态成员。
  2. 其次单例模式是给会实例化变成对象的类用的。HashBucket是一个无成员变量的函数,他没有实例化的需要,所以不需要设计成单例模式。
  3. 所以哈希桶是一种哈希y=f(x)的规则,内存池是一个对象!!!

operator new(size)

与 new a()不同 。= 分配内存 + 在上面构造对象。

operator new(size)= 只申请这么一块空间。

void* 与nullptr的区别

复制代码
void* 是无类型指针,`void* useMemory(size_t size)`是有返回值的,不是没有
nullptr是空指针。

new T()与 new(ptr)T()的区别?

  • 普通 new: new T() = 申请新内存 + 构造。

  • 定位构造。new§ T() = 指针p之前有申请过内存,只是在上面没有创建对象。现在直接在已有的地址 p 上,调用构造函数把对象造出来。

项目测试(重要,先看这)

线程的创建

cpp 复制代码
vthread[k] = std::thread([&]() {
。。。
}

如果不写Lambda表达式就得

cpp 复制代码
// 1. 必须先在外面定义一个函数,用来干活
// 注意:还要把所有需要的变量通过参数传进去,很麻烦
void worker_task(size_t rounds, size_t ntimes, long long& total_costtime) {
    for (size_t j = 0; j < rounds; ++j) {
        // ... 你的测试逻辑 ...
    }
}

// 2. 然后在创建线程时,传入函数名和参数
vthread[k] = std::thread(worker_task, rounds, ntimes, std::ref(total_costtime));

在主线程中定义的size_t total_costtime = 0; 为什么新线程也能使用total_costtime += end1 - begin1;

复制代码
因为Lambda函数有一个【&】,可以把主线程中的变量全部拿过来用,哈哈哈哈哈哈哈。

join()函数

复制代码
就是主线程会在这里等待,等新线程返回。

根据测试代码捋清思路

  1. HashBucket::initMemoryPool();

    • 创建了几个对象(一个对象数组),同时规定对象的槽大小。
    • 没有哈希桶就是这几个对象数组。
  2. BenchmarkMemoryPool(100, 1, 10); 进入P1* p1 = newElement<P1>();

    • 假设sizeof(P1) 是 8字节(对应 0号池子,SlotSize=8)
    • 进入useMemory(8) 算出来找 0号池子。
    • 进入 0号池子的对象重。memoryPool[0].allocate()。
  3. 现在是memoryPool对象的逻辑。

    • freeList_ 是 nullptr? 是。(没人退货),curSlot_ 是 nullptr 吗? 是。(还没进过货),curSlot==lastSlot,触发 allocateNewBlock(),进入 allocateNewBlock()
    • 调用系统: operator new(4096)。更新firstBlock_,curSlot_,lastSlot_。
    • allocateNewBlock()结束后,回到allocate中,把P1类型放入第一个槽中,curSlot_再次后移,返回指向第一个槽的指针给useMemory(),再返回给newElement()
    • newElement()就 new(返回的槽指针) P1();
相关推荐
Elias不吃糖3 小时前
LeetCode每日一练(209, 167)
数据结构·c++·算法·leetcode
铁手飞鹰4 小时前
单链表(C语言,手撕)
数据结构·c++·算法·c·单链表
悦悦子a啊4 小时前
项目案例作业(选做):使用文件改造已有信息系统
java·开发语言·算法
小殊小殊4 小时前
【论文笔记】知识蒸馏的全面综述
人工智能·算法·机器学习
无限进步_4 小时前
C语言动态内存管理:掌握malloc、calloc、realloc和free的实战应用
c语言·开发语言·c++·git·算法·github·visual studio
im_AMBER4 小时前
AI井字棋项目开发笔记
前端·笔记·学习·算法
TT哇5 小时前
【BFS 解决拓扑排序】3. ⽕星词典(hard)
redis·算法·宽度优先
橘颂TA5 小时前
【剑斩OFFER】算法的暴力美学——判定字符是否唯一
算法·c/c++·结构与算法
ModestCoder_5 小时前
PPO-clip算法在Gymnasium的Pendulum环境实现
人工智能·算法·机器人·具身智能