勿忘初心做自己 发表于 2024-9-2 20:23:33

【项目日记】高并发内存池---实现线程缓存

https://i-blog.csdnimg.cn/direct/168cf911fb114a62bac547733f526557.png
   比起那些用大嗓门筹划压抑天下的人,    让全天下都安静下来听你小声语言的人更可畏。        --- 韩寒 《告白与告别》---       

1 框架计划

我们需要实现的是一个这样的结果:线程缓存(256KB)中每个空间位置映射到在哈希表上,对应一个自由链表,申请空间时从自由链表中取出一个对象,没有就去中心缓存进行申请!
看起来很轻易,但是这一句话之中引出了:

[*]自由链表 :这需要我们来计划,可以仿照定长池的接纳链表来计划。
[*]哈希映射规则:哈希映射需要很奇妙的进行计划,需要在一个数组中映射出一个大空间中!
[*]多线程优化:因为项目是针对多线程来进行的优化,以是要保证在多线程情况下可以保证效率!
[*]线程缓存类:需要可以申请空间,释放空间,空间不敷向上申请空间。
以是大致我们需要计划三个类:自由链表类,哈希规则类,线程缓存类。自由链表类和哈希规则类设置为公有类,方便中心缓存和页缓存使用。
//自由链表类
class FreeList
{
public:
        //进行头插
        void Push(void* obj)
        {
        }
        //头删
        void* Pop()
        {
        }
        //判断是否为空
        bool empty()
        {
        }
private:
        //内部是一个指针 指向头结点
        void* _freelist = nullptr;
};

//哈希映射规则类
class SizeClass
{
public:
        //计算对齐数
        static inline size_t RoundUp(size_t size)
        {
        }
        //计算映射的桶下标
        static inline size_t Index(size_t size)
        {
        }
};

class ThreadCache
{
public:
        //申请空间
        void* Allocate(size_t size);
        //释放空间
        void Deallocate(void* ptr, size_t size);
        //向中心缓存申请空间
        void* FetchFromCentralCache(size_tindex, size_t alignSize);
private:
        //哈希映射表
        FreeList _freelist;
};

2 自由链表类和哈希规则

我们先来实现自由链表类和哈希规则,这两个类都写入到Common.h头文件中,每个文件都可以进行访问!
2.1 自由链表类

自由链表类重要就是插入 和 删除。自由链表中每个节点都有一个指针的空间,可以指向后面的节点。通过这个我们就可以写出插入删除的大致逻辑!
class FreeList
{
private:
        void* NextObj(void* obj)
        {
                return *reinterpret_cast<void**>(obj);
        }
public:
        void Push(void* obj)
        {
                //进行头插
                void* next = NextObj(obj);
                next = _freelist;
                _freelist = obj;
        }
        void* Pop()
        {
                //头删
                void* obj = _freelist;
                void* next = NextObj(_freelist);
                _freelist = next;
                return obj;
        }
        bool empty()
        {
                return _freelist == nullptr;
        }
private:
        //内部是一个指针 指向头结点
        void* _freelist = nullptr;
};

2.2 映射规则

首先我们需要明确如何来进行映射,256KB的空间如果满是8字节对齐的对象,会产生三万多个这可不行!!!
以是需要进行一些特殊处置处罚:并且保证团体最多10%左右的内碎片(由对齐规则导致的内存碎片)浪费:
空间范围对齐规则链表中对应位置个数8 byte对齐freelist1616 byte对齐freelist56128 byte对齐freelist561024 byte对齐freelist568*1024 byte对齐freelist24   我们可以来计算一下内存的浪费率:


[*] 申请129字节时 ,按照对齐规则实际需要144字节的空间,那么就是浪费了15字节,
此时的空间浪费率是 15 / 144 = 0.10。此时是该区间内最坏的情况,
[*] 申请8 * 1024 + 1字节时,按照对齐规则需要9216字节的空间,那么就是浪费了1023字节,
此时空间浪费率是1023 / 9216 = 0.11,此时是该区间内最坏的情况,
以是综合来看,空间浪费率是在10%左右!
按照对齐规则可以将的空间大小都能够映射到一个自由链表中去申请对齐后的空间。并且这样只需要208个自由链表,远比30000+少多了奥!!!并且我们还保证了10%左右的空间浪费率!
接下来我们就将这段逻辑写成代码:

