[项目设计] 从零实现的高并发内存池(二)

news2024/12/23 17:43:47

🌈 博客个人主页Chris在Coding

🎥 本文所属专栏[高并发内存池]

❤️ 前置学习专栏[Linux学习]

 我们仍在旅途                     

目录

2.高并发内存池整体架构

3.ThreadCache实现

        3.1 ThreadCache整体架构

        3.2 哈希桶映射规则的实现

        内存对齐

        下标计算

        封装成SizeTable类

        3.3 ThreadCache类

        ThreadCache.h

        FreeList类

        allocate()

        deallocate()

        3.4 TLS无锁访问

        3.5 对外提供申请内存

        3.6 ThreadCache单元测试


2.高并发内存池整体架构

这个高并发内存池的设计是为了在多线程环境下有效地管理内存分配和回收,避免了频繁的锁竞争,提高了性能。以下是这个内存池的整体框架设计:

  1. Thread Cache(线程缓存)

    • 每个线程都有一个私有的线程缓存,用于小内存分配(小于等于256KB)。
    • 线程在需要内存时,首先检查自己的线程缓存,无需加锁即可进行内存分配。
    • 内存释放时也无需加锁,直接返回给线程缓存。
  2. Central Cache(中心缓存)

    • 所有线程共享的中心缓存,用于处理大内存分配需求。
    • 当线程缓存无法满足内存需求时,线程会向中心缓存申请内存。
    • 由于中心缓存是共享资源,访问时需要加锁,但采用哈希桶结构,只有在多个线程同时访问同一个桶时才需要加锁,降低了锁竞争的激烈程度。
  3. Page Cache(页缓存)

    • 以页为单位进行内存存储和分配,用于向中心缓存提供内存。
    • 当中心缓存需要内存时,页缓存会分配一定数量的页给中心缓存。
    • 当中心缓存中的内存满足一定条件时,页缓存会回收内存并尽可能地合并成更大的连续内存块,以减少内存碎片问题的发生。

整体框架设计保证了高并发环境下的内存管理效率和线程安全性。每个线程的私有线程缓存减少了锁竞争,而中心缓存的哈希桶结构和页缓存的内存合并机制则进一步减少了锁的争用,提高了整个内存池的并发处理能力。

3.ThreadCache实现

        3.1 ThreadCache整体架构

  • 哈希桶结构

    • Thread Cache 是一个哈希桶结构,每个哈希桶中存放一个空闲链表,用于管理不同大小的内存块。
    • 哈希桶的数量可以根据需要进行调整,以适应内存块的大小范围。
  • 对齐规则

    • thread cache支持小于等于256KB内存的申请,如果我们将每种字节数的内存块都用一个空闲链表进行管理的话,那么此时我们就需要20多万个空闲链表,光是存储这些空闲链表的头指针就需要消耗大量内存,这显然是得不偿失的,为了减少哈希桶的数量和节约内存空间,内存块的申请会按照一定的对齐规则进行处理。
    • 例如,我们让这些字节数都按照8字节进行向上对齐,那么此时当线程申请1~8字节的内存时会直接给出8字节,而当线程申请9~16字节的内存时会直接给出16字节,以此类推。
  • 内存块申请

    • 当线程要申请内存块时,根据所需大小经过对齐规则计算得到实际需要的字节数。
    • 然后根据计算得到的字节数找到对应的哈希桶。
    • 如果哈希桶中有可用内存块,则从空闲链表中头部获取一个内存块并返回。
    • 如果哈希桶中的空闲链表为空,则需要向下一层的 Central Cache 请求内存块。
  • 内存块释放

    • 当线程释放内存块时,根据内存块的大小找到对应的哈希桶。
    • 将释放的内存块加入到哈希桶对应的空闲链表的头部,以便下次分配使用。
  • 内存碎片

    • 由于对齐规则的存在,可能会导致部分内存块产生内部碎片,即多分配了一些不被利用的内存空间。
    • 这些内部碎片会导致一定程度的空间浪费,但是通过对齐规则可以减少哈希桶数量,提高了内存管理的效率。

        3.2 哈希桶映射规则的实现

这里我们创建一个Common.h来存放项目里面通用的类

内存对齐

对齐规则的目的是将不同大小的内存块按照一定的规则映射到哈希桶中,以降低哈希桶数量,节省内存空间,并减少对齐导致的内存浪费

