C++实现定长内存池

news2024/12/26 22:31:42

项目介绍

        本项目实现的是一个高并发的内存池,它的原型是Google的一个开源项目tcmalloc,tcmalloc全称Thread-Caching Malloc,即线程缓存的malloc,实现了高效的多线程内存管理,用于替换系统的内存分配相关函数malloc和free。tcmalloc的知名度也是非常高的,不少公司都在用它,比如Go语言就直接用它做了自己的内存分配器。该项目就是把tcmalloc中最核心的框架简化后拿出来,模拟实现出一个mini版的高并发内存池,目的就是学习tcmalloc的精华。该项目主要涉及C/C++、数据结构(链表、哈希桶)、操作系统内存管理、单例模式、多线程、互斥锁等方面的技术。

内存池介绍

池化技术

  在说内存池之前,我们得先了解一下“池化技术”。所谓“池化技术”,就是程序先向系统申请过量的资源,然后自己进行管理,以备不时之需。

  之所以要申请过量的资源,是因为申请和释放资源都有较大的开销,不如提前申请一些资源放入“池”中,当需要资源时直接从“池”中获取,不需要时就将该资源重新放回“池”中即可。这样使用时就会变得非常快捷,可以大大提高程序的运行效率。

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

内存池

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

内存池主要解决的问题

  内存池主要解决的就是效率的问题,它能够避免让程序频繁的向系统申请和释放内存。这里我们可以举一个例子,就好比我们要生活费,今天早上你吃了一碗面花了五元,然后你里面打电话告诉你的爸爸妈妈说今天早上花了五元吃面,你给我转5元,中午吃了一个黄焖鸡,花了12元,告诉爸爸妈妈说今天中午花了12元,那么给我转12元,我们会发现这里太低效了,每次都需要向爸爸妈妈要,不如这样,告诉爸爸妈妈你这个月大概需要800元生活费,让爸爸妈妈一次性给你,这样你就不需要频繁的打电话告诉爸爸妈妈要生活费,非常高效,其次,内存池作为系统的内存分配器,还需要尝试解决内存碎片的问题。

内存碎片分为内部碎片和外部碎片:

外部碎片是一些空闲的小块内存区域,由于这些内存空间不连续,以至于合计的内存足够,但是不能满足一些内存分配申请需求。
内部碎片是由于一些对齐的需求,导致分配出去的空间中一些内存无法被利用。
注意: 内存池尝试解决的是外部碎片的问题,同时也尽可能的减少内部碎片的产生。

malloc

  我们之前一直说C/C++我们要申请内存,我们就需要在堆空间中申请,但是实际上C/C++中我们要动态申请内存一般情况下并不是直接去堆申请的,而是通过malloc函数去申请的,包括C++中的new实际上也是调用了operator new,它底层也是封装了malloc函数的,因为它要符合C++抛异常的机制。

我们申请内存块时是先调用malloc,malloc再去向操作系统申请内存。malloc实际就是一个内存池,malloc相当于向操作系统“批发”了一块较大的内存空间,然后“零售”给程序用,当全部“售完”或程序有大量的内存需求时,再根据实际需求向操作系统“进货”。

malloc的实现方式有很多种,一般不同编译器平台用的都是不同的。比如Windows的VS系列中的malloc就是微软自行实现的,而Linux下的gcc用的是glibc中的ptmalloc。

定长内存池的实现

        malloc其实就是一个通用的内存池,在什么场景下都可以使用,但这也意味着malloc在什么场景下都不会有很高的性能,因为malloc并不是针对某种场景专门设计的。

  定长内存池就是针对固定大小内存块的申请和释放的内存池,由于定长内存池只需要支持固定大小内存块的申请和释放,因此我们可以将其性能做到极致,并且在实现定长内存池时不需要考虑内存碎片等问题,因为我们申请/释放的都是固定大小的内存块。

  我们可以通过实现定长内存池来熟悉一下对简单内存池的控制,其次,这个定长内存池后面会作为高并发内存池的一个基础组件。既然我们这里是实现定长的内存池,所以我们第一步就需要实现定长的功能,如何实现呢?

