【高并发内存池】项目介绍 + 定长内存池 + 整体框架设计

高并发内存池

  • [1. 项目介绍](#1. 项目介绍)
    • [1.1 这个项目做的是什么](#1.1 这个项目做的是什么)
    • [1.2 为什么做这个项目](#1.2 为什么做这个项目)
    • [1.3 这个项目要求的知识储备](#1.3 这个项目要求的知识储备)
  • [2. 什么是内存池](#2. 什么是内存池)
    • [2.1 池化技术](#2.1 池化技术)
    • [2.2 内存池](#2.2 内存池)
    • [2.3 内存池主要解决的问题](#2.3 内存池主要解决的问题)
    • [2.4 malloc](#2.4 malloc)
  • [3. 开胃菜 -- 先设计一个定长的内存池](#3. 开胃菜 -- 先设计一个定长的内存池)
  • [4. 高并发内存池整体框架设计](#4. 高并发内存池整体框架设计)

点赞 👍👍收藏 🌟🌟关注 💖💖
你的支持是对我最大的鼓励,我们一起努力吧!😃😃

1. 项目介绍

1.1 这个项目做的是什么

当前项目是实现一个高并发的内存池,他的原型是google的⼀个开源项目tcmalloc,tcmalloc全称Thread-Caching Malloc,即线程缓存的malloc,实现了高效的多线程内存管理,用于替代系统的内存分配相关的函数(malloc、free)。

我们这个项目是把tcmalloc最核新的框架简化后拿出来,模拟实现出一个自己的高并发内存池,目的就是学习tcamlloc的精华。

1.2 为什么做这个项目

malloc‌通常是C标准库中的内存分配函数,用于基本的内存分配需求。什么场景下都可以用,但是什么场景下都可以用就意味着什么场景下都不会有很高的性能。并且它没有特别针对多线程进行优化,因此在多线程环境中可能会遇到性能瓶颈和锁竞争问题‌。而tcmalloc‌在多线程环境中表现出色,由于其优化的内存管理和内部缓存机制,在相同条件下比malloc显著提高了性能,减少了总耗时。特别是在大量内存分配和释放的操作中,tcmalloc能够更快地完成任务‌。

tcmalloc‌适用于需要高并发和大规模内存操作的应用。‌而malloc‌适用于简单的单线程应用或对性能要求不高的场景。

tcmalloc源码

1.3 这个项目要求的知识储备

这个项目会用到C/C++、数据结构(链表、哈希桶)、操作系统内存管理、单例模式、多线程、互斥锁等等方面的知识。

2. 什么是内存池

2.1 池化技术

所谓"池化技术",就是程序先向系统申请过量的资源,然后自己管理,以备不时之需。之所以要申请过量的资源,是因为每次申请该资源都有较大的开销,不如提前申请好了,这样使用时就会变得非常快捷,大大提高程序运行效率。

在计算机中,有很多使用"池"这种技术的地方,除了内存池,还有连接池、线程池、对象池等。以服务器上的线程池为例,它的主要思想是:先启动若干数量的线程,让它们处于睡眠状态,当接收到客户端的请求时,唤醒池中某个睡眠的线程,让它来处理客户端的请求,当处理完这个请求,线程有进入睡眠状态。

2.2 内存池

内存池是指程序预先从操作系统申请一块足够大内存,此后,当程序中需要申请内存的时候,不是直接向操作系统申请,而是直接从内存池中获取;同理,当程序释放内存的时候,并不真正将内存返回给操作系统,而是返回内存池。当程序退出(或者特定时间)时,内存池才将之前申请的内存真正释放。

2.3 内存池主要解决的问题

内存池 主要解决的当然是效率的问题 (频繁申请小块内存,一次申请大快内存),其次如果作为系统的内存分配器的角度,还需要解决一下内存碎片的问题。那么什么是内存碎片呢?

需要补充说明的是内存碎片分为外碎片和内碎片,。外部碎片 是一些空闲的连续内存区域太小,这些内存空间不连续,以至于合计的内存足够,但是不能满足一些的内存分配申请需求。内部碎片是由于一些对齐的需求,导致分配出去的空间中一些内存无法被利用(比如说你要5字节,但是由于内存对齐,给你一个8字节的内存)。

2.4 malloc

C/C++中我们要动态申请内存都是通过malloc去申请内存,但是我们要知道,实际我们不是直接去堆获取内存的,

malloc实际就是一个内存池。malloc() 相当于向操作系统"批发"了一块较大的内存空间,然后"零售"给程序用。当全部"售完"或程序有大量的内存需求时,再根据实际需求向操作系统"进货"。malloc的实现方式有很多种,一般不同编译器平台用的都是不同的。比如windows的vs系列用的微软自己写的一套,linux gcc用的glibc中的ptmalloc。下面有几篇关于这块的文章,大概可以去简单看看了解一下。

一文了解,Linux内存管理,malloc、free 实现原理

malloc()背后的实现原理------内存池

malloc的底层实现(ptmalloc)

3. 开胃菜 -- 先设计一个定长的内存池

作为程序员(C/C++)我们知道申请内存使用的是malloc,malloc其实就是⼀个通用的大众货,什么场景下都可以用,但是什么场景下都可以用就意味着什么场景下都不会有很高的性能,下面我们就先来设计一个定长内存池做个开胃菜,当然这个定长内存池在我们后面的高并发内存池中也是有价值的,所以学习它目的有两层,先熟悉一下简单内存池是如何控制的,第二它会作为我们后面内存池的一个基础组件。

定长内存池一次向堆要一大块连续的内存,当要内存的找我,释放内存不是直接还给堆,而是给我,我给你管理起来,等下次你在继续找我要。因此它需要有两个指针,_memory指向申请的一大块内存,_freelist指向释放的内存并且用链表方式连接起来,这个链表叫做自由链表

这里有一个问题。自由链表中当前内存块如何知道下一个内存块的地址?

肯定是要保存下一块内存的地址,那如何保存呢?

可以这样做,取出当前内存块前4字节的地址,保存下一块的地址。如何取出当前内存块前4个字节地址呢?

我们可以把指针强制类型转化为int*,然后在解引用, 例如这样*(int*)ptr,就可以取到前4个字节了。

但是要注意的是,32位下地址4个字节,64位下地址8个字节,因此不能就直接把地址转成int*,而是转成void**,在解引用,例如*(void**)ptr,这样在32位下取4字节,64位下取8字节。

未来这个项目是可移值得,即可能放在windows也可能放在linux,但是平台不同,它们找堆申请内存是不一样的,所以我们可以来个条件编译。

windows和Linux下如何直接向堆申请页为单位的大块内存:
VirtualAlloc
brk和mmap

cpp 复制代码
#pragma once

#ifdef _WIN32
#include<windows.h>
#else
//linux
#endif

// 直接去堆上按页申请空间
inline static void* SystemAlloc(size_t kpage)
{
#ifdef _WIN32
	//申请kpage页,每页8kb
	void* ptr = VirtualAlloc(0, kpage << 13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
#else
	// linux下brk mmap等
#endif

	if (ptr == nullptr)
		throw std::bad_alloc();

	return ptr;
}

template<class T>
class ObjectPool
{
public:
	ObjectPool()
		:_memory(nullptr), _remainBytes(0),_freelist(nullptr)
	{}

	T* New()
	{
		T* obj = nullptr;
		//如果链表中有空闲的内存块,就优先使用
		if (_freelist)
		{
			//头删
			void* next = *(void**)_freelist;
			obj = (T*)_freelist;
			_freelist = next;
		}
		else
		{
			//if (_memory == nullptr)//不能直接判断释放为空,因为内存是连续的
			if(_remainBytes < sizeof(T))//如果当前内存块不够一个T类型,就重新申请一大快内存快
			{
				_remainBytes = 128 * 1024;
				//_memory = (char*)malloc(_remain);
				_memory = (char*)SystemAlloc(_remainBytes >> 13);//找对申请,按页为单位,想要每页8kb,申请多少页
				if (_memory == nullptr)
				{
					throw std::bad_alloc();
				}
			}
			obj = (T*)_memory;
			//保证内存块至少能放下一个地址
			size_t Size = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
			_memory += Size;
			_remainBytes -= Size;
		}

		// 定位new,显示调用T的构造函数初始化
		new(obj)T;

		return obj;
	}

	void Delete(T* obj)
	{
		// 显示调用析构函数清理对象
		obj->~T();

		//直接头插
		* (void**)obj = _freelist;
		_freelist = obj;
	}

private:
	char* _memory;//指向申请的一大块内存
	size_t _remainBytes;//记录申请一大块内存还剩下多少内存
	void* _freelist;//指向释放后的内存,以链表形式管理起来
};

4. 高并发内存池整体框架设计

现代很多的开发环境都是多核多线程,在申请内存的场景下,必然存在激烈的锁竞争问题。malloc本身其实已经很优秀,那么我们项目的原型tcmalloc是在多线程高并发的场景下更胜一筹,所以这次我们实现的内存池需要考虑以下几方面的问题。

  1. 性能问题
  2. 多线程环境下,锁竞争问题
  3. 内存碎片问题

concurrent memory pool主要由以下3个部分构成:

  1. thread cache :线程缓存是每个线程独有的,用于小于256KB的内存的分配,线程从这里申请内存不需要加锁,每个线程独享一个cache,这也就是这个并发线程池高效的地方。

  2. central cache :中心缓存是所有线程所共享,thread cache是按需从central cache中获取 的对象。central cache合适的时机回收thread cache中的对象,避免一个线程占用了太多的内存,而其他线程的内存吃紧,达到内存分配在多个线程中更均衡的按需调度的目的 。central cache是存在竞争的,所以从这里取内存对象是需要加锁,首先这里用的是桶锁,其次只有thread cache的没有内存对象时才会找central cache,所以这里竞争不会很激烈

  3. page cache :页缓存是在central cache缓存上面的一层缓存,存储的内存是以页为单位存储及分配的,central cache没有内存对象时,从page cache分配出一个span它管理一定数量的page,并切割成定长大小的小块内存,分配给central cache。当一个span的几个跨度页的对象都回收以后,page cache会回收central cache满足条件的span对象,并且合并相邻的页,组成更大的页,缓解内存碎片的问题。

总结一下,每个线程都有自己的ThreadCach,内存小于256KB的时候都找自己的ThreadCache要(大于256KB后面在说),这样多线程要内存就先不要加锁了。如果ThreadCache中没有了,就再去找CentralCache要,CentralCache是所有线程共享的,因此需要加锁,但是它加的是桶锁,所以锁的竞争没有那么激烈。当ThreadCache中空闲内存太多了CentralCache还会进行回收,避免一个线程栈用太多内存,导致其他线程内存吃紧,让内存分配在多下次中更加均衡了。如果CentralCache中没有合适的内存,就去找PageCache要,PageCache是以页为单位对内存进行存储和分配的。当一个span中几个跨度页的对象都回收以后,PageCache会回收CentralCache满足条件的span对象,并且合并相邻的页,组成更大的页,缓解内存碎片的问题。当PageCache不够就去找堆要。

关于这个高并发内存池,有申请内存也有释放内存,为了更简单一些,我们先说申请,在说释放,最后在针对它进行细节处理以及优化。

相关推荐
星空露珠4 分钟前
迷你世界脚本方块接口:Block
数据结构·游戏·lua
柃歌6 分钟前
【UCB CS 61B SP24】Lecture 19 & 20: Hashing & Hashing II 学习笔记
java·数据结构·笔记·学习·算法
HBryce2438 分钟前
《数据结构》
java·数据结构
创益无界1 小时前
visual Studio Code安装
ide·visual studio
Lenyiin1 小时前
第151场双周赛:将数组按照奇偶性转化、可行数组的数目、移除所有数组元素的最小代价、全排列 Ⅳ
c++·算法·leetcode·周赛·lenyiin
Mr.pyZhang2 小时前
安卓基础组件Looper - 03 java层面的剖析
android·java·数据结构·epoll
Ace'2 小时前
每日一题之R格式
c++·算法
The_era_achievs_hero2 小时前
c++上课题目
数据结构·c++·算法
EutoCool2 小时前
Linux:同步
linux·服务器·c++
Maryhuan2 小时前
【开源-常用C/C++命令行解析库对比】
c语言·开发语言·c++