但如果所有的字节数都按照8字节进行对齐的话,那么我们就需要建立32768(256 × 1024 ÷ 8)个桶,这个数量还是比较多的,实际上我们可以让不同范围的字节数按照不同的对齐数进行对齐,具体对齐方式如下:

字节数对齐数哈希桶下标
[1,128]8 bytes[0,16)
[128+1,1024]16 bytes[16,72)
[1024+1,8*1024]128 bytes[72,128)
[8*1024+1,64*1024]1024 bytes[128,184)
[64*1024,256*1024]8*1024 bytes[184,208)

空间浪费率=对齐后的字节数 / 浪费的字节数​

根据给定的对齐规则,我们可以计算每个区间的最大空间浪费率,然后对整体的空间利用率进行分析。

首先,我们列出每个区间的对齐规则以及计算最大空间浪费率的公式:

  • 区间:1字节到128字节

    • 对齐数:8字节
    • 最大浪费率:7 ÷ 8 = 87.5%
  • 区间:129字节到1024字节

    • 对齐数:16字节
    • 最大浪费率:15 ÷ 144 ≈ 10.42%
  • 区间:1025字节到8 * 1024字节

    • 对齐数:128字节
    • 最大浪费率:127 ÷ 1152 ≈ 11.02%
  • 区间:8 * 1024 + 1字节到64 * 1024字节

    • 对齐数:1024字节
    • 最大浪费率:1023 ÷ 9216 ≈ 11.10%
  • 区间:64 * 1024 + 1字节到256 * 1024字节

    • 对齐数:8 * 1024字节
    • 最大浪费率:8191 ÷ 73728 ≈ 11.10%

根据计算,我们可以看出:

  • 在1字节到128字节的区间中,最大浪费率为87.5%,浪费较为严重。
  • 在其他区间中,最大浪费率均在10%左右,空间利用率较高。

综合来看,整体的空间利用率是比较高的,尤其是针对较大的内存块。但在小内存块的情况下,由于对齐规则会导致较大的内部碎片,空间利用率不够理想。

在处理对齐时,我们先判断该字节数属于哪一个区间,然后再通过调用同一个子函数进行进一步处理。 

static size_t RoundUp(size_t bytes)
{
	if (bytes <= 128)
	{
		return _RoundUp(bytes, 8);
	}
	else if (bytes <= 1024)
	{
		return _RoundUp(bytes, 16);
	}
	else if (bytes <= 8 * 1024)
	{
		return _RoundUp(bytes, 128);
	}
	else if (bytes <= 64 * 1024)
	{
		return _RoundUp(bytes, 1024);
	}
	else if (bytes <= 256 * 1024)
	{
		return _RoundUp(bytes, 8 * 1024);
	}
	else
	{
		assert(false);
		return -1;
	}
}

此时我们就需要编写一个子函数,该子函数需要通过对齐数计算出某一字节数对齐后的字节数,最容易想到的就是下面这种写法。

	static inline size_t _RoundUp(size_t bytes, size_t alignnum)
	{
		size_t alignSize = 0;
		if (bytes % alignnum != 0)
		{
			alignSize = (bytes / alignnum + 1) * alignnum;
		}
		else
		{
			alignSize = bytes;
		}
		return alignSize;
	}

除了上述写法,我们还可以通过位运算的方式来进行计算,虽然位运算可能并没有上面的写法容易理解,但计算机执行位运算的速度是比执行乘法和除法更快的。

static inline size_t _roundup(size_t bytes, size_t alignnum)
{
	return ((bytes + alignnum - 1) & ~(alignnum - 1));
}

请注意,对于第两种方式,需要确保 alignnum 是 2 的幂次方,才能正确进行位运算。第一种方法是对齐的通用解法。

这里我们先看后半部分的式子,由于alignum也就是对齐数都是2的幂次方,此时在二进制下表示alignum一定是最高位为1,其余位置为0。这里以对齐数16为例,它在二进制下的表示为:

0000000000000000000000010000  

那么当alignum-1后,它的二进制结果一定是原本的最高位变为0,后面的位置再变成1

0000000000000000000000001111  

此时再对其取反~

11111111111111111111111111110000

此时再与别的数字&运算,结果就是该数字除以alignum的余数被清0,也就是向下对齐成alignum的整数倍 , 但是,由于我们需要的是将bytes向上对齐成alignum的整数倍,但当我们先(bytes + alignnum - 1)再向下对齐时,只要bytes不是alignnum的整数倍,都会被调整到下一个alignnum的倍数

下标计算