使用我们的非类型模板参数,表示此时申请的对象的空间大小都是N。

// 非类型的模板参数
template <size_t N>
class ObjectPool
{

};

还有另外一种方式,根据传入的模板参数的大小来获得申请的空间,比如传入的是int,那就是4个字节,传入的是double,那就是8个字节。

// 模板参数
// 定长内存池
template <class T>
class ObjectPool
{

};

我们现在实现的是第二种方式,为了更贴切第二中方式的含义,这也就是为什么我们给这个类起名为ObjectPool,因为它是根据对象的大小来申请空间的,我们再来看看定长内存池的需要一些什么样的成员呢?首先我们肯定需要一大块的堆空间内存

// 模板参数
// 定长内存池
template <class T>
class ObjectPool
{
private:
	void* _memory; // 申请一大块堆上的内存空间的指针
};

此时我们看看我们设计的成员变量有没有什么问题,我们申请的这一大块的堆空间内存,首先我们肯定是要划分出一段空间来供调用者使用,比如调用者说他需要10个字节的空间,难道此时我们直接将memory的起始地址并且加上10,将这个段空间给调用者嘛?此时我们要记住,void*它是不支持解引用和加加减减的操作的,所以此时要想切割出10个字节的空间,我们就需要将void*强制类型转换为char*才可以,那为什么我们不直接将memory的类型设置为char*呢?这样不就更方便一点嘛!

// 模板参数
// 定长内存池
template <class T>
class ObjectPool
{
private:
	char* _memory; // 申请一大块堆上的内存空间的指针
};

同时我们应该还要想到,未来有很多个人需要不同大小的内存空间,于是就想我们的内存池中拿,但是未来当他们部分人用完了,要归还的时候,我们不能直接把它释放归还给操作系统,因为我们当时申请了多少就应该释放多少,同时我们也不能归还到我们的内存池,这样会出现碎片化的问题,所以我们是不是还要对这些释用完的内存进行管理起来,我们可以将这些释放回来的定长内存块链接成一个链表,这里我们将管理释放回来的内存块的链表叫做自由链表,为了能找到这个自由链表,我们还需要一个指向自由链表的指针,此时在这个链表中,我们不进行解引用和加加减减的操作,并且我们也不知道归还的内存的指针的类型,我们此时指针的类型可以定义成void*。此时有一个小细节,我们这个内存块的空间在32位平台下内存块空间至少大于4字节,因为我们还要存储下一个内存块的指针,这样我们才能管理起来,我们规定内存块的前4个字节存储下一个内存块的地址。

// 模板参数
// 定长内存池
template <class T>
class ObjectPool
{
private:
	char* _memory; // 申请一大块堆上的内存空间的指针
	void* _freeList; // 自由链表
};

随后我们利用构造函数的初始化列表将成员进行初始化,这里我们都设置位空指针即可。

// 模板参数
// 定长内存池
template <class T>
class ObjectPool
{
public:
	ObjectPool()
		: _memory(nullptr)
		, _freeList(nullptr)
	{}
private:
	char* _memory; // 申请一大块堆上的内存空间的指针
	void* _freeList; // 自由链表
};

随后我就要申请一大块内存空间,如果此时申请空间失败,我们抛出一个异常,并让程序直接退出。

# include <iostream>

using  std::cin; 
using  std::cout;
using std::bad_alloc;

// 模板参数
// 定长内存池
template <class T>
class ObjectPool
{
public:
	ObjectPool()
		: _memory(nullptr)
		, _freeList(nullptr)
	{}
	T* New()
	{
		if (_memory == nullptr)
		{
			// malloc的返回值是void*,这里需要强制类型转换
			_memory = (char*)malloc(128 * 1024); // 128KB
			if (_memory == nullptr)
			{
				throw bad_alloc();
			}
		}
	}
private:
	char* _memory; // 申请一大块堆上的内存空间的指针
	void* _freeList; // 自由链表
};

现在我们就可以根据传入的模板参数T的类型,给它分配一个T*的空间。

