【项目日记(四)】第一层: 线程缓存的具体实现

简介: 【项目日记(四)】第一层: 线程缓存的具体实现

1. 前言

由于此项目需要创建多个文件

所以我直接在.h文件中既放声明

也存放实现,减少文件的数量

本章重点:

本篇文章着重讲解ThreadCache线程
缓存结构的具体实现,包含内存对齐的
方法,申请/释放内存的函数以及向中心
缓存中索要/还回内存的函数!本篇文章
大多数都是代码实现,请大家耐心学习


2. ThreadCache结构设计

在上一章谈到,线程缓存是一个哈希桶结构

每个桶中存放的是自由链表(小块儿内存)

由于自由链表不止在ThreadCache中使用,所以定义一个shared.h存放所有文件共享的内容!

shared.h文件中的自由链表结构:

//管理切分好的小对象的自由链表
class FreeList
{
public:
  void Push(void* obj)
  {
    assert(obj);
    //头插
    *(void**)obj = _freeList;
    _freeList = obj;
    _size++;
  }
  void PushRange(void* start, void* end, size_t n)//范围型的插入
  {
    *(void**)end = _freeList;
    _freeList = start;
    _size += n;
  }
  void* Pop()
  {
    assert(_freeList);
    //头删
    void* obj = _freeList;
    _freeList = *(void**)obj;
    --_size;
    return obj;
  }
  void PopRange(void*& start, void*& end, size_t n)//start和end是输出型参数
  {
    assert(n <= _size);
    start = _freeList;
    end = start;
    for (int i = 0; i < n - 1; i++)
      end = *(void**)end;
    _freeList = *(void**)end;
    *(void**)end = nullptr;
    _size -= n;
  }
  bool Empty()
  {
    return _freeList == nullptr;
  }
  size_t& MaxSize()
  {
    return _maxSize;
  }
  size_t Size()
  {
    return _size;
  }
private:
  void* _freeList = nullptr;
  size_t _maxSize = 1;//记录ThreadCache一次性最多向CentralCache申请多少个对象
  size_t _size = 0;//记录自由链表中挂的内存个数
};

关于什么是自由链表的问题我们在
定长池的实现中已经讲解过了,如果
你不懂,简易从头开始看博客,学项目!

前置文章: 定长池的实现

然后在ThreadCache.h中,需要实现以下函数

class ThreadCache
{
public:
  //向线程缓存申请内存
  void* Allocate(size_t size);
  //向线程缓存释放内存
  void Deallocate(void* ptr, size_t size);
  // 从中心缓存获取对象
  void* FetchFromCentralCache(size_t index, size_t size);
  // 释放对象时,链表过长时,回收内存回到中心缓存
  void ListTooLong(FreeList& list, size_t size);
private:
  FreeList _freeList[208];
};

3. 哈希桶中的内存对齐规则

你可能会有以下问题:

  1. 为什么上面写的哈希桶个数是208?
  2. 要是遇见不规则的字节数该怎么办?

这里用到一个非常巧妙的方法,也就是内存对齐!比如想要申请1~7字节的内存,先把它对齐为8字节再进行后续的操作,这么做的原因有两个: 一是因为如果申请多少内存就给多少内存,那么我们需要的哈希桶的数量就太多了,不合理. 二是因为释放内存时比较方便

内存对齐的具体规则:

这里有一个问题: 为什么不都使用8字节对齐?

因为若使用8字节对齐,共256KB内存

一共要使用三万多个桶,也是不合理的

这样的对齐方式只用使用208个桶!

对齐规则的具体实现:

static inline size_t AlignUp(size_t size)//将size向上对齐
{
  if (size <= 128)
    return _AlignUp(size, 8);
  else if (size <= 1024)
    return _AlignUp(size, 16);
  else if (size <= 8 * 1024)
    return _AlignUp(size, 128);
  else if (size <= 64 * 1024)
    return _AlignUp(size, 1024);
  else if (size <= 256 * 1024)
    return _AlignUp(size, 8*1024);
  else
    return _AlignUp(size, 1 << PAGE_SHIFT);
}
static inline size_t _AlignUp(size_t size, size_t align_number)
{
  return (((size)+align_number - 1) & ~(align_number - 1));
}

4. 线程缓存申请内存的全过程

首先,要先把申请的内存进行内存对齐
然后再用这个对齐后的内存去找对应
的哈希桶结构,若这个桶中有小块儿内
存,那么直接将小块儿内存返回,若桶中
没有内存了,就去中心缓存中拿内存!