此时我们编写一个子函数来处理字节数在某个区间内的对齐后坐标,容易想到的就是下面的方法

static inline size_t _Index(size_t bytes, size_t alignnum)
{
	size_t index = 0;
	if (bytes % alignnum != 0)
	{
		index = bytes / alignnum;
	}
	else
	{
		index = bytes / alignnum - 1;
	}
	return index;
}

当然,为了提高效率下面也提供了一个用位运算来解决的方法。 需要注意的是,此时我们并不是传入该字节数的对齐数,而是将对齐数写成2的n次方的形式后,将这个n值进行传入。所以对于第两种方式,仍需要确保 alignnum 是 2 的幂次方

static  size_t _Index(size_t bytes, size_t align_shift)
{
	return ((bytes + (1 << align_shift) - 1) >> align_shift) - 1;
}

当 align_shift 是指定的对齐数的对数形式时(例如,如果对齐数是 8,那么 align_shift 应该是 3,因为 8 是 2 的 3 次幂),此时满足:

alignnum = 1 << align_shift

此时式子就可以变成:

((bytes + alignnum - 1) >> align_shift) - 1;

这是我们可以参考原先向上对齐的式子来思考,原先的向上对齐的式子是在(bytes + alignnum - 1)的基础上把二进制下alignnum的最高位除外的后面位都置为0。而这里我们选择向右位移align_shift,其实本质上就是把alignnum的最高位除外的后面位都位移掉。这里我们就可以理解成先把后面位都置为0(也就是先向上对齐),然后>>align_shift就相当于除以alignnum。

这时我们会得到得结果就是bytes / alignnum向上取整得结果,当然这里由于坐标是从0开始计算的,我们还要减一。


在获取某一字节数对应的哈希桶下标时,也是先判断该字节数属于哪一个区间,然后再通过调用一个子函数进行进一步处理。这里要注意在计算当前区间坐标时,我们要减去上一个区间的最大值,在计算出当前区间的坐标后,还要加上前面的坐标总和。同时我们此时采用第二种子函数设计,所以传入的都是对齐数的幂次方数。

static size_t Index(size_t bytes)
{

	assert(bytes <= MAXSIZE);
	if (bytes <= 128)
	{
		return _Index(bytes, 3);
	}
	else if (bytes <= 1024)
	{
		return _Index(bytes-128, 4)+16;
	}
	else if (bytes <= 8 * 1024)
	{
		return _Index(bytes-1024, 7)+72;
	}
	else if (bytes <= 64 * 1024)
	{
		return _Index(bytes-8*1024, 10)+128;
	}
	else
	{
		return _Index(bytes-64*1024, 13)+184;
	}
}

封装成SizeTable类

这里我们在Common.h中封装一个SizeTable类,用来专门处理哈希桶映射规则

class SizeTable
{
public:
	static inline size_t _RoundUp(size_t bytes, size_t alignnum)
	{
		return ((bytes + alignnum - 1) & ~(alignnum - 1));
	}
	static size_t RoundUp(size_t bytes)
	{
		if (bytes <= 128)
		{
			return _RoundUp(bytes, 8);
		}
		else if (bytes <= 1024)
		{
			return _RoundUp(bytes, 16);
		}
		else if (bytes <= 8 * 1024)
		{
			return _RoundUp(bytes, 128);
		}
		else if (bytes <= 64 * 1024)
		{
			return _RoundUp(bytes, 1024);
		}
		else if (bytes <= 256 * 1024)
		{
			return _RoundUp(bytes, 8 * 1024);
		}
		else
		{
			assert(false);
			return -1;
		}
	}
	static  size_t _Index(size_t bytes, size_t align_shift)
	{
		return ((bytes + (1 << align_shift) - 1) >> align_shift) - 1;
	}
	static size_t Index(size_t bytes) 
	{

		assert(bytes <= MAXSIZE);
		if (bytes <= 128)
		{
			return _Index(bytes, 3);
		}
		else if (bytes <= 1024)
		{
			return _Index(bytes - 128, 4) + 16;
		}
		else if (bytes <= 8 * 1024)
		{
			return _Index(bytes - 1024, 7) + 72;
		}
		else if (bytes <= 64 * 1024)
		{
			return _Index(bytes - 8 * 1024, 10) + 128;
		}
		else
		{
			return _Index(bytes - 64 * 1024, 13) + 184;
		}
	}
};

        3.3 ThreadCache类

ThreadCache.h