// 模板参数
// 定长内存池
template <class T>
class ObjectPool
{
public:
	ObjectPool()
		: _memory(nullptr)
		, _freeList(nullptr)
	{}
	T* New()
	{
		if (_memory == nullptr)
		{
			// malloc的返回值是void*,这里需要强制类型转换
			_memory = (char*)malloc(128 * 1024); // 128KB
			if (_memory == nullptr)
			{
				throw bad_alloc();
			}
		}

		T* obj = (T*)_memory;
		_memory += sizeof(T);

		return obj;
	}
private:
	char* _memory; // 申请一大块堆上的内存空间的指针
	void* _freeList; // 自由链表
};

我们看看此时我们的代码有没有问题,当我们申请的空间都被用完的时候,此时我们应该再去申请空间,但是此时的_memory的指针不为空指针,此时还是有地址的,不过此时不属于你,你是不可以使用的,所以我们的代码还是有问题的,我们不应该使用_memory为空来判断申请空间,它只适用于第一次申请空间的情况,此时为了解决这个问题,我们还可以定义一个成员变量,表示剩余空间的大小,然后根据这个来判断是否需要申请空间。

template <class T>
class ObjectPool
{
public:
	ObjectPool()
		: _memory(nullptr)
		, _freeList(nullptr)
	{}
	T* New()
	{
		if (_remainBytes == 0)
		{
			// malloc的返回值是void*,这里需要强制类型转换
			_remainBytes = 128 * 1024;
			_memory = (char*)malloc(_remainBytes); // 128KB
			if (_memory == nullptr)
			{
				throw bad_alloc();
			}
		}

		T* obj = (T*)_memory;
		_memory += sizeof(T);
		_remainBytes -= sizeof(T);
		return obj;
	}
private:
	char* _memory; // 申请一大块堆上的内存空间的指针
	void* _freeList; // 自由链表
	size_t _remainBytes = 0;  //指向大块内存在切分过程中剩余字节数
};

同时我们这里如果最后剩余的空间不够一个T类型的大小,那么我们上面的判断还是有问题的,此时就可能出现越界的问题,所以我们还要改一下判断。

// 模板参数
// 定长内存池
template <class T>
class ObjectPool
{
public:
	ObjectPool()
		: _memory(nullptr)
		, _freeList(nullptr)
	{}
	T* New()
	{
		// 当剩余的内存不够一个对象大小时,需要重现开一个大块空间
		if (_remainBytes < sizeof(T))
		{
			// malloc的返回值是void*,这里需要强制类型转换
			_remainBytes = 128 * 1024;
			_memory = (char*)malloc(_remainBytes); // 128KB
			if (_memory == nullptr)
			{
				throw bad_alloc();
			}
		}

		T* obj = (T*)_memory;
		_memory += sizeof(T);
		_remainBytes -= sizeof(T);
		return obj;
	}
private:
	char* _memory; // 申请一大块堆上的内存空间的指针
	void* _freeList; // 自由链表
	size_t _remainBytes = 0;  //指向大块内存在切分过程中剩余字节数
};

现在我们再来处理一下对象归还回来的空间,改怎么处理呢?当现在有一个内存块归还了,我们要让freeList指向这个内存块,这个内存的前4个字节指向NULL,怎么做呢?我们肯定需要找到这个4字节,可以直接对象归还回来的空间强制类型转换为int*,此时解引用就可以访问到这个这个空间,然后将其设置为空。

void Delete(T* obj)
{
	if (_freeList == nullptr)
	{
		_freeList = obj;
		*((int*)obj) = nullptr;
	}
}

但是还有点问题,指针在不同的平台下指针的大小是不同的,32位平台下指针的大小是4个字节,64位平台下的指针的大小是8个字节,所以我们的代码不具有移植性,但是我们怎么知道我们的平台是32位的还是64位的呢?当然我们可以通过sizeof来判断,但是不够优雅,我们写一个优雅的方法。

void Delete(T* obj)
{
	if (_freeList == nullptr)
	{
		_freeList = obj;
		// *((int*)obj) = nullptr;
		*((void**)obj) = nullptr;
	}
}

