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

🌈 博客个人主页Chris在Coding

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

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

 我们仍在旅途                     

目录

8 使用定长内存池脱离new

9. 释放对象时不传大小

10.性能优化

        10.1 与malloc在多线程下对比测试

        10.2 性能瓶颈分析

        10.3 基于基数树的性能优化


 

8 使用定长内存池脱离new

在我们这个项目中,我们是为了在高并发场景下替代 malloc 函数的使用,决定使用 tcmalloc,但由于 tcmalloc 内部不能调用 malloc 函数,因此需要避免直接使用 new 来申请内存。而我们之前使用的new 实际上是封装了 malloc 函数。 为了彻底摆脱对 malloc 函数的依赖,我们这里就要利用之前实现的定长内存池。

class PageCache
{
public:// ...
private:// ...ObjMemoryPool<Span, 18000> _Spanpool;
};

这个定长内存池主要用于管理 Span 结构的对象申请和释放,而 Span 对象在系统中主要是在 层创建的。 因此,在 PageCache 类中定义了一个_Spanpool,用于 Span 对象的申请和释放。这样一来,系统在申请 Span 对象的空间时,可以直接从_Spanpool 中获取,而不需要通过调用 malloc 函数。这样做可以彻底避免对 malloc 函数的调用,使得系统更加适合在高并发场景下运行,并且能够充分利用 tcmalloc 提供的内存管理功能。这里为了应对大量的内存申请释放我们将每一次申请的Span个数设置为了18000,具体数字可以自行斟酌。

Span* span = _Spanpool.New();
_Spanpool.Delete(span);

现在我们就可以将之前的new,delete分别改写成我们新的接口 


之后我们也记得将申请ThreadCache的部分也替换,但是要注意的是此处从该定长内存池中申请内存时需要加锁,防止多个线程同时申请自己的ThreadCache对象而导致线程安全问题。

static void* ConcurrentAlloc(size_t size)
{// ...if (pTLSThreadCache == nullptr){static ObjMemoryPool<ThreadCache, 50> TdPool;static std::mutex tdMtx;tdMtx.lock();pTLSThreadCache = TdPool.New();tdMtx.unlock();}// ...
}

9. 释放对象时不传大小

实际上在使用free去释放内存时,我们是不会去传入申请的内存块的大小,所以这里实现的接口也应该能做到不传入对象的大小也能正常释放内存

为了实现在释放对象时不再需要显式传入对象的大小,我们可以通过在 Span 结构中增加一个 _size 成员来实现。每个 Span 管理的内存块被切分成多个对象,而这些对象的大小可能不同。因此,为每个 Span 增加一个_size 成员,用于表示这个 Span 管理的内存块被切分的对象的大小。这样一来,在释放对象时,系统可以通过对象的地址找到其对应的 Span,并从中获取对象的大小,而无需显式传入对象的大小。

struct Span
{PAGEID _pageId = 0;size_t _n = 0;Span* _next = nullptr;Span* _prv = nullptr;size_t _used = 0;void* _freeList = nullptr;bool _isused = false;size_t _size = 0;
};

而所有的Span都是从PageCache中拿出来的,因此每当我们调用NewSpan获取到一个k页的Span时,就应该将这个Span的_size确定下来。

Span* CentralCache::GetOneSpan(SpanList& list, size_t align_size)
{// ...PageCache::GetInstance()._pagemtx.lock();Span* span = PageCache::GetInstance().NewSpan(SizeTable::PageSizeLimit(align_size));span->_size = align_size;span->_isused = true;PageCache::GetInstance()._pagemtx.unlock();// ...
}

这里我们还要记得给申请大于256KB内存时获得的kspan写上size大小