按照上述的对齐规则,ThreadCache中桶的个数,也就是空闲链表的个数是208,以及Thread Cache允许申请的最大内存大小256KB,我们可以将这些数据按照如下方式进行定义。

static const int NFREELISTS = 208 ;

static const int MAXSIZE = 256 * 1024;

现在就可以对ThreadCache类进行定义了,这里我们可以参考上一章实现的定长内存池,我们类中的_Freelists就是一个存储208个空闲链表的数组,这里的allocate和deallocate就对应着内存块的申请与释放,而FetchFromCentralCache接口我们留到编写CentralCache时实现

#pragma once
#include"common.h"
class ThreadCache
{
public:
	void* allocate(size_t size);
	void  deallocate(void* ptr, size_t size);
	// void* FetchFromCentralCache(size_t index, size_t size);
private:
	FreeList _freelists[NFREELISTS];
};

static _declspec(thread) ThreadCache* pTLSThreadCache = nullptr;

FreeList类

鉴于当前项目比较复杂,我们在Common.h对空闲链表这个结构进行封装,目前我们就提供Push和Pop两个接口,后面在需要时还会添加对应的成员函数。

class FreeList
{
public:
	void push(void* obj) //头插
	{
		assert(obj);
		*(void**)obj = _FreeList;
		_FreeList = obj;
	}
	void* pop() //头删
	{
		assert(_FreeList);
		void* obj = _FreeList;
		_FreeList = *(void**)_FreeList;;
		return obj;
	}
	bool empty()
	{
		return _FreeList == nullptr;
	}
private:
	void* _FreeList = nullptr;
};

allocate()

我们创建ThreadCache.cpp来编写ThreadCache接口的具体实现

在ThreadCache申请对象时,通过所给字节数计算出对应的哈希桶下标,如果桶中空闲链表不为空,则从该空闲链表中取出一个对象进行返回即可;但如果此时空闲链表为空,那么我们就需要从CentralCache进行获取了,如果空闲链表中对象不够,我们就会通过FetchFromCentralCache到CentralCache中进一步申请内存,这个在后面CentralCache再具体实现。

void* ThreadCache::allocate(size_t size)
{
	assert(size <= MAXSIZE);
	size_t align_size = SizeTable::RoundUp(size);
	size_t index = SizeTable::Index(size);
	if (!_freelists[index].empty())
	{
		return _freelists[index].pop();
	}
	else
	{
		// return FetchFromCentralCache(index, align_size);
	}
}

deallocate()

void ThreadCache::deallocate(void* ptr, size_t size)
{
	assert(ptr);
	assert(size <= MAXSIZE);
	// 找对映射的空闲链表桶,对象插入进入
	size_t index = SizeTable::Index(size);
	_freelists[index].push(ptr);
}

        3.4 TLS无锁访问

线程局部存储(TLS),也称为线程本地存储,是一种变量存储的方法,使得每个线程都可以拥有自己独立的变量实例,而这些变量在不同线程之间是相互隔离的。TLS 允许每个线程在其执行期间拥有自己的全局变量的副本,而不需要担心与其他线程共享相同变量的问题。

在 Windows 平台上,可以使用_declspec(thread)语法实现线程局部存储。

由于每个线程都有一个自己独享的单例的ThreadCache,那应该如何创建这个ThreadCache呢?我们不能将这个ThreadCache创建为全局的,因为全局变量是所有线程共享的,这样就不可避免的需要锁来控制,增加了控制成本和代码复杂度。所以我们这里将ThreadCache设计为线程局部数据

//TLS - Thread Local Storage
static _declspec(thread) ThreadCache* pTLSThreadCache = nullptr;

但不是每个线程被创建时就立马有了属于自己的ThreadCache,而是当该线程调用相关申请内存的接口时才会创建自己的ThreadCache,因此在申请内存时我们会有下面的逻辑。

//通过TLS,每个线程无锁的获取自己专属的ThreadCache对象
if (pTLSThreadCache == nullptr)
{
	pTLSThreadCache = new ThreadCache;
}

        3.5 对外提供申请内存

创建ConcurrentAlloc,h,编写一个向外提供内存申请释放的ConcurrentAlloc和ConcurrentFree函数

#pragma once
#include "Common.h"
#include "ThreadCache.h"

static void* ConcurrentAlloc(size_t size)
{
	// 通过TLS 每个线程无锁的获取自己的专属的ThreadCache对象
	if (pTLSThreadCache == nullptr)
	{
		pTLSThreadCache = new ThreadCache;
	}
    cout << std::this_thread::get_id() << ":" << pTLSThreadCache << endl; //单元测试使用
	return pTLSThreadCache->allocate(size);
}