此时我们是将obj强制类型转化位void**,void**解引用看的就是一个void*的大小,此时void*是一个指针,32位平台下指针的大小是4个字节,64位平台下的指针的大小是8个字节,很好的就做到了平台的区分,随后在来归还一个内存块,此时肯定要进自由链表来管理的,此时是头插还是尾插呢?我们知道尾插需要找尾,时间复杂度尾O(N),而头插的效率为O(1),所以我们选择头插法。

void Delete(T* obj)
{
	if (_freeList == nullptr)
	{
		_freeList = obj;
		// *((int*)obj) = nullptr;
		*((void**)obj) = _freeList;
	}
	else
	{
		// 头插法
		*((void**)obj) = _freeList;
		_freeList = obj;
	}
}

此时我们会发现我们的头插方法也适用于freeList为空的情况,所以我们的代码就可以简省一点。

void Delete(T* obj)
{
	// 头插法
	*((void**)obj) = _freeList;
	_freeList = obj;
}

同时我们这里需要注意一个点,我们并不是每次都会向我们的大块内存拿空间,如果我们的freeList里面有了很多归还的空间,我们可以来使用一下他们,此时就需要拿ferrList的第一个空间块,需要进行头删。

// 模板参数
// 定长内存池
template <class T>
class ObjectPool
{
public:
	ObjectPool()
		: _memory(nullptr)
		, _freeList(nullptr)
	{}
	T* New()
	{
		T* obj = nullptr;
		// 优先把还回来的内存块对象,再次重复利用
		if (_freeList != nullptr)
		{
			void* next = *((void**)_freeList);
			obj = (T*)_freeList;
			_freeList = next;
			return obj;
		}
		else
		{
			// 当剩余的内存不够一个对象大小时,需要重现开一个大块空间
			if (_remainBytes < sizeof(T))
			{
				// malloc的返回值是void*,这里需要强制类型转换
				_remainBytes = 128 * 1024;
				_memory = (char*)malloc(_remainBytes); // 128KB
				if (_memory == nullptr)
				{
					throw bad_alloc();
				}
			}

			obj = (T*)_memory;
			_memory += sizeof(T);
			_remainBytes -= sizeof(T);
			return obj;
		}
	}
	void Delete(T* obj)
	{
		// 头插法
		*((void**)obj) = _freeList;
		_freeList = obj;
	}
private:
	char* _memory; // 申请一大块堆上的内存空间的指针
	void* _freeList; // 自由链表
	size_t _remainBytes = 0;  //指向大块内存在切分过程中剩余字节数
};

同时我们上面的代码还存在一个问题,如果此时调用者需要的空间只需要一个字节,那么此时它要归还的时候,存储自由链表中,但是节点中至少要大于4个字节,不然存储不了下一个内存块的指针啊,所以我们这里还需要处理一下,对于不满足存储一个指针的空间,我们将空间的大小设置为指针的大小。

// 模板参数
// 定长内存池
template <class T>
class ObjectPool
{
public:
	ObjectPool()
		: _memory(nullptr)
		, _freeList(nullptr)
	{}
	T* New()
	{
		T* obj = nullptr;
		// 优先把还回来的内存块对象,再次重复利用
		if (_freeList != nullptr)
		{
			void* next = *((void**)_freeList);
			obj = (T*)_freeList;
			_freeList = next;
		}
		else
		{
			// 当剩余的内存不够一个对象大小时,需要重现开一个大块空间
			if (_remainBytes < sizeof(T))
			{
				// malloc的返回值是void*,这里需要强制类型转换
				_remainBytes = 128 * 1024;
				_memory = (char*)malloc(_remainBytes); // 128KB
				if (_memory == nullptr)
				{
					throw bad_alloc();
				}
			}

			obj = (T*)_memory;
			size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
			_memory += objSize;
			_remainBytes -= objSize;
		}
		return obj;
	}
	void Delete(T* obj)
	{
		// 头插法
		*((void**)obj) = _freeList;
		_freeList = obj;
	}
private:
	char* _memory; // 申请一大块堆上的内存空间的指针
	void* _freeList; // 自由链表
	size_t _remainBytes = 0;  //指向大块内存在切分过程中剩余字节数
};

同时我们这里申请了空间,但是我们没有初始化,我们可以将传入的对象通过构造函数进行初始化,并且再归还的时候调用析构函数释放对象。