static void* ConcurrentAlloc(size_t size)
{if (size > MAXSIZE){// ...PageCache::GetInstance()._pagemtx.lock();Span* span = PageCache::GetInstance().NewSpan(kpage);span->_size = size;PageCache::GetInstance()._pagemtx.unlock();// ...}// ...
}

现在当我们释放对象时,就不用再传入大小了。

static void ConcurrentFree(void* ptr)
{Span* span = PageCache::GetInstance().MapObjToSpan(ptr);size_t size = span->_size;if (size > MAXSIZE){Span* span = PageCache::GetInstance().MapObjToSpan(ptr);//超过256KB的内存,ThreadCache中无法维护,都是以Span类型储存并维护PageCache::GetInstance()._pagemtx.lock();PageCache::GetInstance().ReleaseSpanToPageCache(span);PageCache::GetInstance()._pagemtx.unlock();}else{assert(pTLSThreadCache);pTLSThreadCache->deallocate(ptr, size);}
}

10.性能优化

        10.1 与malloc在多线程下对比测试

#include"ConcurrentAlloc.h"// ntimes 一轮申请和释放内存的次数
// rounds 轮次
void BenchmarkMalloc(size_t ntimes, size_t nworks, size_t rounds)
{std::vector<std::thread> vthread(nworks);std::atomic<size_t> malloc_costtime = 0;std::atomic<size_t> free_costtime = 0;for (size_t k = 0; k < nworks; ++k){vthread[k] = std::thread([&, k]() {std::vector<void*> v;v.reserve(ntimes);for (size_t j = 0; j < rounds; ++j){size_t begin1 = clock();for (size_t i = 0; i < ntimes; i++){//v.push_back(malloc(16));v.push_back(malloc((16 + i) % 8192 + 1));}size_t end1 = clock();size_t begin2 = clock();for (size_t i = 0; i < ntimes; i++){free(v[i]);}size_t end2 = clock();v.clear();malloc_costtime += (end1 - begin1);free_costtime += (end2 - begin2);}});}for (auto& t : vthread){t.join();}printf("%u个线程并发执行%u轮次,每轮次malloc %u次: 花费:%u ms\n",nworks, rounds, ntimes, (size_t)malloc_costtime);printf("%u个线程并发执行%u轮次,每轮次free %u次: 花费:%u ms\n",nworks, rounds, ntimes, (size_t)free_costtime);printf("%u个线程并发malloc&free %u次,总计花费:%u ms\n",nworks, nworks * rounds * ntimes, (size_t)malloc_costtime + free_costtime);
}// 单轮次申请释放次数 线程数 轮次
void BenchmarkConcurrentMalloc(size_t ntimes, size_t nworks, size_t rounds)
{std::vector<std::thread> vthread(nworks);std::atomic<size_t> malloc_costtime = 0;std::atomic<size_t> free_costtime = 0;for (size_t k = 0; k < nworks; ++k){vthread[k] = std::thread([&]() {std::vector<void*> v;v.reserve(ntimes);for (size_t j = 0; j < rounds; ++j){size_t begin1 = clock();for (size_t i = 0; i < ntimes; i++){//v.push_back(ConcurrentAlloc(16));v.push_back(ConcurrentAlloc((16 + i) % 8192 + 1));}size_t end1 = clock();size_t begin2 = clock();for (size_t i = 0; i < ntimes; i++){ConcurrentFree(v[i]);}size_t end2 = clock();v.clear();malloc_costtime += (end1 - begin1);free_costtime += (end2 - begin2);}});}for (auto& t : vthread){t.join();}printf("%u个线程并发执行%u轮次,每轮次concurrent alloc %u次: 花费:%u ms\n",nworks, rounds, ntimes, (size_t)malloc_costtime);printf("%u个线程并发执行%u轮次,每轮次concurrent dealloc %u次: 花费:%u ms\n",nworks, rounds, ntimes, (size_t)free_costtime);printf("%u个线程并发concurrent alloc&dealloc %u次,总计花费:%u ms\n",nworks, nworks * rounds * ntimes, (size_t)malloc_costtime + free_costtime);
}int main()
{size_t n = 10000;cout << "==========================================================" << endl;BenchmarkConcurrentMalloc(n, 4, 10);cout << endl << endl;BenchmarkMalloc(n, 4, 10);cout << "==========================================================" << endl;return 0;
}

经过测试我们会发现我们现在实现的TcMalloc跟malloc在多线程情况下相比仍显得捉襟见肘, 这里我们进一步分析再次优化我们得代码

        10.2 性能瓶颈分析

这里我使用的是VS2022自带的性能探查器,这里我们先进入项目属性页面,把链接器下面高级这一栏中的配置文件改成/PROFILE

 再进入调试栏打开性能探查器

 

 等待系统分析结束后,我们可以得到这样的分析结果

这里我们不难发现,内存块的释放其实占用了大部分的运行时间,而其中花费时间最多的就是MapObjToSpan

通过分析我们不难得出,之所以调用MapObjToSpan会花费如此多的时间,其主要原因是因为在锁的竞争上,因此本项目的性能优化点就在于解决映射时,锁的竞争问题

        10.3 基于基数树的性能优化

基数树(Radix Trie)是一种多叉树,也称为基数特里树或压缩前缀树。相比于普通的Trie(前缀树),基数树具有更高的空间利用率。在基数树中,具有唯一子节点的每个节点都与其父节点合并,这样可以大大减少内部节点的数量,提高了空间利用率。

 为什么使用基数树不需要加锁?

  1. 对于使用 C++ 中的 mapunordered_map 进行映射关系的读取,需要加锁的原因在于它们的底层数据结构(红黑树和哈希表)在插入数据时可能会引起结构变化,为了避免数据不一致性问题,需要在读取操作时加锁。

  2. 相比之下,基数树的特性不同。基数树在空间开辟后不会发生结构变化,因此不需要在读取操作时加锁。此外,基数树的读取和建立映射操作不会同时对同一个页进行操作,因为读取映射时是针对 _useCount 不为 0 的页,而建立映射时是针对 _useCount 等于 0 的页,所以不存在同时对同一页面进行读取和建立映射操作的情况。

 二层基数树

在32位的环境下,一页的大小为8K为例,此时存储页号最多需要19个比特位。而二层基数树实际上就是把这19个比特位分为两次进行映射。

比如用前5个比特位在基数树的第一层进行映射,映射后得到对应的第二层,然后用剩下的比特位在基数树的第二层进行映射,映射后最终得到该页号对应的span指针。

在二层基数树中,第一层的数组占用的空间为 2^{5} X 4 = 2^{7} 字节(即 128 字节),而第二层的数组最多占用的空间为 2^{5}​​X 2^{14}X 4 = 2^{7}  字节(即 2 MB)。这意味整个二层基数树加起来对系统的花销并不大。

二层基数树只需要在一开始分配第一层的数组空间,当需要对某个页面号进行映射时,再分配相应的第二层数组空间即可。这种延迟分配的策略可以节省内存,并在实际需要时才分配所需的内存空间,提高了内存的利用率。

template<int bits>
class PageMap_Two
{
private:static const int _rootbits = 5;static const int _leafbits = bits - _rootbits;static const int _rootlen = 1 << _rootbits;static const int _leaflen = 1 << _leafbits;struct Leaf{void* _points[_leaflen];};Leaf* _root[_rootlen];
public:PageMap_Two(){//先把root层清理干净memset(_root, 0, sizeof _root);_PreMemoryAlloc();}inline void _Ensure(size_t start, size_t n){for (uintptr_t key = start; key <= start + n - 1;){const uintptr_t i1 = key >> _leafbits;if (_root[i1] == NULL) //第一层i1下标指向的空间未开辟{//开辟对应空间static ObjMemoryPool<Leaf, 100>leafPool;Leaf* leaf = (Leaf*)leafPool.New();memset(leaf, 0, sizeof(*leaf));_root[i1] = leaf;}key = ((key >> _leafbits) + 1) << _leafbits; //继续后续检查}}void _PreMemoryAlloc(){_Ensure(0, 1 << bits);}void* get(uintptr_t k) const{const uintptr_t i1 = k >> _leafbits;const uintptr_t i2 = k & (_leaflen - 1);if ((k >> bits) > 0 || _root[i1] == nullptr) {return nullptr;}return _root[i1]->_points[i2];}void set(uintptr_t k, void* v) {const uintptr_t i1 = k >> _leafbits;const uintptr_t i2 = k & (_leaflen - 1);assert(i1 < _rootlen);_root[i1]->_points[i2] = v;}
};
  • 类模板 PageMap_Two 接受一个整数模板参数 bits,用于确定页面编号的位数。
  • 类内部定义了一个名为 Leaf 的结构体,用于表示基数树的叶子节点,每个叶子节点包含一个指针数组 _points,用于存储页面的指针。
  • 类内部使用 _root 数组来表示基数树的根节点数组,数组长度为 _rootlen,每个元素存储一个指向叶子节点的指针。
  • 构造函数 PageMap_Two() 初始化 _root 数组,并调用 _PreMemoryAlloc() 函数预先分配内存。
  • _PreMemoryAlloc() 函数用于预先分配内存空间,确保根据页面编号的位数 bits 来确定需要的节点数量,并对应创建相应的叶子节点。
  • get(uintptr_t k) 函数用于根据页面编号 k 获取对应页面的指针。它首先计算页面编号所在的根节点索引 i1 和叶子节点索引 i2,然后根据索引访问对应的叶子节点 _root[i1] 并返回指针数组 _points[i2] 中的值。
  • set(uintptr_t k, void* v) 函数用于设置页面编号为 k 的页面指针为 v。它首先计算页面编号所在的根节点索引 i1 和叶子节点索引 i2,然后直接将指针 v 赋值给 _root[i1] 的指针数组 _points[i2]。 

 三层基数树 

在64位系统下,页面编号需要更多的位数来表示,因此使用三层基数树可以更有效地管理页面的映射关系。下面是为什么需要在64位系统下使用三层基数树的几个原因:

  1. 更大的地址空间: 在64位系统下,地址空间更大,通常为 2^{64},因此需要更多的位来表示页面编号。单层基数树可能不足以覆盖整个地址空间,因此需要更多的层级来管理更多的页面编号。

  2. 减少内存消耗: 在64位系统下,单层基数树需要分配更大的数组来存储映射关系,这可能会导致较大的内存消耗。使用多层基数树可以将地址空间分成更小的部分,每一层的数组大小相对较小,从而减少了每层的内存消耗。

  3. 更快的查找速度: 在多层基数树中,每一层的数组相对较小,因此可以更快地进行查找操作。单层基数树可能会出现较深的树结构,导致查找速度较慢,而多层基数树可以将查找路径分解成多个较短的路径,提高了查找效率。

template <int bits>
class PageMap_Three
{
private:static const int _interiorbits = (bits + 2) / 3;       //第一、二层对应页号的比特位个数static const int _interiorlen = 1 << _interiorbits; //第一、二层存储元素的个数static const int _leafbits = bits - 2 * _interiorbits; //第三层对应页号的比特位个数static const int _leaflen = 1 << _leafbits;         //第三层存储元素的个数struct Node{Node* nodes[_interiorlen];};struct Leaf{void* points[_leaflen];};Node* NewNode(){static ObjMemoryPool<Node, 1000> nodePool;Node* result = nodePool.New();if (result != NULL){memset(result, 0, sizeof(*result));}return result;}Node* _root;
public:PageMap_Three(){_root = NewNode();}void* get(uintptr_t k) const{const uintptr_t i1 = k >> (_leafbits + _interiorbits);         //第一层对应的下标const uintptr_t i2 = (k >> _leafbits) & (_interiorlen - 1); //第二层对应的下标const uintptr_t i3 = k & (_leaflen - 1);                    //第三层对应的下标//页号超出范围,或映射该页号的空间未开辟if ((k >> bits) > 0 || _root->nodes[i1] == NULL || _root->nodes[i1]->nodes[i2] == NULL){return NULL;}return reinterpret_cast<Leaf*>(_root->nodes[i1]->nodes[i2])->points[i3]; //返回该页号对应span的指针}void set(uintptr_t  k, void* v){assert(k >> bits == 0);const uintptr_t i1 = k >> (_leafbits + _interiorbits);         //第一层对应的下标const uintptr_t i2 = (k >> _leafbits) & (_interiorlen - 1); //第二层对应的下标const uintptr_t i3 = k & (_leaflen - 1);                    //第三层对应的下标Ensure(k, 1); //确保映射第k页页号的空间是开辟好了的reinterpret_cast<Leaf*>(_root->nodes[i1]->nodes[i2])->points[i3] = v; //建立该页号与对应span的映射}//确保映射[start,start+n-1]页号的空间是开辟好了的void Ensure(uintptr_t start, size_t n){for (uintptr_t key = start; key <= start + n - 1;){const uintptr_t i1 = key >> (_leafbits + _interiorbits);         //第一层对应的下标const uintptr_t i2 = (key >> _leafbits) & (_interiorlen - 1); //第二层对应的下标if (_root->nodes[i1] == NULL) //第一层i1下标指向的空间未开辟{//开辟对应空间Node* n = NewNode();_root->nodes[i1] = n;}if (_root->nodes[i1]->nodes[i2] == NULL) //第二层i2下标指向的空间未开辟{//开辟对应空间static ObjMemoryPool<Leaf, 100> leafPool;Leaf* leaf = leafPool.New();;memset(leaf, 0, sizeof(*leaf));_root->nodes[i1]->nodes[i2] = reinterpret_cast<Node*>(leaf);}key = ((key >> _leafbits) + 1) << _leafbits; //继续后续检查}}
};

 代码更改

class PageCache
{
public:static PageCache& GetInstance(){return _SingleInstance;}std::mutex _pagemtx;Span* NewSpan(size_t k);Span* MapObjToSpan(void* obj);void ReleaseSpanToPageCache(Span* span);
private:PageCache(){}PageCache(const PageCache&) = delete;static PageCache  _SingleInstance;SpanList _Spanlists[NPAGES];/*std::unordered_map<PAGEID,Span*> _IdToSpan;*/
#ifdef _WIN64PageMap_Three <64 - PAGESHIFT> _IdToSpan;
#elif _WIN32PageMap_Two <32 - PAGESHIFT> _IdToSpan;
#endifObjMemoryPool<Span, 18000> _Spanpool;
};

同时我们的MapObjSpan也不需要再加锁访问 

Span* PageCache::MapObjToSpan(void* obj)
{PAGEID id = ((PAGEID)obj >> PAGESHIFT);Span* ret = (Span*)_IdToSpan.get(id);if (ret != nullptr){return ret;}else{assert(false);return nullptr;}
}

 此时当我们需要建立页号与span的映射时,就调用基数树当中的set函数。

_IdToSpan.set(span->_pageId, span);

而当我们需要读取某一页号对应的span时,就调用基数树当中的get函数。

Span* ret = (Span*)_IdToSpan.get(id);

更改后再次运行测试程序,我们可以明显看到优化效果

 

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

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

相关文章

[LeetCode][239]【学习日记】滑动窗口最大值——O(n)单调队列

题目 239. 滑动窗口最大值 难度&#xff1a;困难相关标签相关企业提示 给你一个整数数组 nums&#xff0c;有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。 返回滑动窗口中的最大值。 示例 1…

雷赛控制卡的扩展IO连接

雷赛控制卡的扩展IO点无法控制问题处理 现象 因设备的上IO点较多&#xff0c;所以使用了多个雷赛32点位的IO扩展卡。上位机程序在控制输出IO时发现主模块IO和第一个扩展IO的输出可以控制。但第二个IO扩展卡和第三个IO扩展卡的输出控制不了。经排查出发现轴卡在初始化时只连接了…

校园小情书微信小程序,社区小程序前后端开源,校园表白墙交友小程序

功能 表白墙卖舍友步数旅行步数排行榜情侣脸漫画脸个人主页私信站内消息今日话题评论点赞收藏 效果图

2024年腾讯云学生服务器优惠活动「云+校园」政策解读

2024年腾讯云学生服务器优惠活动「云校园」&#xff0c;学生服务器优惠价格&#xff1a;轻量应用服务器2核2G学生价30元3个月、58元6个月、112元一年&#xff0c;轻量应用服务器4核8G配置191.1元3个月、352.8元6个月、646.8元一年&#xff0c;CVM云服务器2核4G配置842.4元一年&…

几何工具的使用

Geometry - Creation 创建几何 CogCreateCircleTool&#xff1a;创建圆CogCreateEllipseTool:创建椭圆CogCreateLineBisectPointsTool&#xff1a;带有两个点的平行线CogCreateLineParallelTool:在某一点创建某条线的平行线CogCreateLinePerpendicularTool:在某一点创建某条线…

LT6813/ADBMS1818底层驱动---均衡控制

1、LT6813采用内部均衡的原理 2、平衡控制结构体 根据数据库中读取的控制值设置平衡。要为单元设置平衡&#xff0c;必须将相应的位写入配置寄存器中。LTC 驱动程序仅执行数据库中 BMS 写入的数据。 参数 ltc_stateLTC 状态机的状态pSpi接口指向 SPI 配置的指针pTxBuff &…

数据结构(八)——初识单链表

&#x1f600;前言 单链表是数据结构中最基本的一种链表结构&#xff0c;它由一系列节点组成&#xff0c;每个节点包含数据和指向下一个节点的指针。单链表具有灵活性和动态性&#xff0c;可以根据需要插入、删除和查找元素&#xff0c;适用于各种场景和问题的解决。 在本篇文章…

day7-网络编程

1>基于UDP的网络聊天室 Ser.c #include <myhead.h> #define SER_IP "10.211.55.9" // 服务器IP #define SER_PORT 9999struct user {char usrName[20];struct sockaddr_in cin; }; int main(int argc, char const *argv[]) {// 1.创建用于监听的套接字int…

MyBatis-Flex学习总结

写在前面的话 MyBatis-Flex 是一个优雅的 MyBatis 增强框架&#xff0c;它非常轻量、同时拥有极高的性能与灵活性。我们可以轻松的使用 Mybaits-Flex 链接任何数据库&#xff0c;其内置的 QueryWrapper 帮助我们极大的减少了 SQL 编写的工作的同时&#xff0c;减少出错的可能性…

鸿蒙开发岗成春招最大黑马,“金三银四”应届生如何突围?

一年一度春招时间到&#xff0c;技术岗位已成为众多人才竞相追求的“职业高地”&#xff0c;也是未来职业发展的重要方向之一。鸿蒙人才在春招市场上成为“香饽饽”&#xff0c;与往年不同的是&#xff0c;许多应届生放弃考公执念向程序员进攻&#xff0c;这一现象背后蕴含着深…

腾讯云服务器和阿里云服务器价格测评_2024年费用大PK

2024年阿里云服务器和腾讯云服务器价格战已经打响&#xff0c;阿里云服务器优惠61元一年起&#xff0c;腾讯云服务器61元一年&#xff0c;2核2G3M、2核4G、4核8G、4核16G、8核16G、16核32G、16核64G等配置价格对比&#xff0c;阿腾云atengyun.com整理阿里云和腾讯云服务器详细配…

day52(vueJS)json-server模拟数据

json-server介绍&#xff1a;&#xff1a;&#xff1a;JSON Server 是一个用于快速搭建 REST API 的工具&#xff0c;它可以帮助我们在开发过程中快速模拟 一个后端 API 服务器&#xff0c;方便前端开发人员进行接口调试和开发。使用 JSON Server&#xff0c;你可以通过创建一个…

第三百八十七回

文章目录 1. 概念介绍2. 使用方法3. 示例代码 我们在上一章回中介绍了DateRangePickerDialog Widget相关的内容,本章回中将介绍Radio Widget.闲话休提&#xff0c;让我们一起Talk Flutter吧。 1. 概念介绍 我们在这里说的Radio Widget是指单选按钮&#xff0c;没有选中时是圆形…

代码随想录算法训练营第二天|977、有序数组的平方

977. 有序数组的平方 已解答 简单 相关标签 相关企业 给你一个按 非递减顺序 排序的整数数组 nums&#xff0c;返回 每个数字的平方 组成的新数组&#xff0c;要求也按 非递减顺序 排序。 示例 1&#xff1a; 输入&#xff1a;nums [-4,-1,0,3,10] 输出&#xff1a;[0,1,9,16,…

Linux设备模型(七) - Netlink

一&#xff0c;什么是netlink通信机制 Netlink套接字是用以实现用户进程与内核进程通信的一种特殊的进程间通信(IPC) ,也是网络应用程序与内核通信的最常用的接口。Netlink 是一种特殊的 socket&#xff0c;它是 Linux 所特有的。 Netlink 是一种在内核与用户应用间进行双向数…

我的创作周年纪念日

机缘 最初成为创作者的初心&#xff1a;整理自己的知识体系&#xff0c;普及前端知识 实战项目中的经验分享日常工作学习过程中的记录通过文章进行技术交流归纳和整理自己的知识体系 收获 创作的过程中收获&#xff1a; 获得了909粉丝的关注获得了很多正向的反馈&#xff0c…

【C语言】终の指针(前篇)

个人主页点这里~ 指针初阶点这里~ 指针初阶2.0点这里~ 指针进阶点这里~ 终の指针 一、回调函数二、qsort函数1、整形比较2、结构数据比较①结构体②-> 的使用③结构数据比较 一、回调函数 回调函数就是⼀个通过函数指针调用的函数。 把一个函数的指针作为参数传递给另一…

dubbo3适配springboot2.7.3

版本详细 <dependency><groupId>org.apache.dubbo</groupId><artifactId>dubbo</artifactId><version>3.0.3</version> </dependency><parent><groupId>org.springframework.boot</groupId><artifactId&…

mysql数据库入门到精通-Windows版本mysql安装(1)

文章目录 一、数据库介绍1.1、数据库概念1.2、为什么要使用数据库1.3、关系型数据库与非关系型数据库1.4、数据库术语1.5、mysql下载及地址 二、安装mysql数据库三、mysql的管理工具3.1、mysql command line client使用 四、SQL结构化查询语言4.1、SQL概述4.2、SQL发展4.3、SQL…

水牛社:专为创业者和网赚小白精心打造的助手

近几年来&#xff0c;经济发展快是快&#xff0c;物价也在蹭蹭往上涨&#xff0c;但工资却不见明显提升&#xff0c;随着生活成本的增加&#xff0c;单单靠工资&#xff0c;已经很难维持生活开支&#xff0c;加之疫情的影响&#xff0c;很多行业发展不景气&#xff0c;一些岗位…