static void ConcurrentFree(void* ptr, size_t size)
{
	assert(pTLSThreadCache);

	pTLSThreadCache->deallocate(ptr, size);
}

        3.6 ThreadCache单元测试

这里我们编写简单的程序来对我们现在实现的ThreadCache做简单的测试

#include "ConcurrentAlloc.h"
void Alloc1()
{
	for (size_t i = 0; i < 5; ++i)
	{
		void* ptr = ConcurrentAlloc(6);
	}
}
void Alloc2()
{
	for (size_t i = 0; i < 5; ++i)
	{
		void* ptr = ConcurrentAlloc(7);
	}
}

void TLSTest()
{
	std::thread t1(Alloc1);
	t1.join();

	std::thread t2(Alloc2);
	t2.join();
}
int main()
{
	TLSTest();
	return 0;
}

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

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

相关文章

【JSON2WEB】08 Amis的事件和校验

CRUD操作中&#xff0c;新增、编辑、删除数据后要同步刷新列表&#xff0c;这个可以用Amis的事件来实现。 1 新增数据后刷新列表 Step 1 找到【新增数据】弹窗的【提交】按钮 Step 2 添加鼠标点击事件 这里的 组件ID&#xff1a;u:13d67a44214e 为表格2的组件ID&#xff0c; …

2024常用开源测试开发工具!

今天为大家奉献一篇测试开发工具集锦干货。在本篇文章中&#xff0c;将给大家推荐几款日常工作中经常用到的测试开发工具神器&#xff0c;涵盖了自动化测试、性能压测、流量复制、混沌测试、造数据等。 1、AutoMeter-API 自动化测试平台 AutoMeter 是一款针对分布式服务&…

MySQL 8.0.35 企业版安装和启用TDE插件keyring_encrypted_file

本文主要记录MySQL企业版TDE插件keyring_encrypted_file的安装和使用。 TDE说明 TDE( Transparent Data Encryption,透明数据加密) 指的是无需修改应用就可以实现数据的加解密&#xff0c;在数据写磁盘的时候加密&#xff0c;读的时候自动解密。加密后其他人即使能够访问数据库…

Vue-03

Vue指令 v-bind 作用&#xff1a;动态设置html的标签属性&#xff08;src url title…&#xff09; 语法&#xff1a;v-bind:属性名"表达式" 举例代码如下&#xff1a; 实现效果如下&#xff1a; 案例&#xff1a;图片切换 实现代码如下&#xff1a; 实现的效果…

#WEB前端(CCS常用属性,补充span、div)

1.实验&#xff1a; 复合元素、行内元素、块内元素、行内块元素 2.IDE&#xff1a;VSCODE 3.记录&#xff1a; span为行内元素&#xff1a;不可设置宽高&#xff0c;实际占用控件决定分布空间。 div为块内元素&#xff1a;占满整行&#xff0c;可以设置宽高 img为行内块元…

新手想玩硬件,买单片机还是树莓派好?

新手想玩硬件&#xff0c;买单片机还是树莓派好&#xff1f; 在开始前我有一些资料&#xff0c;是我根据网友给的问题精心整理了一份「单片机的资料从专业入门到高级教程」&#xff0c; 点个关注在评论区回复“888”之后私信回复“888”&#xff0c;全部无偿共享给大家&#x…

单链表的排序-力扣算法题

文章目录 概要例题解题思路&#xff1a;1、递归分割2、递归排序实际的含义3、递归回溯与合并 case解析&#xff1a;1、初始链表&#xff1a;2、第一轮分割&#xff1a;3、继续分割&#xff1a;有序子链表合并&#xff1a;最终合并&#xff1a;结果&#xff1a; 代码实现总结&am…

JavaScript继承 寄生组合式继承 extends