// 模板参数
// 定长内存池
template <class T>
class ObjectPool
{
public:
	ObjectPool()
		: _memory(nullptr)
		, _freeList(nullptr)
	{}
	T* New()
	{
		T* obj = nullptr;
		// 优先把还回来的内存块对象,再次重复利用
		if (_freeList != nullptr)
		{
			void* next = *((void**)_freeList);
			obj = (T*)_freeList;
			_freeList = next;
		}
		else
		{
			// 当剩余的内存不够一个对象大小时,需要重现开一个大块空间
			if (_remainBytes < sizeof(T))
			{
				// malloc的返回值是void*,这里需要强制类型转换
				_remainBytes = 128 * 1024;
				_memory = (char*)malloc(_remainBytes); // 128KB
				if (_memory == nullptr)
				{
					throw bad_alloc();
				}
			}

			obj = (T*)_memory;
			size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
			_memory += objSize;
			_remainBytes -= objSize;
		}

		// 定位new,显示调用构造函数进行初始化
		new(obj)T;
		return obj;
	}
	void Delete(T* obj)
	{
		// 显示调用析构函数清理对象
		obj->~T();
		// 头插法
		*((void**)obj) = _freeList;
		_freeList = obj;
	}
private:
	char* _memory; // 申请一大块堆上的内存空间的指针
	void* _freeList; // 自由链表
	size_t _remainBytes = 0;  //指向大块内存在切分过程中剩余字节数
};

此时我们的定长内存池就已经实现完成啦!我们来测试一下。

struct TreeNode
{
	int _val;
	TreeNode* _left;
	TreeNode* _right;
	TreeNode()
		:_val(0)
		, _left(nullptr)
		, _right(nullptr)
	{}
};

void TestObjectPool()
{
	// 申请释放的轮次
	const size_t Rounds = 3;
	// 每轮申请释放多少次
	const size_t N = 1000000;
	std::vector<TreeNode*> v1;
	v1.reserve(N);

	//malloc和free
	size_t begin1 = clock();
	for (size_t j = 0; j < Rounds; ++j)
	{
		for (int i = 0; i < N; ++i)
		{
			v1.push_back(new TreeNode);
		}
		for (int i = 0; i < N; ++i)
		{
			delete v1[i];
		}
		v1.clear();
	}
	size_t end1 = clock();

	//定长内存池
	ObjectPool<TreeNode> TNPool;
	std::vector<TreeNode*> v2;
	v2.reserve(N);
	size_t begin2 = clock();
	for (size_t j = 0; j < Rounds; ++j)
	{
		for (int i = 0; i < N; ++i)
		{
			v2.push_back(TNPool.New());
		}
		for (int i = 0; i < N; ++i)
		{
			TNPool.Delete(v2[i]);
		}
		v2.clear();
	}
	size_t end2 = clock();

	cout << "new cost time:" << end1 - begin1 << endl;
	cout << "object pool cost time:" << end2 - begin2 << endl;
}

我们上面也提到,我们的malloc本章也是一个内存池,如果我们不想要这个内存池,我们也可以自己去堆上申请空间,此时在Windows下,可以调用VirtualAlloc函数;在Linux下,可以调用brk或mmap函数。

#ifdef _WIN32
	#include <Windows.h>
#else
	//...
#endif

//直接去堆上申请按页申请空间
inline static void* SystemAlloc(size_t kpage)
{
#ifdef _WIN32
	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;
}

可以看到在这个过程中,定长内存池消耗的时间比malloc/free消耗的时间要短。这就是因为malloc是一个通用的内存池,而定长内存池是专门针对申请定长对象而设计的,因此在这种特殊场景下定长内存池的效率更高,正所谓“尺有所短,寸有所长”,最后我们这里的定长内存也不需要手动释放,因为我们也无法释放,因为归还的内存已经乱了,那此时不就会出现内存泄漏的问题嘛?不会,只要我们的进程是正常退出的,最后会自动帮我们释放内存的,所以不用担心。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1698077.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Mysql之主从同步