void* ThreadCache::Allocate(size_t size)
{
  assert(size <= MAX_BYTES);//申请的字节数不能大于了256KB
  size_t align_size = AlignmentRule::AlignUp(size);//计算对齐数
  size_t index = AlignmentRule::Index(size);//计算在哪个桶
  if (!_freeList[index].Empty())//若当前自由链表有资源则优先拿释放掉的资源
    return _freeList[index].Pop();
  else//自由链表没有就从中心缓存获取空间
    return ThreadCache::FetchFromCentralCache(index, align_size);
}

注:计算在哪个桶的函数会放在文章最后

当走到else语句,也就是要去中心缓存获取内存时,会有个问题:一次性拿几个小块儿内存过来?拿多了怕浪费,拿少了又怕要重新再来拿,这里采用的是慢启动的算法,第一次先拿一个,第二次再拿两个,以此类推.除此之外,小对象应该多拿,大对象应该少拿.所以拿的个数应该是这两个条件中较小的内一个

void* ThreadCache::FetchFromCentralCache(size_t index, size_t size)
{
  //采用慢开始的反馈调节算法,小对象给多一点,大对象给少一点
  size_t massNum = min(_freeList[index].MaxSize(),AlignmentRule::NumMoveSize(size));//向中心缓存获取多少个对象
  if (_freeList[index].MaxSize() == massNum)//慢增长,最开始一定是给1,不会一次性给它很多内存,若不断有size大小的内存需求,再慢慢多给你
    _freeList[index].MaxSize() += 1;
  void* begin = nullptr;
  void* end = nullptr;
  //需要massnum个对象,但是实际上不一定有这么多个,返回值为实际上获取到的个数
  size_t fact = CentralCache::GetInstance()->CentralCache::FetchRangeObj(begin,end,massNum,size);//要massmun个对象,每个对象的大小是size
  assert(fact != 0);
  if (fact == 1)
  {
    assert(begin == end);
    return begin;
  }
  else
  {
    //如果从中心缓存获取了多个,则将第一个返回给threadcachee,然后再将剩余的内存挂在threadcache的自由链表中
    _freeList[index].PushRange((*(void**)begin), end, fact - 1);
    return begin;
  }
  return nullptr;
}

注: 根据对象大小获取个数的函数放在文章最后

FetchRangeObj函数是中心缓存要关心的东西


5. 线程缓存释放内存的全过程

由于申请内存时已经内存对齐了,所以释放的内存肯定是已经对齐过的了,找到对应的桶,直接将这个小块儿内存挂在桶后面,若当换回来的内存在自由链表中的长度大于一次性向中心缓存申请的长度,就将内存还给中心缓存

void ThreadCache::Deallocate(void* ptr, size_t size)
{
  assert(ptr);
  assert(size <= MAX_BYTES);
  //找到对应的自由链表桶[[0,208]
  size_t index = AlignmentRule::Index(size);
  _freeList[index].Push(ptr);
  //当换回来的内存在自由链表中的长度大于一次性向中心缓存申请的长度,就将内存还给中心缓存
  if (_freeList[index].Size() >= _freeList[index].MaxSize())
    ListTooLong(_freeList[index],size);
}
void ThreadCache::ListTooLong(FreeList& list, size_t size)//大于等于一个批量内存就还回去一个批量,不一定全部还回去
{
  void* start = nullptr;
  void* end = nullptr;
  list.PopRange(start, end, list.MaxSize());//start和end是输出型参数
  CentralCache::GetInstance()->ReleaseListToSpans(start, size);
}

注:ReleaseListToSpans函数是中心缓存需要关心的


6. 对项目边角代码的拓展

这里存放的是shared.h文件中,存放

如何通过字节数来找到对应的桶的函数

以及慢增长函数的具体实现:

慢增长函数:

static const size_t MAX_BYTES = 256 * 1024; //最大内存限制,256KB
static const size_t N_FREE_LIST = 208; //哈希桶的数量
static size_t NumMoveSize(size_t size)//此函数代表从中心缓存取多少个对象(和慢增长对比)
{
  assert(size > 0);
  // [2, 512],一次批量移动多少个对象的(慢启动)上限值
  // 小对象一次批量上限高
  // 大对象一次批量上限低
  int num = MAX_BYTES / size; //256KB除单个对象大小
  if (num < 2)//最少给2个
    num = 2;
  if (num > 512)//最大给512个
    num = 512;
  return num;
}

通过字节数计算在哪个桶:

// 计算映射的哪一个自由链表桶
static inline size_t Index(size_t bytes)
{
  assert(bytes <= MAX_BYTES);
  // 每个区间有多少个链
  static int group_array[4] = { 16, 56, 56, 56 };
  if (bytes <= 128) 
    return _Index(bytes, 3);
  else if (bytes <= 1024) 
    return _Index(bytes - 128, 4) + group_array[0];
  else if (bytes <= 8 * 1024) 
    return _Index(bytes - 1024, 7) + group_array[1] + group_array[0];
  else if (bytes <= 64 * 1024) 
    return _Index(bytes - 8 * 1024, 10) + group_array[2] + group_array[1]
      + group_array[0];
  else if (bytes <= 256 * 1024) 
    return _Index(bytes - 64 * 1024, 13) + group_array[3] +
      group_array[2] + group_array[1] + group_array[0];
  else
    return -1;
}
static inline size_t _Index(size_t bytes, size_t align_shift)
{
  return ((bytes + (1 << align_shift) - 1) >> align_shift) - 1;
}

对于边角的函数,我们了解它的原理即可
不需要完全掌握它是如何实现的!


🔎 下期预告:中心缓存的具体实现🔍


相关文章
|
1月前
|
缓存 安全 算法
Java面试题:如何通过JVM参数调整GC行为以优化应用性能?如何使用synchronized和volatile关键字解决并发问题?如何使用ConcurrentHashMap实现线程安全的缓存?
Java面试题:如何通过JVM参数调整GC行为以优化应用性能?如何使用synchronized和volatile关键字解决并发问题?如何使用ConcurrentHashMap实现线程安全的缓存?
17 0
|
4天前
|
缓存 开发框架 .NET
看看 Asp.net core Webapi 项目如何优雅地使用内存缓存
看看 Asp.net core Webapi 项目如何优雅地使用内存缓存
|
4天前
|
Cloud Native Java 调度
项目环境测试问题之线程同步器会造成执行完任务的worker等待的情况如何解决
项目环境测试问题之线程同步器会造成执行完任务的worker等待的情况如何解决
|
4天前
|
存储 缓存 开发框架
看看 Asp.net core Webapi 项目如何优雅地使用分布式缓存
看看 Asp.net core Webapi 项目如何优雅地使用分布式缓存
|
22天前
|
存储 缓存 算法
同时使用线程本地变量以及对象缓存的问题
【7月更文挑战第15天】同时使用线程本地变量和对象缓存需小心处理以避免数据不一致、竞争条件及内存泄漏等问题。线程本地变量使各线程拥有独立存储,但若与对象缓存关联,可能导致多线程环境下访问旧数据。缺乏同步机制时,多线程并发修改缓存中的共享对象还会引起数据混乱。此外,若线程结束时未释放对象引用,可能导致内存泄漏。例如,在Web服务器场景下,若一更新缓存而另一线程仍获取旧数据,则可能返回错误信息;在图像处理应用中,若多线程无序修改算法对象则可能产生错误处理结果。因此,需确保数据一致性、避免竞争条件并妥善管理内存。
|
2月前
|
敏捷开发 缓存 测试技术
阿里云云效产品使用问题之构建Vue3项目,怎么让node_modules缓存下来
云效作为一款全面覆盖研发全生命周期管理的云端效能平台,致力于帮助企业实现高效协同、敏捷研发和持续交付。本合集收集整理了用户在使用云效过程中遇到的常见问题,问题涉及项目创建与管理、需求规划与迭代、代码托管与版本控制、自动化测试、持续集成与发布等方面。
|
1月前
|
设计模式 存储 缓存
Java面试题:结合设计模式与并发工具包实现高效缓存;多线程与内存管理优化实践;并发框架与设计模式在复杂系统中的应用
Java面试题:结合设计模式与并发工具包实现高效缓存;多线程与内存管理优化实践;并发框架与设计模式在复杂系统中的应用
36 0
|
1月前
|
设计模式 存储 缓存
Java面试题:结合单例模式与Java内存模型,设计一个线程安全的单例类?使用内存屏障与Java并发工具类,实现一个高效的并发缓存系统?结合观察者模式与Java并发框架,设计一个可扩展的事件处理系统
Java面试题:结合单例模式与Java内存模型,设计一个线程安全的单例类?使用内存屏障与Java并发工具类,实现一个高效的并发缓存系统?结合观察者模式与Java并发框架,设计一个可扩展的事件处理系统
20 0
|
1月前
|
监控 Java
Java面试题:Java内存、多线程与并发工具包的深度探索,Java内存管理策略及其优化技巧,Java多线程并发控制的工具类与机制,Java并发工具包在实际项目中的应用
Java面试题:Java内存、多线程与并发工具包的深度探索,Java内存管理策略及其优化技巧,Java多线程并发控制的工具类与机制,Java并发工具包在实际项目中的应用
20 0
|
2月前
|
Java Linux Shell
Linux环境下,让Jar项目多线程部署成为可能
Linux环境下,让Jar项目多线程部署成为可能
27 1