JavaScript继承 1、JS 的继承到底有多少种实现方式呢? 2、ES6 的 extends 关键字是用哪种继承方式实现的呢? 继承种类 原型链继承 function Parent1() {this.name parentlthis.play [1, 2, 3] }function Child1() {this.type child2 }Child1.prototype new Parent1(…

(十)SpringCloud系列——openfeign的高级特性实战内容介绍

前言 本节内容主要介绍一下SpringCloud组件中微服务调用组件openfeign的一些高级特性的用法以及一些常用的开发配置&#xff0c;如openfeign的超时控制配置、openfeign的重试机制配置、openfeign集成高级的http客户端、openfeign的请求与响应压缩功能&#xff0c;以及如何开启…

bashplotlib,一个有趣的 Python 数据可视化图形库

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站AI学习网站。 目录 前言 什么是Bashplotlib库&#xff1f; 安装Bashplotlib库 使用Bashplotlib库 Bashplotlib库的功能特性 1. 绘…

Linux速览(1)——基础指令篇

在上一章对Linux有了一些基础了解之后&#xff0c;本章我们来学习一下Linux系统下一些基本操作的常用的基础指令。 目录 1. ls 指令 2. pwd&&whoami命令 3. cd 指令 4. touch指令 5.mkdir指令&#xff08;重要&#xff09;&#xff1a; 6.rmdir指令 && …

pandas行列求众数及按列去重

创建示例数据框如下&#xff1a; df2pd.DataFrame(data{A:[1,2,3,3,4,4,4,4],B:[a,b,c,c,d,d,d,d],C:[11,22,33,33,44,44,44,44],D:[11,22,33,33,44,44,44,44]}) print(df2.mode()) #求列众数 print(df2.loc[:,[A,C,D]].mode(axis1)) #求特定列的行众数 df2.drop_duplicates(s…

记一次:android学习笔记一(学习目录-不要看无内容)

学习目录如下 B站学习的名称--Android开发从入门到精通(项目案例版) 网址:https://www.bilibili.com/video/BV1jW411375J/ 第0章:安装 android stoid 参考地址https://blog.csdn.net/adminstate/article/details/130542368 第一章:第一个安卓应用 第二章:用户界面设…

类和对象基础知识

1. C和C语言最大的区别 以洗衣服为例&#xff0c; C语言是手洗&#xff0c;你需要对每一个过程非常的清楚&#xff0c;一步一步都需要自己亲自去完成&#xff0c; 而C更像是机洗&#xff0c;整个过程划分为人、衣服、洗衣粉、洗衣机四个对象的交互过程&#xff0c; 而人是不…

C语言指针的初步认识--学习笔记(2)

1.数组名的理解 我们在使⽤指针访问数组的内容时&#xff0c;有这样的代码&#xff1a; int arr[10]{1,2,3,4,5,6,7,8,9,10}; int* p&arr[0]; 这⾥我们使⽤ &arr[0] 的⽅式拿到了数组第⼀个元素的地址&#xff0c;但是其实数组名本来就是地址&#xff0c;⽽且 是数组…

React 事件机制原理

相关问题 React 合成事件与原生 DOM 事件的区别React 如何注册和触发事件React 事件如何解决浏览器兼容问题 回答关键点 React 的事件处理机制可以分为两个阶段&#xff1a;初始化渲染时在 root 节点上注册原生事件&#xff1b;原生事件触发时模拟捕获、目标和冒泡阶段派发合…

羊大师揭秘羊奶与健康,美味的保健佳品

羊大师揭秘羊奶与健康&#xff0c;美味的保健佳品 羊奶确实是一种美味且健康的保健佳品&#xff0c;其独特的营养成分和风味使其成为许多人的健康选择。以下是一些羊奶与健康的关系&#xff1a; 营养丰富&#xff1a;羊奶含有丰富的蛋白质、脂肪、矿物质和维生素&#xff0c;…

Go字符串实战操作大全!

目录 1. 引言文章结构概览 2. Go字符串基础字符串的定义与特性什么是字符串&#xff1f;Go字符串的不可变性原则 字符串的数据结构Go字符串的内部表达byte和rune的简介 3. 字符串操作与应用3.1 操作与应用字符串连接字符串切片字符串查找字符串比较字符串的替换字符串的大小写转…

【动态规划专栏】

动态规划基础知识 概念 动态规划&#xff08;Dynamic Programming&#xff0c;DP&#xff09;&#xff1a;用来解决最优化问题的算法思想。 动态规划是分治思想的延伸&#xff0c;通俗一点来说就是大事化小&#xff0c;小事化无的艺术。 一般来说&#xff0c;…

Android 开发环境搭建的步骤

本文将为您详细讲解 Android 开发环境搭建的步骤。搭建 Android 开发环境需要准备一些软件和工具&#xff0c;以下是一些基础步骤&#xff1a; 1. 安装 Java Development Kit (JDK) 首先&#xff0c;您需要安装 Java Development Kit (JDK)。JDK 是 Android 开发的基础&#xf…