1.BinLog同步机制 Mysql要去保证高可用&#xff0c;或者去分担请求压力&#xff0c;一般会去主从部署&#xff0c;读写分离。写库只负责写&#xff0c;而读库更多的去承担读的请求&#xff0c;从库不写数据&#xff0c;数据从主库同步&#xff0c;那么到底是怎么同步的呢&…

【2024】HNCTF

Web Please_RCE_Me GET传参输入?moranflag&#xff0c;之后获取源码&#xff1a;<?php if($_GET[moran] flag){highlight_file(__FILE__);if(isset($_POST[task])&&isset($_POST[flag])){$str1 $_POST[task];$str2 $_POST[flag];if(preg_match(/system|eval|a…

【C#】未能加载文件或程序集“CefSharp.Core.Runtime.dll”或它的某一个依赖项。找不到指定的模块。

欢迎来到《小5讲堂》 这是《C#》系列文章&#xff0c;每篇文章将以博主理解的角度展开讲解。 温馨提示&#xff1a;博主能力有限&#xff0c;理解水平有限&#xff0c;若有不对之处望指正&#xff01; 目录 背景错误提示分析原因解决方法Chromium知识点相关文章 背景 最近在使…

RabbitMQ-默认读、写方式介绍

1、RabbitMQ简介 rabbitmq是一个开源的消息中间件&#xff0c;主要有以下用途&#xff0c;分别是&#xff1a; 应用解耦&#xff1a;通过使用RabbitMQ&#xff0c;不同的应用程序之间可以通过消息进行通信&#xff0c;从而降低应用程序之间的直接依赖性&#xff0c;提高系统的…

有什么普通人可以做的赚钱软件?盘点9个适合普通人长期做的软件

在这个互联网高速发展的时代&#xff0c;智能手机已经成为我们生活中不可分割的一部分。众多APP的涌现&#xff0c;使得许多朋友都在寻求通过手机赚钱的方法。 然而&#xff0c;面对市面上琳琅满目的网上赚钱APP&#xff0c;我们该如何挑选呢&#xff1f;别担心&#xff0c;今…

python web自动化(验证码处理)

1.解决验证码问题的常⻅⼏种⽅式 1&#xff09; Debug模式启动浏览器&#xff08;浏览器复⽤&#xff09;&#xff1a; 原理&#xff1a;浏览器是有缓存记录的&#xff0c;只需要 沿⽤已经保存有登录记录的浏览器 进⾏后续的操作就⾏ 2&#xff09;识别法&#xff1a; 原理…

pycharm中,出现SyntaxError: Non-ASCII character ‘\xe4‘ in file... 的问题以及解决方法

文章目录 一、问题描述二、解决方法 一、问题描述 在pycharm中&#xff0c;使用python中编写中文字符时&#xff0c;会提示如下错误信息&#xff1a; SyntaxError: Non-ASCII character \xe4 in file ...... on line 8, but no encoding declared; see http://python.org/dev…

网上比较受认可的赚钱软件有哪些?众多兼职选择中总有一个适合你

在这个互联网高速发展的时代&#xff0c;网上赚钱似乎成了一种潮流。但是&#xff0c;你是否还在靠运气寻找赚钱的机会&#xff1f;是否还在为找不到靠谱的兼职平台而苦恼&#xff1f; 今天&#xff0c;就为你揭秘那些真正靠谱的网上赚钱平台&#xff0c;让你的赚钱之路不再迷…

MySQL--InnoDB体系结构

目录 一、物理存储结构 二、表空间 1.数据表空间介绍 2.数据表空间迁移 3.共享表空间 4.临时表空间 5.undo表空间 三、InnoDB内存结构 1.innodb_buffer_pool 2.innodb_log_buffer 四、InnoDB 8.0结构图例 五、InnoDB重要参数 1.redo log刷新磁盘策略 2.刷盘方式&…

S1E45:单链表1 课后作业

测试题&#xff1a;0. 相比起数组来说&#xff0c;单链表具有哪些优势呢&#xff1f; 答&#xff1a;长度非固定&#xff0c;可以申请添加长度 答案&#xff1a;对于数组来说&#xff0c;随机插入或者删除其中间的某一个元素&#xff0c;都是需要大量的移动操作&#xff0c;而…