[*]RoundUp函数:这个函数是用来计算对齐后需要申请空间的大小,按照我们的几个分类写成多少个if条件判定语句,然后再通过子函数_RoundUp来进行最终的计算。进行对齐的计算为:

[*]如果不能整除就需要向上对齐( 申请空间大小 / 对齐数 + 1)* 对齐数
[*]能整除就直接是申请空间大小
[*]这里有一种非常奇妙的方法:(对齐数 - 1)取反 &(申请空间大小 + 对齐数 - 1)。
原理是对齐数 - 1取反后会得到对齐数对应大小的比特位右边的位置全为0!然后申请空间大小 + 对齐数 - 1会得到向上对齐的最大大小,在取交集,就会得到8的倍数的对齐空间大小了!

[*]Index函数:这个函数是用来找到申请空间大小对应的自由链表!同样也按几个分类写出多少个if条件判定语句,然后通过子函数_Index来进行最终的计算,这个就好理解了,首先先减去前面的不同对齐数的空间大小 ,然后计算在该区间属于第几个自由链表,然后在加上前面不同对齐数的链表数!
class SizeClass
{
private:
        //普通算法
        //static inline size_t _RoundUp(size_t size , size_t alignNum)
        //{
        //        size_t alignsize;
        //        //需要向上对齐
        //        if (size % alignNum != 0)
        //        {
        //                alignsize = (size / alignNum + 1) * alignNum;24 8
        //        }
        //        //已经对齐
        //        else
        //        {
        //                alignsize = size ;
        //        }
        //        return alignsize;
        //}
        //优秀算法
        static inline size_t _RoundUp(size_t size, size_t alignnum)
        {
                return ~(alignnum - 1) & (size + alignnum - 1);
        }
        static inline size_t _Index(size_t bytes, size_t align_shift)
        {
                //通过字节数返回对应的桶
                //       ( 字节数 + 对齐数 - 1 )/ 对齐数 - 1
                return ((bytes + (1 << align_shift) - 1) >> align_shift) - 1;
        }
public:
        static inline size_t RoundUp(size_t size)
        {
                assert(size < MAX_BYTES);
                //通过若干个if语句进行处理
                if (size <= 128)
                {
                        //按照8字节对齐
                        return _RoundUp(size, 8);
                }
                else if (size <= 1024)
                {
                        return _RoundUp(size, 16);
                }
                else if (size <= 8 * 1024)
                {
                        return _RoundUp(size, 128);
                }
                else if (size <= 64 * 1024)
                {
                        return _RoundUp(size, 1024);
                }
                else if (size <= 256 * 1024)
                {
                        return _RoundUp(size, 8*1024);
                }
                else
                {
                        assert(false);
                        return -1;
                }

        }
        static inline size_t Index(size_t size)
        {
                assert(size < MAX_BYTES);
                int num = { 16 , 56 , 56 , 56 , 24 };
                //通过若干个if语句进行处理
                if (size <= 128)
                {
                        //按照8字节对齐
                        return _Index(size, 3);
                }
                else if (size <= 1024)
                {
                        return _Index(size - 128 , 4) + num;
                }
                else if (size <= 8 * 1024)
                {
                        return _Index(size - 1024 , 7) + num + num;
                }
                else if (size <= 64 * 1024)
                {
                        return _Index(size - 8 * 1024, 10) + num + num + num;
                }
                else if (size <= 256 * 1024)
                {
                        return _Index(size - 64 * 1024, 13) + num + num + num + num;
                }
                else
                {
                        assert(false);
                        return -1;
                }
        }
};
3 实现线程缓存

我们做好了自由链表和哈希规则之后我们来进行线程缓存的实现!
3.1 申请内存

申请内存的逻辑很简朴,首先先通过哈希规则得到对齐后的空间大小和对应桶的下标,有了这两个元素我们就可以来进行申请空间!