基于tcp实现自定义应用层协议

认识协议 协议&#xff08;Protocol&#xff09; 是一种通信规则或标准&#xff0c;用于定义通信双方或多方之间如何交互和传输数据。在计算机网络和通信系统中&#xff0c;协议规定了通信实体之间信息交换的格式、顺序、定时以及有关同步等事宜的约定。简易来说协议就是通信…

网络工程师---第三十八天

ISIS&#xff1a; ISIS含义&#xff1a;中间系统到中间系统IS-IS。 ISIS特点&#xff1a;①内部网关协议IGP&#xff08;Interior Gateway Protocol&#xff09;&#xff0c;用于自治系统内部&#xff1b; ②IS-IS也是一种链路状态协议&#xff0c;使用最短路径优先SPF算法进…

电子阅览室在管理时需注意什么

关于如今的绝大多数人来说&#xff0c;想必都听说过“电子阅览室”这一概念。它首要运用在校园中&#xff0c;给学生们供给愈加丰厚的常识储藏。它也是一个独立的局域网&#xff0c;在校园网络中作为重要的一个组成部分而存在。但是&#xff0c;一个好的电子阅览室是需求满意运…

python文件IO基础知识

目录 1.open函数打开文件 2.文件对象读写数据和关闭 3.文本文件和二进制文件的区别 4.编码和解码 读写文本文件时 读写二进制文件时 5.文件指针位置 6.文件缓存区与flush()方法 1.open函数打开文件 使用 open 函数创建一个文件对象&#xff0c;read 方法来读取数据&…

Docker学习(4):部署web项目

一、部署vue项目 在home目录下创建项目目录 将打包好的vue项目放入该目录下&#xff0c;dist是打包好的vue项目 在项目目录下&#xff0c;编辑default.conf 内容如下&#xff1a; server {listen 80;server_name localhost; # 修改为docker服务宿主机的iplocation / {r…

[JAVASE] 类和对象(六) -- 接口(续篇)

目录 一. Comparable接口 与 compareTo方法 1.1 Comparable接口 1.2 compareTo方法的重写 1.2.1 根据年龄进行比较 1.2.2 根据姓名进行比较 1.4 compareTo 方法 的使用 1.3 compareTo方法的缺点(重点) 二. Comparator接口 与 compare方法 2.1 Comparator接口 2.2 compare 方法…

使用AWR对电路进行交流仿真---以整流器仿真为例

使用AWR对电路进行交流仿真—以整流器仿真为例 生活不易&#xff0c;喵喵叹气。马上就要上班了&#xff0c;公司的ADS的版权紧缺&#xff0c;主要用的软件都是NI 的AWR&#xff0c;只能趁着现在没事做先学习一下子了&#xff0c;希望不要裁我。 本AWR专栏只是学习的小小记录而…

2024.5.25期末测试总结

成绩&#xff1a; 配置&#xff1a; 可能与实际有些出入 题目&#xff1a; 第一题&#xff1a; 代码思路&#xff1a; 一道模拟题&#xff0c;按照公式计算出sumpow(2,i)&#xff0c;判断sum>H&#xff0c;输出 代码&#xff1a; #include<bits/stdc.h> using name…

LiveGBS流媒体平台GB/T28181用户手册-基础配置:信令服务配置、流媒体服务配置、白名单、黑名单、更多配置

LiveGBS流媒体平台GB/T28181用户手册-基础配置:信令服务配置、流媒体服务配置、白名单、黑名单、更多配置 1、基础配置1.1、信令服务配置1.2、白名单1.3、黑名单1.4、流媒体服务配置 2、搭建GB28181视频直播平台 1、基础配置 LiveGBS相关信令服务配置和流媒体服务配置都在这里…

Spark运行模式详解

Spark概述 Spark 可以在多种不同的运行模式下执行&#xff0c;每种模式都有其自身的特点和适用场景。 部署Spark集群大体上分为两种模式&#xff1a;单机模式与集群模式。大多数分布式框架都支持单机模式&#xff0c;方便开发者调试框架的运行环境。但是在生产环境中&#xff…