[*]该空间大小对应的自由链表中有对象,我们就Pop出来一个就可以了!
[*]该空间大小对应的自由链表中没有对象,就需要向中心内存进行申请一个对齐后看空间大小的空间!
//申请空间
void* ThreadCache::Allocate(size_t size)
{
        //根据RoundUp计算出对齐后的内存大小
        size_t alignSize = SizeClass::RoundUp(size);
        //根据Index找到对应桶
        size_t index = SizeClass::Index(size);

        //进行取出数据
        if (!_freelist.empty())
        {
                return _freelist->Pop();
        }
        else
        {
                //向CentralCache申请内存!
                return FetchFromCentralCache(index, alignSize);
        }
}
3.2 释放内存

释放内存的逻辑更加简朴,将需要释放的对象空间直接Push到空间大小对应的自由链表中就可以了
//释放空间
void ThreadCache::Deallocate(void* ptr, size_t size)
{
        assert(ptr);
        assert(size < MAX_BYTES);
        //根据Index找到对应桶
        size_t index = SizeClass::Index(size);

        _freelist.Push(ptr);

}
这样我们就实现了线程缓存的部分!!!向中心缓存申请的部分还要比及完成中心缓存才可以进行联动!
4 多线程优化

因为我们的项目是要在多线程情况下进行运行,以是要保证线程缓存支持多线程,还要保证线程安全。
现在有个问题:线程缓存是一个类,如何保证每个线程内部都有一个线程缓存!!!
首先肯定是要创建一个全局变量,制止重复构造。但如果只是在主线程中创建一个全局变量,那么就会导致多个线程竞争这个公共资源。那么有没有一种方法可以在线程中创建专属的全局变量,方便进行使用吗、呢?
固然有:TLS( Thread Local Shortage 线程局部存储)无锁访问线程数据。通过这个我们就可以在线程中创建独属于该线程的全局变量。以是我们可以到场一个TSL全局变量;
//TLS( Thread Local Shortage 线程局部存储)无锁访问线程数据
//该写法仅限于VS系列编译器
_declspec(thread) static ThreadCache* pThreadCache = nullptr;
上层进行调用时,先判定pThreadCache是否为空,如果为空那么就创建一个哈希表。反之直接进行使用!这样就制止了使用锁来办理,不需要线程阻塞期待!效率大大提升啊!!!
5 运行测试

为了保证项目标没有BUG,我们要实时进行测试,我们完成了线程缓存,就要保证线程缓存没有问题:
我们先写一下高并发内存池申请内存的接口,将线程缓存使用起来!
// 线程开辟空间函数
// 1. void* ConcurrentAlloc(size_t size)
// 2. voidConcurrentFree(void* ptr)
//
void* ConcurrentAlloc(size_t size)
{
        //在该线程中进行内存的申请
        if (pThreadCache == nullptr)
        {
                pThreadCache = new ThreadCache;
        }
        //进行开辟空间
        void * obj = pThreadCache->Allocate(size);
        cout << std::this_thread::get_id() << " : " << pThreadCache << endl;
        return obj;
}
voidConcurrentFree(void* ptr , size_t size)
{
        assert(ptr);
        //回收空间
        pThreadCache->Deallocate(ptr, size);
}

测试代码:
#include"ObjectPool.h"
#include"ThreadCache.h"
#include"ConcurrentAlloc.h"
#include<windows.h>

//-------- 线程缓存测试 -------
void Alloc1()
{
        for (size_t i = 0; i < 5; i++)
        {
                Sleep(100);
                //申请内存
                ConcurrentAlloc(8);
        }
}
void Alloc2()
{
        for (size_t i = 0; i < 6; i++)
        {
                //申请内存
                ConcurrentAlloc(15);
                Sleep(100);
        }
}
void ThreadCacheTest()
{
        cout << "---- ThreadCacheTest() Begin! ----" << endl;
        std::thread t1(Alloc1);
        std::thread t2(Alloc2);

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

        cout << "---- ThreadCacheTest() Done! ----" << endl;
}

//---------------------------

int main()
{
        //TestObjectPool();
        //ThreadCache tc;
        ThreadCacheTest();
        return 0;
}
运行结果:
https://i-blog.csdnimg.cn/direct/65bfc1c9ad014961b51f89b5685f76c9.png
这样就看到每个线程都有对应的资自由链表数组!!!
非常好!!!接下来我们就来实现中心缓存!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 【项目日记】高并发内存池---实现线程缓存