当前位置: 首页 > news >正文

C++项目:高并发内存池_下

目录

8. thread cache回收内存

9. central cache回收内存

10. page cache回收内存

11. 大于256KB的内存申请和释放

11.1 申请

11.2 释放

12. 使用定长内存池脱离使用new

13. 释放对象时优化成不传对象大小

14. 多线程环境下对比malloc测试

15. 调试和复杂问题的调试技巧

16. 性能瓶颈分析

17. 使用基数树进行优化


8. thread cache回收内存

当某个线程申请的对象不用了,可以将其释放给thread cache,然后thread cache将该对象插入到对应哈希桶的自由链表当中即可。

但是随着线程不断的释放,对应自由链表的长度也会越来越长,这些内存堆积在一个thread cache中就是一种浪费,应该将这些内存还给central cache,这样一来,这些内存对其他线程来说也是可申请的,因此当thread cache某个桶当中的自由链表太长时可以进行一些处理。

如果thread cache某个桶当中自由链表的长度超过它一次批量向central cache申请的对象个数,那么此时就要把该自由链表当中的这些对象还给central cache。

void ThreadCache::Deallocate(void* ptr, size_t size) // 释放内存对象
{assert(ptr);assert(size <= MAX_BYTES);// 找对映射的自由链表桶,对象插入进入size_t index = SizeClass::Index(size);_freeLists[index].Push(ptr);// 当链表长度大于一次批量申请的内存时就开始还一段list给central cacheif (_freeLists[index].Size() >= _freeLists[index].MaxSize()) // 要增加Size接口{ListTooLong(_freeLists[index], size);}
}void ThreadCache::ListTooLong(FreeList& list, size_t size)
{void* start = nullptr;void* end = nullptr;list.PopRange(start, end, list.MaxSize()); // 取批量的内存,要增加PopRange接口CentralCache::GetInstance()->ReleaseListToSpans(start, size);
}

要提供Size接口,并维护_size,再增加PopRange接口

调用的地方增加传入参数(倒数第三行):

当thread cache的某个自由链表过长时,实际就是把这个自由链表当中全部的对象都还给central cache了,但这里在设计PopRange接口时还是设计的是取出指定个数的对象,因为在某些情况下当自由链表过长时,我们可能并不一定想把链表中全部的对象都取出来还给central cache,这样设计就是为了增加代码的可修改性。

当判断thread cache是否应该还对象给central cache时,还可以综合考虑每个thread cache整体的大小。比如当某个thread cache的总占用大小超过一定阈值时,就将该thread cache当中的对象还一些给central cache,这样就尽量避免了某个线程的thread cache占用太多的内存。对于这一点,在tcmalloc当中就是考虑到了的,但这里就简化了。


9. central cache回收内存

当thread cache中某个自由链表太长时,会将自由链表当中的这些对象还给central cache中的span。需要注意的是,还给central cache的这些对象不一定都是属于同一个span的。central cache中的每个哈希桶当中可能都不止一个span,因此计算出还回来的对象应该还给central cache的哪一个桶后,还需要知道还回来的对象到底应该还给这个桶当中的哪一个span。

如何根据对象的地址得到对象所在的页号?

某个页当中的所有地址除以页的大小都等该页的页号。比如假设一页的大小是100,那么地址0~99都属于第0页,它们除以100都等于0,而地址100~199都属于第1页,它们除以100都等于1。

如何找到一个对象对应的span?

虽然现在可以通过对象的地址得到其所在的页号,但是还是不能知道这个对象到底属于哪一个span。因为一个span管理的可能是多个页。

为了解决这个问题,可以建立页号和span之间的映射。由于这个映射关系在page cache进行span的合并时也需要用到,因此直接将其存放到page cache里面。这时就需要在PageCache类当中添加一个映射关系了,这里可以用STL当中的unordered_map进行实现,并且添加一个函数接口,用于让central cache获取这里的映射关系。(下面代码中只展示了PageCache

类当中新增的成员)

class PageCache
{
public:// 获取从对象到span的映射Span* MapObjectToSpan(void* obj);
private:std::unordered_map<PAGE_ID, Span*> _idSpanMap;
};

每当page cache分配span给central cache时,都需要记录一下页号和span之间的映射关系。此后当thread cache还对象给central cache时,才知道应该具体还给哪一个span。

因此当central cache在调用NewSpan接口向page cache申请k页的span时,page cache在返回这个k页的span给central cache之前,应该建立这k个页号与该span之间的映射关系,这里改一下之前写的NewSpan(多了两个for循环):

Span* PageCache::NewSpan(size_t k) // 获取一个K页的span
{assert(k > 0 && k < NPAGES);if (!_spanLists[k].Empty()) // 先检查第k个桶里面有没有span{//return _spanLists[k].opFront(); // 有就直接返回Span* kSpan = _spanLists[k].PopFront();//建立页号与span的映射,方便central cache回收小块内存时查找对应的spanfor (PAGE_ID i = 0; i < kSpan->_n; i++){_idSpanMap[kSpan->_pageId + i] = kSpan;}return kSpan;}// 检查一下后面的桶里面有没有span,如果有->进行切分for (size_t i = k + 1; i < NPAGES; ++i){if (!_spanLists[i].Empty()){Span* nSpan = _spanLists[i].PopFront();Span* kSpan = new Span;// 在nSpan的头部切一个k页span下来返回,nSpan再挂到对应映射的位置kSpan->_pageId = nSpan->_pageId; // _pageId类似地址kSpan->_n = k;nSpan->_pageId += k; // 起始页的页号往后走nSpan->_n -= k; // 页数减等k_spanLists[nSpan->_n].PushFront(nSpan);//建立页号与span的映射,方便central cache回收小块内存时查找对应的spanfor (PAGE_ID i = 0; i < kSpan->_n; i++){_idSpanMap[kSpan->_pageId + i] = kSpan;}return kSpan;}}// 走到这说明后面没有大页的span了->去找堆要一个128页的spanSpan* bigSpan = new Span;void* ptr = SystemAlloc(NPAGES - 1);bigSpan->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT; // 转换为页号bigSpan->_n = NPAGES - 1;_spanLists[bigSpan->_n].PushFront(bigSpan);return NewSpan(k); // 把新申请的span插入后再递归调用一次自己(避免代码重复)
}

此时就可以通过对象的地址找到该对象对应的span了,直接将该对象的地址除以页的大小得到页号,然后在unordered_map当中找到其对应的span即可。

Span* PageCache::MapObjectToSpan(void* obj)
{PAGE_ID id = ((PAGE_ID)obj >> PAGE_SHIFT);auto ret = _idSpanMap.find(id);if (ret != _idSpanMap.end()){return ret->second;}else{assert(false);return nullptr;}
}

通过某个页号查找其对应的span时,该页号与其span之间的映射一定是建立过的,如果此时没有在unordered_map当中找到,则说明之前的代码逻辑有问题,因此当没有找到对应的span时可以直接用断言结束程序,以表明程序逻辑出错。


此时就能写central cache的回收内存了,当thread cache还对象给central cache时,就可以依次遍历这些对象,将这些对象插入到其对应span的自由链表当中,并且及时更新该span的_usseCount计数即可。

在thread cache还对象给central cache的过程中,如果central cache中某个span的_useCount减到0时,说明这个span分配出去的对象全部都还回来了,那么此时就可以将这个span再进一步还给page cache。

void CentralCache::ReleaseListToSpans(void* start, size_t size)
{size_t index = SizeClass::Index(size); // 先算出是哪个桶_spanLists[index]._mtx.lock();while (start){void* next = NextObj(start);Span* span = PageCache::GetInstance()->MapObjectToSpan(start);NextObj(start) = span->_freeList;span->_freeList = start;span->_useCount--;// 说明span的切分出去的所有小块内存都回来了// 这个span就可以再回收给page cache,pagecache可以再尝试去做前后页的合并if (span->_useCount == 0){_spanLists[index].Erase(span);span->_freeList = nullptr;span->_next = nullptr;span->_prev = nullptr;// 释放span给page cache时,使用page cache的锁就可以了_spanLists[index]._mtx.unlock(); // 这时把桶锁解掉PageCache::GetInstance()->_pageMtx.lock();PageCache::GetInstance()->ReleaseSpanToPageCache(span);PageCache::GetInstance()->_pageMtx.unlock();_spanLists[index]._mtx.lock();}start = next;}_spanLists[index]._mtx.unlock();
}

需要注意,如果要把某个span还给page cache,需要先将这个span从central cache对应的双链表中移除,然后再将该span的自由链表置空,因为page cache中的span是不需要切分成一个个的小对象的,以及该span的前后指针也都应该置空,因为之后要将其插入到page cache对应的双链表中。但span当中记录的起始页号以及它管理的页数是不能清除的,否则对应内存块就找不到了。

并且在central cache还span给page cache时也存在锁的问题,此时需要先将central cache中对应的桶锁解掉,然后再加上page cache的大锁之后才能进入page cache进行相关操作,当处理完毕回到central cache时,除了将page cache的大锁解掉,还需要立刻获得central cache对应的桶锁,然后将还未还完对象继续还给central cache中对应的span。


10. page cache回收内存

如果central cache中有某个span的_useCount减到0了,那么central cache就需要将这个span还给page cache了。

这个过程看似是非常简单的,page cache只需将还回来的span挂到对应的哈希桶上就行了。但实际为了缓解内存碎片的问题,page cache还需要尝试将还回来的span与其他空闲的span进行合并。

合并的过程可以分为向前合并和向后合并。如果还回来的span的起始页号是num,该span所管理的页数是n。那么在向前合并时,就需要判断第num-1页对应span是否空闲,如果空闲则可以将其进行合并,并且合并后还需要继续向前尝试进行合并,直到不能进行合并为止。而在向后合并时,就需要判断第num+n页对应的span是否空闲,如果空闲则可以将其进行合并,并且合并后还需要继续向后尝试进行合并,直到不能进行合并为止。

因此page cache在合并span时,是需要通过页号获取到对应的span的,这就是要把页号与span之间的映射关系存储到page cache的原因。

需要注意的是,当通过页号找到其对应的span时,这个span此时可能挂在page cache,也可能挂在central cache。而在合并时只能合并挂在page cache的span,因为挂在central cache的span当中的对象正在被其他线程使用。

可是我们不能通过span结构当中的_useCount成员,来判断某个span到底是在central cache还是在page cache。因为当central cache刚向page cache申请到一个span时,这个span的_useCount就是等于0的,这时可能当我们正在对该span进行切分的时候,page cache就把这个span拿去进行合并了,这显然是不合理的。

所以可以在span结构中再增加一个_isUse成员,用于标记这个span是否正在被使用,而当一个span结构被创建时我们默认该span是没有被使用的。

struct Span // 管理多个连续页大块内存跨度的结构
{PAGE_ID _pageId = 0; // 大块内存起始页的页号size_t  _n = 0;      // 页的数量Span* _next = nullptr;	// 双向链表的结构Span* _prev = nullptr;size_t _useCount = 0; // 切好小块内存,被分配给thread cache的计数void* _freeList = nullptr;  // 切好的小块内存的自由链表bool _isUse = false; // 是否在被使用
};

当central cache向page cache申请到一个span时,需要立即将该span的_isUse改为true。

当central cache将某个span还给page cache时,也就需要将该span的_isUse改成false。


在合并page cache当中的span时,需要通过页号找到其对应的span,而一个span是在被分配给central cache时,才建立的各个页号与span之间的映射关系,因此page cache当中的span也需要建立页号与span之间的映射关系。

与central cache中的span不同的是,在page cache中,只需建立一个span的首尾页号与该span之间的映射关系。因为当一个span在尝试进行合并时,如果是往前合并,那么只需要通过一个span的尾页找到这个span,如果是向后合并,那么只需要通过一个span的首页找到这个span。也就是说,在进行合并时我们只需要用到span与其首尾页之间的映射关系就够了。

因此申请k页的span时,如果是将n页的span切成了一个k页的span和一个n-k页的span,除了需要建立k页span中每个页与该span之间的映射关系之外,还需要建立剩下的n-k页的span与其首尾页之间的映射关系。

Span* PageCache::NewSpan(size_t k) // 获取一个K页的span
{assert(k > 0 && k < NPAGES);if (!_spanLists[k].Empty()) // 先检查第k个桶里面有没有span{//return _spanLists[k].PopFront(); // 有就直接返回Span* kSpan = _spanLists[k].PopFront();//建立页号与span的映射,方便central cache回收小块内存时查找对应的spanfor (PAGE_ID i = 0; i < kSpan->_n; i++){_idSpanMap[kSpan->_pageId + i] = kSpan;}return kSpan;}// 检查一下后面的桶里面有没有span,如果有->进行切分for (size_t i = k + 1; i < NPAGES; ++i){if (!_spanLists[i].Empty()){Span* nSpan = _spanLists[i].PopFront();Span* kSpan = new Span;// 在nSpan的头部切一个k页span下来返回,nSpan再挂到对应映射的位置kSpan->_pageId = nSpan->_pageId; // _pageId类似地址kSpan->_n = k;nSpan->_pageId += k; // 起始页的页号往后走nSpan->_n -= k; // 页数减等k_spanLists[nSpan->_n].PushFront(nSpan); // 将剩下的挂到对应映射的位置//存储nSpan的首尾页号与nSpan之间的映射,方便page cache合并span时进行前后页的查找_idSpanMap[nSpan->_pageId] = nSpan;_idSpanMap[nSpan->_pageId + nSpan->_n - 1] = nSpan;//建立页号与span的映射,方便central cache回收小块内存时查找对应的spanfor (PAGE_ID i = 0; i < kSpan->_n; i++){_idSpanMap[kSpan->_pageId + i] = kSpan;}return kSpan;}}// 走到这说明后面没有大页的span了->去找堆要一个128页的spanSpan* bigSpan = new Span;void* ptr = SystemAlloc(NPAGES - 1);bigSpan->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT; // 转换为页号bigSpan->_n = NPAGES - 1;_spanLists[bigSpan->_n].PushFront(bigSpan);return NewSpan(k); // 把新申请的span插入后再递归调用一次自己(避免代码重复)
}

此时page cache当中的span就都与其首尾页之间建立了映射关系,现在就可以进行span的合并了,需要注意的是,在向前或向后进行合并的过程中:

  • 如果没有通过页号获取到其对应的span,说明对应到该页的内存块还未申请,此时需要停止合并。
  • 如果通过页号获取到了其对应的span,但该span处于被使用的状态,那我们也必须停止合并。
  • 如果合并后大于128页则不能进行本次合并,因为page cache无法对大于128页的span进行管理。
void PageCache::ReleaseSpanToPageCache(Span* span)
{while (true) // 对span前后的页,尝试进行合并,缓解内存碎片问题{PAGE_ID prevId = span->_pageId - 1;auto ret = _idSpanMap.find(prevId);if (ret == _idSpanMap.end()) // 前面的页号没有,不合并{break;}Span* prevSpan = ret->second; // 前面相邻页的span在使用,不合并if (prevSpan->_isUse == true){break;}if (prevSpan->_n + span->_n > NPAGES - 1) // 合并出超过128页的span没办法管理,不合并{break;}span->_pageId = prevSpan->_pageId;span->_n += prevSpan->_n;_spanLists[prevSpan->_n].Erase(prevSpan);delete prevSpan;}while (true) // 向后合并{PAGE_ID nextId = span->_pageId + span->_n;auto ret = _idSpanMap.find(nextId);if (ret == _idSpanMap.end()){break;}Span* nextSpan = ret->second;if (nextSpan->_isUse == true){break;}if (nextSpan->_n + span->_n > NPAGES - 1){break;}span->_n += nextSpan->_n;_spanLists[nextSpan->_n].Erase(nextSpan);delete nextSpan;}_spanLists[span->_n].PushFront(span);span->_isUse = false;_idSpanMap[span->_pageId] = span;_idSpanMap[span->_pageId + span->_n - 1] = span;
}

在合并span时,由于这个span是在page cache的某个哈希桶的双链表当中的,因此在合并后需要将其从对应的双链表中移除,然后再将这个被合并了的span结构进行delete。

除此之外,在合并结束后,除了将合并后的span挂到page cache对应哈希桶的双链表当中,还需要建立该span与其首位页之间的映射关系,便于此后合并出更大的span。

测试代码:

void TestConcurrentAlloc3()
{void* p1 = ConcurrentAlloc(6);void* p2 = ConcurrentAlloc(8);void* p3 = ConcurrentAlloc(1);void* p4 = ConcurrentAlloc(7);void* p5 = ConcurrentAlloc(8);cout << p1 << endl; // 申请的小块内存正好是连续的cout << p2 << endl;cout << p3 << endl;cout << p4 << endl;cout << p5 << endl;ConcurrentFree(p1, 6);ConcurrentFree(p2, 8);ConcurrentFree(p3, 1);ConcurrentFree(p4, 7);ConcurrentFree(p5 ,8);cout << endl;cout << p1 << endl;cout << p2 << endl;cout << p3 << endl;cout << p4 << endl;cout << p5 << endl;
}void MultiThreadAlloc1()
{std::vector<void*> v;for (size_t i = 0; i < 7; ++i){void* ptr = ConcurrentAlloc(6);v.push_back(ptr);}for (auto e : v){ConcurrentFree(e, 6);}
}
void MultiThreadAlloc2()
{std::vector<void*> v;for (size_t i = 0; i < 7; ++i){void* ptr = ConcurrentAlloc(16);v.push_back(ptr);}for (auto e : v){ConcurrentFree(e, 16);}
}
void TestMultiThread()
{std::thread t1(MultiThreadAlloc1);std::thread t2(MultiThreadAlloc2);t1.join();t2.join();
}

至此我们便完成了这三个对象的申请和释放流程。


11. 大于256KB的内存申请和释放

11.1 申请

每个线程的thread cache是用于申请小于等于256KB的内存的,而对于大于256KB的内存,我们可以考虑直接向page cache申请,但page cache中最大的页也就只有128页,因此如果是大于128页的内存申请,就只能直接向堆申请了。

当申请的内存大于256KB时,虽然不是从thread cache进行获取,但在分配内存时也是需要进行向上对齐的,对于大于256KB的内存我们可以直接按页进行对齐。

之前实现RoundUp函数时,对传入字节数大于256KB的情况直接做了断言处理,因此这里需要对RoundUp函数稍作修改。

	static inline size_t RoundUp(size_t size) // 返回size对齐以后的值{if (size <= 128) {return _RoundUp(size, 8);}else if (size <= 1024) {return _RoundUp(size, 16);}else if (size <= 8 * 1024) {return _RoundUp(size, 128);}else if (size <= 64 * 1024) {return _RoundUp(size, 1024);}else if (size <= 256 * 1024) {return _RoundUp(size, 8 * 1024);}else{ //大于256KB的按页对齐return _RoundUp(size, 1 << PAGE_SHIFT);}}

现在对于之前的申请逻辑就需要进行修改了,当申请对象的大小大于256KB时,就不用向thread cache申请了,这时先计算出按页对齐后实际需要申请的页数,然后通过调用NewSpan申请指定页数的span即可。

static void* ConcurrentAlloc(size_t size)
{if (size > MAX_BYTES) //大于256KB的内存申请{//计算出对齐后需要申请的页数size_t alignSize = SizeClass::RoundUp(size);size_t kPage = alignSize >> PAGE_SHIFT;//向page cache申请kPage页的spanPageCache::GetInstance()->_pageMtx.lock();Span* span = PageCache::GetInstance()->NewSpan(kPage);span->_objSize = size;PageCache::GetInstance()->_pageMtx.unlock();void* ptr = (void*)(span->_pageId << PAGE_SHIFT);return ptr;}else{// 通过TLS,每个线程无锁地获取自己专属的ThreadCache对象if (pTLSThreadCache == nullptr){//pTLSThreadCache = new ThreadCache;static std::mutex tcMtx;static ObjectPool<ThreadCache> tcPool;tcMtx.lock();pTLSThreadCache = tcPool.New();tcMtx.unlock();}//cout << std::this_thread::get_id() << ":" << pTLSThreadCache << endl;return pTLSThreadCache->Allocate(size);}
}

也就是说,申请大于256KB的内存时,会直接调用page cache当中的NewSpan函数进行申请,因此这里要再对NewSpan函数进行改造,当需要申请的内存页数大于128页时,就直接向堆申请对应页数的内存块。而如果申请的内存页数是小于128页的,那就在page cache中进行申请,因此当申请大于256KB的内存调用NewSpan函数时也是需要加锁的,因为可能是在page cache中进行申请的。


11.2 释放

当释放对象时,我们需要判断释放对象的大小:

当释放对象时,需要先找到该对象对应的span,但是在释放对象时只知道该对象的起始地址。这也就是在申请大于256KB的内存时,也要给申请到的内存建立span结构,并建立起始页号与该span之间的映射关系的原因。此时就可以通过释放对象的起始地址计算出起始页号,进而通过页号找到该对象对应的span。

static void ConcurrentFree(void* ptr, size_t size)
{if (size > MAX_BYTES) //大于256KB的内存释放{Span* span = PageCache::GetInstance()->MapObjectToSpan(ptr);PageCache::GetInstance()->_pageMtx.lock();PageCache::GetInstance()->ReleaseSpanToPageCache(span);PageCache::GetInstance()->_pageMtx.unlock();}else{assert(pTLSThreadCache);pTLSThreadCache->Deallocate(ptr, size);}
}

因此page cache在回收span时也需要进行判断,如果该span的大小是小于等于128页的,那么直接还给page cache进行了,page cache会尝试对其进行合并。而如果该span的大小是大于128页的,那么说明该span是直接向堆申请的,我们直接将这块内存释放给堆,然后将这个span结构进行delete就行了。

直接向堆申请内存时我们调用的接口是VirtualAlloc,与之对应的将内存释放给堆的接口叫做VirtualFree,而Linux下的brk和mmap对应的释放接口叫做sbrk和unmmap。此时也可以将这些释放接口封装成一个叫做SystemFree的接口,需要将内存释放给堆时直接调用SystemFree即可。

inline static void SystemFree(void* ptr) // 直接将内存还给堆
{
#ifdef _WIN32VirtualFree(ptr, 0, MEM_RELEASE);
#else//linux下sbrk unmmap等
#endif
}


12. 使用定长内存池脱离使用new

tcmalloc是要在高并发场景下替代malloc进行内存申请的,因此tcmalloc在实现的时,其内部是不能调用malloc函数的,我们当前的代码中存在通过new获取到的内存,而new在底层实际上就是封装了malloc。

为了完全脱离掉malloc函数,此时之前实现的定长内存池就起作用了,代码中使用new时基本都是为Span结构的对象申请空间,而span对象基本都是在page cache层创建的,因此可以在PageCache类当中定义一个_spanPool,用于span对象的申请和释放。

class PageCache
{
public://...
private://...ObjectPool<Span> _spanPool;
};

然后将代码中使用new的地方替换为调用定长内存池当中的New函数,将代码中使用delete的地方替换为调用定长内存池当中的Delete函数。

Span* span = _spanPool.New(); //申请span对象_spanPool.Delete(span); //释放span对象

注意,当使用定长内存池当中的New函数申请Span对象时,New函数通过定位new也是对Span对象进行了初始化的。ConcurrentAlloc接口改一下:

		// 通过TLS,每个线程无锁地获取自己专属的ThreadCache对象if (pTLSThreadCache == nullptr){//pTLSThreadCache = new ThreadCache;static std::mutex tcMtx;static ObjectPool<ThreadCache> tcPool;tcMtx.lock();pTLSThreadCache = tcPool.New();tcMtx.unlock();}cout << std::this_thread::get_id() << ":" << pTLSThreadCache << endl;return pTLSThreadCache->Allocate(size);

这里我们将用于申请ThreadCache类对象的定长内存池定义为静态的,保持全局只有一个,让所有线程创建自己的thread cache时,都在个定长内存池中申请内存就行了。

但注意在从该定长内存池中申请内存时需要加锁,防止多个线程同时申请自己的ThreadCache对象而导致线程安全问题。


最后在SpanList的构造函数中也用到了new,因为SpanList是带头循环双向链表,所以在构造期间我们需要申请一个span对象作为双链表的头结点。

class SpanList // 带头双向循环链表
{
public:SpanList(){_head = _spanPool.New();_head->_next = _head;_head->_prev = _head;}
private:Span* _head;static ObjectPool<Span> _spanPool;
};

由于每个span双链表只需要一个头结点,因此将这个定长内存池定义为静态的,保持全局只有一个,让所有span双链表在申请头结点时,都在一个定长内存池中申请内存就行了。


13. 释放对象时优化成不传对象大小

使用malloc函数申请内存时,需要指明申请内存的大小;而使用free函数释放内存时,只需要传入指向这块内存的指针即可。

目前实现的内存池,在释放对象时除了需要传入指向该对象的指针,还需要传入该对象的大小。

原因如下:

  • 如果释放的是大于256KB的对象,需要根据对象的大小来判断这块内存到底应该还给page cache,还是应该直接还给堆。
  • 如果释放的是小于等于256KB的对象,需要根据对象的大小计算出应该还给thread cache的哪一个哈希桶。

如果我们也想做到,在释放对象时不用传入对象的大小,那么就需要建立对象地址与对象大小之间的映射。由于现在可以通过对象的地址找到其对应的span,而span的自由链表中挂的都是相同大小的对象。

因此可以在Span结构中再增加一个_objSize成员,该成员代表着这个span管理的内存块被切成的一个个对象的大小。

struct Span // 管理多个连续页大块内存跨度的结构
{PAGE_ID _pageId = 0; // 大块内存起始页的页号size_t  _n = 0;      // 页的数量Span* _next = nullptr;	// 双向链表的结构Span* _prev = nullptr;size_t _useCount = 0; // 切好小块内存,被分配给thread cache的计数void* _freeList = nullptr;  // 切好的小块内存的自由链表bool _isUse = false; // 是否在被使用size_t _objSize = 0; //切好的小对象的大小
};

而所有的span都是从page cache中拿出来的,因此每当我们调用NewSpan获取到一个k页的span时,就应该将这个span的_objSize保存下来。

代码中有两处,一处是在central cache中获取非空span时,如果central cache对应的桶中没有非空的span,此时会调用NewSpan获取一个k页的span;另一处是当申请大于256KB内存时,会直接调用NewSpan获取一个k页的span。

此时当我们释放对象时,就可以直接从对象的span中获取到该对象的大小,准确来说获取到的是对齐以后的大小。

static void ConcurrentFree(void* ptr)
{Span* span = PageCache::GetInstance()->MapObjectToSpan(ptr);size_t size = span->_objSize;if (size > MAX_BYTES) //大于256KB的内存释放{Span* span = PageCache::GetInstance()->MapObjectToSpan(ptr);PageCache::GetInstance()->_pageMtx.lock();PageCache::GetInstance()->ReleaseSpanToPageCache(span);PageCache::GetInstance()->_pageMtx.unlock();}else{assert(pTLSThreadCache);pTLSThreadCache->Deallocate(ptr, size);}
}

读取映射关系时的加锁问题

我们将页号与span之间的映射关系是存储在PageCache类当中的,当我们访问这个映射关系时是需要加锁的,因为STL容器是不保证线程安全的。

对于当前代码来说,如果我们此时正在page cache进行相关操作,那么访问这个映射关系是安全的,因为当进入page cache之前是需要加锁的,因此可以保证此时只有一个线程在进行访问。

但如果我们是在central cache访问这个映射关系,或是在调用ConcurrentFree函数释放内存时访问这个映射关系,那么就存在线程安全的问题。因为此时可能其他线程正在page cache当中进行某些操作,并且该线程此时可能也在访问这个映射关系,因此在page cache外部访问这个映射关系时是需要加锁的。

实际就是在调用page cache对外提供访问映射关系的函数时需要加锁,这里可以考虑使用C++当中的unique_lock,当然也可以用普通的锁。

Span* PageCache::MapObjectToSpan(void* obj)
{PAGE_ID id = ((PAGE_ID)obj >> PAGE_SHIFT);std::unique_lock<std::mutex> lock(_pageMtx); // 构造时加锁,析构时自动解锁auto ret = _idSpanMap.find(id);if (ret != _idSpanMap.end()){return ret->second;}else{assert(false);return nullptr;}
}

14. 多线程环境下对比malloc测试

之前只是对代码进行了一些基础的单元测试,下面在多线程场景下对比malloc进行测试。

// ntimes 一轮申请和释放内存的次数 || nworks 线程数 ||  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, malloc_costtime + free_costtime);
}// ntimes 一轮申请和释放内存的次数 || nworks 线程数 ||  rounds 轮次
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, malloc_costtime + free_costtime);
}int main()
{size_t n = 1000;cout << "=========================================================" << endl;BenchmarkConcurrentMalloc(n, 5, 10);cout << endl << endl;BenchmarkMalloc(n, 5, 10);cout << "=========================================================" << endl;return 0;
}

在测试函数中,通过clock函数分别获取到每轮次申请和释放所花费的时间,然后将其对应累加到malloc_costtime和free_costtime上。最后就得到了,nworks个线程跑rounds轮,每轮申请和释放ntimes次,这个过程申请所消耗的时间、释放所消耗的时间、申请和释放总共消耗的时间。

创建线程时让线程执行的是lambda表达式,而这里在使用lambda表达式时,以值传递的方式捕捉了变量k,以引用传递的方式捕捉了其他父作用域中的变量,因此可以将各个线程消耗的时间累加到一起。

将所有线程申请内存消耗的时间都累加到malloc_costtime上, 将释放内存消耗的时间都累加到free_costtime上,此时malloc_costtime和free_costtime可能被多个线程同时进行累加操作的,所以存在线程安全的问题。鉴于此,在定义这两个变量时使用了atomic类模板,这时对它们的操作就是原子操作了。

先来测试一下固定大小内存的申请和释放:(如果运行失败可以先看下面的调试技巧,这里调试完换到Release测试)

运行后可以看到,malloc的效率还是更高的。

由于此时申请释放的都是固定大小的对象,每个线程申请释放时访问的都是各自thread cache的同一个桶,当thread cache的这个桶中没有对象或对象太多要归还时,也都会访问central cache的同一个桶。此时central cache中的桶锁就不起作用了,因为我们让central cache使用桶锁的目的就是为了,让多个thread cache可以同时访问central cache的不同桶,而此时每个thread cache访问的却都是central cache中的同一个桶。

下面再来测试一下不同大小内存的申请和释放:(两个地方都要改)

在Release下已经超过malloc。

Debug运行后可以看到,相比malloc来说还是差一点。

15. 调试和复杂问题的调试技巧

多线程调试比单线程调试要复杂得多,调试时各个线程之间会相互切换,并且每次调试切换的时机也是不固定的,这就使得调试过程变得非常难以控制。

本人在此项目也在调试花了很多时间,下面举几个例子来记录一下调试过程和技巧。

下面是在上面多线程测试出现一些问题(问题已被本人调试找出,现改回有问题之前):

运行一下:(断言报错了)

自己写的断言报错还好,可以定位到报错的地方,而且这个问题是必现的。

现在根据报错找到Common.hpp的339行:

此时并不能找到上一步的原因,所以要用到条件断点来帮助调试:

1、条件断点

一般情况下我们可以直接运行程序,通过报错来查找问题。如果此时报的是断言错误,那么我们可以直接定位到报错的位置,然后将此处的断言改为与断言条件相反的if判断,在if语句里面打上一个断点,但注意空语句是无法打断点的,这时随便在if里面加上一句代码就可以打断点了。

运行到条件断点处后,我们就可以对当前程序进行进一步分析,找出断言错误的被触发原因。

2、查看函数栈帧

当程序运行到断点处时,需要对当前位置进行分析,如果检查后发现当前函数是没有问题的,这时可能需要回到调用该函数的地方进行进一步分析,此时依次点击“调试→窗口→调用堆栈”。

点击调用堆栈后:

双击函数栈帧中的其他函数,就可以跳转到该函数对应的栈帧。

需要注意的是,监视窗口只能查看当前栈帧中的变量。如果要查看此时其他函数栈帧中变量的情况,就可以通过函数栈帧跳转来查看。

此时知道有函数调用了PopFront,把鼠标移到各个变量发现_head的next和prev都等于_head,所以此时_head是空的,说明逻辑错了。

此时再跳到上一层栈帧:

此时就知道是手误了,正确逻辑应该是这样的:

上面错误的代码就相当于访问了第0个桶,第0个肯定是空的。

把条件断点换回断言再调试运行一下:

又崩了,还是跳到上几层栈帧:

此时还是找不到问题就要分析了,PopRange出问题了,大概率是Push或者PushRange有问题:

Push单个对象就是一个头插,怎么看都没问题,那应该就是PushRange有问题了:

此时再看看ThreadCache里调用PushRange的地方:

此时对比就应该想到是不是第三个参数有问题,所以在PushRange里改一下代码:

还是验证+条件断点:

	void PushRange(void* start, void* end, size_t n) // 头插一段范围的内存给_freeList{NextObj(end) = _freeList; // 赋值给end的头4/8字节_freeList = start;// 测试验证+条件断点int i = 0;void* cur = start;while (cur){cur = NextObj(cur);++i;}if (n != i){int test = 0;}_size += n; // 这里在参数增加一个个数n}

此时鼠标移到 n 和 i 或者通过自动窗口就能看到 n 和 i 并不能对应上,你说给我的是94个,但我验证了才有51个,所以还是上一层的问题,应该就是FetchRangeObj的问题,但是是同级的函数跳不到上一层栈帧,此时就把PushRange的调试代码注释掉,在FetchRangeObj再打类似的调试代码:

此时发现还是对不上,问题就在附近了,此时就找了很久就发现span有问题:

最下面的_objSize一个对象的大小是16,一页的大小是8 * 1024 = 8192,_useCount最大才512,但此时已经到533了。

所以此时就是span切错了,是GetOneSpan的问题,再往上一层打条件断点:

如果不看之前的代码能找到哪里出错吗?

此时运行结果是疑似死循环了,再看一个技巧:

3、疑似死循环时中断程序

当你在某个地方设置断点后,如果迟迟没有运行到断点处,而程序也没有崩溃,这时有可能是程序进入到某个死循环了。这时我们可以依次点击“调试→全部中断”。

这时程序就会在当前运行的地方停下来。

再按F10就发现死循环的原因是 j 出问题了。

此时就发现是少了一句代码,最后忘记把tail的next置空了:

现在终于找出了两个问题,前面都是固定申请16字节的大小,现让申请随机一点,把n调大一点:

找到断言错误的地方打个断点然后查看栈帧:

跳到上一层栈帧发现指针错了:

此时就看_idSpanMap有没有错,然后把unordered_map改成map一个个看桶里的数据。

最后就发现了这个问题(第一个问题的时候可能也看出来了):

在NewSpan这直接返回的也要建立和ID的映射:

调试技巧就先讲到这了,调试还是在于平时的积累和动手。


16. 性能瓶颈分析

下面分析当前项目的瓶颈在哪里,但这不能简单的凭感觉,应该用性能分析的工具来进行分析。

VS编译器中就带有性能分析的工具的,依次点击“调试→性能查看器”进行性能分析,注意该操作要在Debug模式下进行。

将代码中n的值由10000调成了1000,否则该分析过程可能会花费较多时间,并且将malloc的测试代码进行了屏蔽,因为要分析的是我们实现的高并发内存池。

点击后再选择检测选项查看各个函数的用时时间,然后点击开始就行了。


17. 使用基数树进行优化

tcmalloc源码中还有很多优化,其中之一就实现了基数树进行优化。

基数树实际上就是一个分层的哈希表,根据所分层数不同可分为单层基数树、二层基数树、三层基数树等。

单层基数树实际采用的就是直接定址法,每一个页号对应span的地址就存储数组中在以该页号为下标的位置。

最坏的情况下需要建立所有页号与其span之间的映射关系,因此这个数组中元素个数应该与页号的数目相同,数组中每个位置存储的就是对应span的指针。

下面对tcmalloc源码中的单层基数树进行简单修改:

// Single-level array
template <int BITS>
class TCMalloc_PageMap1 
{
private:static const int LENGTH = 1 << BITS;void** array_;public:typedef uintptr_t Number;//explicit TCMalloc_PageMap1(void* (*allocator)(size_t)) {explicit TCMalloc_PageMap1(){//array_ = reinterpret_cast<void**>((*allocator)(sizeof(void*) << BITS)); // 改成下面三行size_t size = sizeof(void*) << BITS; // 需要开辟数组的大小size_t alignSize = SizeClass::_RoundUp(size, 1 << PAGE_SHIFT); // 按页对齐后的大小array_ = (void**)SystemAlloc(alignSize >> PAGE_SHIFT); // 向堆申请空间(页数memset(array_, 0, sizeof(void*) << BITS); // 对申请到的内存进行清理}// Return the current value for KEY.  Returns NULL if not yet set,// or if k is out of range.void* get(Number k) const{if ((k >> BITS) > 0) // k的范围不在[0, 2^BITS-1]{return NULL;}return array_[k]; // 返回该页号对应的span}// REQUIRES "k" is in range "[0,2^BITS-1]".// REQUIRES "k" has been ensured before.//// Sets the value 'v' for key 'k'.void set(Number k, void* v) {array_[k] = v; // 建立映射}
};

此时当我们需要建立映射时就调用set函数,需要读取映射关系时,就调用get函数就行了。

代码中的非类型模板参数BITS表示存储页号最多需要比特位的个数。在32位下我们传入的是32-PAGE_SHIFT,在64位下传入的是64-PAGE_SHIFT。而其中的LENGTH成员代表的就是页号的数目,即2^BITS。

比如32位平台下,以一页大小为8K为例,此时页的数目就是2^32 / 2^13 = 2^19。因此存储页号最多需要19个比特位,此时传入非类型模板参数的值就是32 − 13 = 19。由于32位平台下指针的大小是4字节,因此该数组的大小就是2^19 * 4 = 2M,内存消耗不大,是可行的。但如果是在64位平台下,此时该数组的大小是2^51 * 8 = 2^54 = 2^24 G,这显然是不可行的,实际上对于64位的平台,我们需要使用三层基数树。

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

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

在二层基数树中,第一层的数组占用2^5 * 4 = 2^7 Byte空间,第二层的数组最多占用2^5 * 2 ^14 * 4 = 2M,二层基数树相比一层基数树的好处就是,一层基数树必须一开始就把2M的数组开辟出来,而二层基数树一开始时只需将第一层的数组开辟出来,当需要进行某一页号映射时再开辟对应的第二层的数组就行了。

// Single-level array
template <int BITS>
class TCMalloc_PageMap1 
{
private:static const int LENGTH = 1 << BITS;void** array_;public:typedef uintptr_t Number;//explicit TCMalloc_PageMap1(void* (*allocator)(size_t)) {explicit TCMalloc_PageMap1(){//array_ = reinterpret_cast<void**>((*allocator)(sizeof(void*) << BITS)); // 改成下面三行size_t size = sizeof(void*) << BITS; // 需要开辟数组的大小size_t alignSize = SizeClass::_RoundUp(size, 1 << PAGE_SHIFT); // 按页对齐后的大小array_ = (void**)SystemAlloc(alignSize >> PAGE_SHIFT); // 向堆申请空间(页数memset(array_, 0, sizeof(void*) << BITS); // 对申请到的内存进行清理}// Return the current value for KEY.  Returns NULL if not yet set,// or if k is out of range.void* get(Number k) const{if ((k >> BITS) > 0) // k的范围不在[0, 2^BITS-1]{return NULL;}return array_[k]; // 返回该页号对应的span}// REQUIRES "k" is in range "[0,2^BITS-1]".// REQUIRES "k" has been ensured before.//// Sets the value 'v' for key 'k'.void set(Number k, void* v) {array_[k] = v; // 建立映射}
};

因此在二层基数树中有一个Ensure函数,当需要建立某一页号与其span之间的映射关系时,需要先调用该Ensure函数确保用于映射该页号的空间是开辟了的,如果没有开辟则会立即开辟。

在32位平台下,就算将二层基数树第二层的数组全部开辟出来也就消耗了2M的空间,内存消耗也不算太多,因此可以在构造二层基数树时就把第二层的数组全部开辟出来。


上面一层基数树和二层基数树都适用于32位平台,而对于64位的平台就需要用三层基数树了。三层基数树与二层基数树类似,三层基数树实际上就是把存储页号的若干比特位分为三次进行映射。

此时只有当要建立某一页号的映射关系时,再开辟对应的数组空间,而没有建立映射的页号就可以不用开辟其对应的数组空间,此时就能在一定程度上节省内存空间。

因此当我们要建立某一页号的映射关系时,需要先确保存储该页映射的数组空间是开辟好了的,也就是调用代码中的Ensure函数,如果对应数组空间未开辟则会立马开辟对应的空间。

相关文章:

C++项目:高并发内存池_下

目录 8. thread cache回收内存 9. central cache回收内存 10. page cache回收内存 11. 大于256KB的内存申请和释放 11.1 申请 11.2 释放 12. 使用定长内存池脱离使用new 13. 释放对象时优化成不传对象大小 14. 多线程环境下对比malloc测试 15. 调试和复杂问题的调试技…...

【UE5】RTS游戏的框选功能+行军线效果实现

目录 效果 步骤 一、项目准备 二、框选NPC并移动到指定地点 三、框选效果 四、行军线效果 效果 步骤 一、项目准备 1. 新建一个俯视角游戏工程 2. 新建一个pawn、玩家控制器和游戏模式,这里分别命名为“MyPawn”、“MyController”和“MyGameMode” 3. 打开“MyGam…...

多图超详细:Docker安装知识库AI客服RAGFlow的详细步骤、使用教程及注意事项:

RAGFlow 介绍 RAGFlow 是一款基于深度文档理解的开源检索增强生成&#xff08;RAG&#xff09;引擎&#xff0c;通过结合信息检索与生成式 AI 技术&#xff0c;解决复杂场景下的数据处理和可信问答问题。其核心设计目标是提供透明化、可控化的文档处理流程&#xff0c;并通过多…...

docker compose安装智能体平台N8N

使用 docker volume create n8n_data 创建了一个名为 n8n_data 的数据卷。你通过 docker run 启动容器&#xff0c;映射了端口 5678&#xff0c;并挂载了 n8n_data 数据卷。 以下是对应的 docker-compose.yml 配置文件&#xff1a; version: "3.7"services:n8n:ima…...

FRP调用本地摄像头完成远程拍照

from flask import Flask, Response import cv2app Flask(__name__)# 基础文字回复 app.route(/) def hello_world():return <h1>你好啊世界</h1><img src"/camera" width"640" /># 摄像头拍照并返回图像 app.route(/camera) def captu…...

【Linux】39.一个基础的HTTP Web服务器

文章目录 1. 实现一个基础的HTTP Web服务器1.1 功能实现&#xff1a;1.2 Log.hpp-日志记录器1.3 HttpServer.hpp-网页服务器1.4 Socket.hpp-网络通信器1.5 HttpServer.cc-服务器启动器 1. 实现一个基础的HTTP Web服务器 1.1 功能实现&#xff1a; 总体功能&#xff1a; 提供We…...

蓝桥杯-小明的背包(动态规划-Java)

0/1背包问题介绍 0/1背包问题是经典的动态规划问题&#xff0c;具体描述如下&#xff1a; 解题思路&#xff1a; 输入数据 首先&#xff0c;程序通过 Scanner 从输入中读取数据&#xff1a; n 表示物品的数量。 v 表示背包的最大容量。 接着读取每个物品的重量和价值&#xff…...

(四十一)Dart 中的空安全与 `late` 关键字教程

Dart 中的空安全与 late 关键字教程 空安全简介 空安全&#xff08;Null Safety&#xff09;是 Dart 语言的一项重要特性&#xff0c;旨在帮助开发者避免空指针异常&#xff08;NullPointerException&#xff09;。空安全通过在编译时检查变量是否可能为 null&#xff0c;从而…...

GaussDB使用指南

目录 1. GaussDB 概述 1.1 GaussDB 简介 1.2 核心技术架构 1.3 适用场景与行业案例 2. GaussDB 安装与部署 2.1 环境准备与依赖检查 2.2 单机版安装&#xff08;Linux&#xff09; 2.3 分布式集群部署 3. GaussDB 基础操作与语法 3.1 数据库连接与用户管理 3.2 DDL …...

算法训练之动态规划(一)

♥♥♥~~~~~~欢迎光临知星小度博客空间~~~~~~♥♥♥ ♥♥♥零星地变得优秀~也能拼凑出星河~♥♥♥ ♥♥♥我们一起努力成为更好的自己~♥♥♥ ♥♥♥如果这一篇博客对你有帮助~别忘了点赞分享哦~♥♥♥ ♥♥♥如果有什么问题可以评论区留言或者私信我哦~♥♥♥ ✨✨✨✨✨✨ 个…...

dubbo配置中心

配置中心 简介 配置中心&#xff08;config-center&#xff09;在dubbo中可承担两类职责&#xff1a; 外部化配置&#xff1a;启动配置的集中式存储。流量治理规则存储。 Dubbo动态配置中心定义了两个不同层次的隔离选项&#xff0c;分别是namespace和group。 namespace&a…...

移动端六大语言速记:第11部分 - 内存管理

移动端六大语言速记&#xff1a;第11部分 - 内存管理 本文将对比Java、Kotlin、Flutter(Dart)、Python、ArkTS和Swift这六种移动端开发语言在内存管理方面的特性&#xff0c;帮助开发者理解和掌握各语言的内存管理机制。 11. 内存管理 11.1 垃圾回收机制对比 各语言垃圾回收…...

对象的创建方式有哪些?在虚拟机中具体的创建过程是怎样的?

在Java中&#xff0c;对象的创建方式及其在虚拟机中的具体过程如下&#xff1a; 一、对象的创建方式 使用 new 关键字 最常见的对象创建方式&#xff0c;直接调用类的构造方法。 MyClass obj new MyClass();反射&#xff08;Reflection&#xff09; 通过 Class 或 Constructor…...

openwrt软路由配置3

1.启用sftp文件连接 使用ssh连接openwrt时&#xff0c;我发现无法打开sftp windows进行上传和下载文件&#xff0c;提示 sftp channel closed by server: stderr:ash /usr/libexec/sftp-server:not found 原因是系统刚刚装好后&#xff0c;没有安装openssh-sftp-server包 opk…...

C语言for循环嵌套if相关题目

一、题目引入 以下代码程序运行结果是多少? 二、思路解析 进入一个for循环 a<100 进入第一个if b1不大于20为假 进入第二个if b4 a这时a自增为2 当b4时,满足第二个if条件 1.b4,a2 当b7时,满足第二个if条件 2.bb37,a3 当b10时,满足第二个if条件 …...

Redis与Mysql双写一致性如何保证?

我们在面试的时候redis与mysql双写一致性是一个常考的问题&#xff0c;今天我们就一起探讨一下吧 所谓的一致性就是数据的一致性&#xff0c;在分布式系统中&#xff0c;可以理解为多个节点中数据的值是一致的。 强一致性&#xff1a; 这种一致性级别是最符合用户直觉的&…...

STM32 CRC校验与芯片ID应用全解析:从原理到实践 | 零基础入门STM32第九十七步

主题内容教学目的/扩展视频CRC与芯片ID原理实现CRC校验和读取芯片ID为单片机应用提供数据验证和身份识别的功能。 师从洋桃电子&#xff0c;杜洋老师 &#x1f4d1;文章目录 一、CRC校验功能解析1.1 CRC基本原理1.2 核心功能对比 二、CRC校验应用实战2.1 典型应用场景2.2 程序实…...

《微服务与事件驱动架构》读书分享

《微服务与事件驱动架构》读书分享 Building Event-Driver Microservices 英文原版由 OReilly Media, Inc. 出版&#xff0c;2020 作者&#xff1a;[加] 亚当 • 贝勒马尔 译者&#xff1a;温正东 作者简介&#xff1a; 这本书由亚当贝勒马尔&#xff08;Adam Bellemare…...

⼤模型(LLMs)基础

⼤模型&#xff08;LLMs&#xff09;基础 ⽬前 主流的开源模型体系 有哪些&#xff1f;prefix Decoder 和 causal Decoder 和 Encoder-Decoder 区别是什么&#xff1f;⼤模型LLM的 训练⽬标 是什么&#xff1f;涌现能⼒是啥原因&#xff1f;为何现在的⼤模型⼤部分是Decoder o…...

IDEA :物联网ThingsBoard-gateway配置,运行Python版本,连接thingsboard,接入 MQTT 设备

准备阶段&#xff08;教程只针对本地操作&#xff0c;未涉及虚拟机环境&#xff09; Thingsboard源码编译并运行 没有操作过的小伙伴&#xff0c;可以看我上一篇文章 物联网ThingsBoard源码本地编译篇&#xff0c;超详细教程&#xff0c;小白看过来&#xff01;_thingsboard…...

面向大模型的开发框架LangChain

这篇文章会带给你 如何使用 LangChain&#xff1a;一套在大模型能力上封装的工具框架如何用几行代码实现一个复杂的 AI 应用面向大模型的流程开发的过程抽象 文章目录 这篇文章会带给你写在前面LangChain 的核心组件文档&#xff08;以 Python 版为例&#xff09;模型 I/O 封装…...

每日算法:洛谷U535992 J-C 小梦的宝石收集(双指针、二分)

题目描述 小梦有 n 颗能量宝石&#xff0c;其中第 i 颗的能量为 ai​&#xff0c;但这些能量宝石十分不稳定&#xff0c;随时有可能发生崩坏&#xff0c;导致他们全部消失&#xff01; 小梦想要留住宝石们&#xff0c;不希望他们发生崩坏&#xff0c;同时他发现&#xff1a;如…...

写给新人的深度学习扫盲贴:ReLu和梯度

一、ReLU&#xff08;Rectified Linear Unit&#xff0c;修正线性单元&#xff09; 梯度是深度学习中最常用的激活函数之一&#xff0c;因其简单、高效且能有效缓解梯度消失问题而被广泛使用。 1. 数学定义 函数表达式&#xff1a; $$ \text{ReLU}(x) \max(0, x) \begin{…...

Spring 框架的核心基础:IoC 和 AOP

一、IoC&#xff08;Inversion of Control&#xff0c;控制反转&#xff09; 定义&#xff1a; IoC&#xff08;Inversion of Control&#xff0c;控制反转&#xff09;&#xff0c;就是把对象创建和依赖关系的管理交给 Spring 容器&#xff0c;而不是由程序员手动去创建对象…...

JavaScript逆向工程实战:如何精准定位加密参数生成位置

前言&#xff1a;一个令人困惑的调试案例 最近在进行某网站的JavaScript逆向分析时&#xff0c;我遇到了一个有趣的现象&#xff1a;当我尝试定位一个名为m的加密参数&#xff08;值为MTIwMTE3NDQxODk1NTY1NjkA这样的Base64字符串&#xff09;时&#xff0c;调试器却带我来到了…...

SSM智能停车场管理系统

&#x1f345;点赞收藏关注 → 添加文档最下方联系方式咨询本源代码、数据库&#x1f345; 本人在Java毕业设计领域有多年的经验&#xff0c;陆续会更新更多优质的Java实战项目希望你能有所收获&#xff0c;少走一些弯路。&#x1f345;关注我不迷路&#x1f345; 项目视频 SS…...

[定位器]晶艺LA1823,4.5V~100V, 3.5A,替换MP9487,MP9486A,启烨科技

Features  4.5V to 100V Wide Input Range  3.5A Typical Peak Current Limit  Integrated 500mΩ low resistance high side power MOS.  Constant On Time Control with Constant Switching Frequency.  180μA Low Quiescent Current  150kHz/240kHz/420kHz Swi…...

天元证券|A股大反攻!北证50涨超10%!芯片股大爆发

今日&#xff0c;A股全线走强。 科技成长股领涨&#xff0c;北证50指数飙升逾10%&#xff0c;科创50也大涨超4%&#xff0c;深证成指、上证指数午后也稳步拉升涨逾1%。值得注意的是&#xff0c;上证50指数临近收盘集合竞价的时候直线拉升。近4600只个股上涨&#xff0c;成交稳步…...

利用python从零实现Byte Pair Encoding(BPE):NLP 中的“变形金刚”

BPE&#xff1a;NLP 界的“变形金刚”&#xff0c;从零开始的奇幻之旅 在自然语言处理&#xff08;NLP&#xff09;的世界里&#xff0c;有一个古老而神秘的传说&#xff0c;讲述着一种强大的魔法——Byte Pair Encoding&#xff08;BPE&#xff09;。它能够将普通的文本“变形…...

最新Web系统全面测试指南

你有没有遇到过这样的情况&#xff1a; 系统上线当天&#xff0c;用户频频报错&#xff0c;运维一脸懵逼&#xff0c;开发说“我本地没问题”&#xff1f; 你明明写了几十个测试用例&#xff0c;结果却还是有 Bug 漏网&#xff1f; Web 系统测试&#xff0c;不只是点点点&#…...

OpenBMC:BmcWeb 处理http请求6 调用路由处理函数

OpenBMC:BmcWeb 处理http请求5 检查权限-CSDN博客 检查完权限后,调用了rule.handle(*req, asyncResp, params); template <typename... Args> class TaggedRule :public BaseRule,public RuleParameterTraits<TaggedRule<Args...>> {void handle(const Req…...

售货机管理系统:智慧零售时代的运营新引擎

一、引言 在快节奏的都市生活中,自动售货机已成为便捷消费的重要场景。然而,传统售货机依赖人工补货、手工对账,常面临库存失衡、设备故障发现滞后、数据孤岛等痛点。如何突破效率瓶颈?本文将深入剖析榕壹云售货机管理系统的项目背景、客户定位、技术与核心功能、系统优势…...

Python基础全解析:从输入输出到字符编码的深度探索

一、Python程序交互的基石&#xff1a;Print函数详解 1.1 基础输出功能 # 输出数字 print(20.5) # 输出浮点数&#xff1a;20.5 print(0b0010) # 输出二进制数&#xff1a;10# 输出字符串 print(Hello World!) # 经典输出示例# 表达式计算 print(4 4 * (2-1)…...

Python第八章02:数据可视化Pyecharts包无法使用

PS:本节纯属个人在学习过程中遇到问题、解决问题的经验分享&#xff0c;对学习进度没影响&#xff0c;没有遇到该问题的小伙伴可跳过。 首先&#xff0c;在学习数据图形化过程中&#xff0c;通过命令提示符安装了Pyecharts包&#xff0c;在命令提示符中验证安装成功。 在PyChar…...

【人工智能】如何通过精准提示工程实现完美的珠宝首饰展示

AI艺术创作指南&#xff1a;如何通过精准提示工程实现完美的珠宝首饰展示 引言&#xff1a;认知边界的突破 在AI艺术创作的漫长探索中&#xff0c;许多创作者面临着相似的困扰&#xff1a;当他们看到别人能够通过算法编织出如同文艺复兴时期细腻油画般的奢华珠宝展示图&#…...

Redis学习总结(持续更新)

Redis 目前在学习redis&#xff0c;遇到的一些问题会放在这里&#xff0c;加深自己的印象。 1. Redis缓存相较于传统Session存储的特点 Session的存储方式&#xff1a; 通常&#xff0c;传统的Session是存储在应用服务器的内存中&#xff0c;比如Tomcat的Session管理器。用户…...

RabbitMQ从入门到实战-3(高可靠性)

文章目录 发送者可靠性发送者重连发送者确认&#xff08;一般不会开启&#xff09;指定returncallback和confrimfallbacktips MQ可靠性数据持久化LazyQueue&#xff08;默认模式且不可更改&#xff09; 消费者的可靠性消费者确认机制消费者失败重试业务幂等性唯一消息id业务判断…...

RTK 实时动态定位概述

01 引言 RTK(实时动态定位,Real-Time Kinematic)是一种高精度的卫星导航定位技术,通过差分校正方法,将GNSS(全球导航卫星系统)的定位精度从米级提升至厘米级(通常1-3厘米),广泛应用于测绘、无人机、自动驾驶、精准农业等领域。 02 概述 1. RTK的基本原理 RTK的核…...

Conda 环境离线迁移实战:解决生产环境网络限制的高效方案20250409

Conda 环境离线迁移实战&#xff1a;解决生产环境网络限制的高效方案 在生产环境无法联网的前提下&#xff0c;如何高效、安全地部署 Python 虚拟环境&#xff0c;是许多企业在实际运维中必须面对的问题。特别是当前常见的开发环境基于 Miniconda&#xff0c;生产环境使用 Ana…...

dify使用知识库

注意 要用向量模型 导入文件 选择向量模型 要下载好后&#xff0c;才可以导入模型&#xff0c; 这个模型没法在ollama中run 聊天工具添加知识库 效果...

HTTP:一.概述

http是干嘛的? 超文本传输协议(英语:HyperText Transfer Protocol,缩写:HTTP)是一种用于分布式、协作式和超媒体信息系统的应用层协议。HTTP是万维网的数据通信的基础。设计HTTP最初的目的是为了提供一种发布和接收HTML页面的方法。通过HTTP或者HTTPS协议请求的资源由统…...

Appium工作原理及环境的搭建(1)

1、Appium的介绍&#xff1a; 一、什么是Appium Desktop&#xff1f; Appium Desktop是Appium项目的桌面版GUI工具&#xff0c;提供了一个友好的界面&#xff0c;用于启动Appium服务器、查看设备日志、与设备交互、调试自动化脚本等。相比于命令行工具&#xff0c;Appium Des…...

Interactron: Embodied Adaptive Object Detection(训练时进行更新参数) 还没看懂

Interactron: Embodied Adaptive Object Detection 创新点 这些方法通常存在两个主要的共同假设。第一&#xff0c;模型在固定的训练集上进行训练&#xff0c;并在预先录制的测试集上进行评估。第二&#xff0c;模型在训练阶段结束后保持冻结状态&#xff0c;即训练完成后不再…...

【Pandas】pandas DataFrame copy

Pandas2.2 DataFrame Conversion 方法描述DataFrame.astype(dtype[, copy, errors])用于将 DataFrame 中的数据转换为指定的数据类型DataFrame.convert_dtypes([infer_objects, …])用于将 DataFrame 中的数据类型转换为更合适的类型DataFrame.infer_objects([copy])用于尝试…...

Redis基础指令(Windows)

1.cmd命令行启动redis 直接cmd打开整个文件 1.1.启动server 输入指令&#xff1a; redis-server.exe redis.windows.conf 会进入serve端 1.2.启动客户端 &#xff01;&#xff01;重新打开一个cmd&#xff0c;方法和上面一样&#xff01;&#xff01; 之后输入 redis-…...

MV-DLS600P激光振镜立体相机(MV-DLS600P)重要参数解析

功能特性 采用激光振镜技术&#xff0c;亚毫米级图像采集精度 高能效激光模块配合精准曝光同步&#xff0c;性能更稳定 支持多帧融合&#xff0c;无惧金属工件表面反光干扰 支持RGB、深度图同步对齐输出&#xff0c;便于二次开发 配备窄带滤光片&#xff0c;抗干扰能力更强&…...

C语言【输出字符串中的大写字母】

题目 输出字符串中的大写字母 思路&#xff08;注意事项&#xff09; 纯代码 #include<stdio.h> #include<string.h>int main(){char str[20], ans[20];fgets(str, sizeof(str), stdin);str[strcspn(str, "\n")] \0;for (int i 0, j 0; i < strl…...

UniApp基于xe-upload实现文件上传组件

xe-upload地址&#xff1a;文件选择、文件上传组件&#xff08;图片&#xff0c;视频&#xff0c;文件等&#xff09; - DCloud 插件市场 致敬开发者&#xff01;&#xff01;&#xff01; 感觉好用的话&#xff0c;给xe-upload的作者一个好评 背景&#xff1a;开发中经常会有…...

deque容器

1.定义 也叫双端数组&#xff0c;可以对头部进行插入和删除。 2.与vector区别 3.内部工作原理 他是把整个地址划分成多块小地址&#xff08;缓冲区&#xff09;&#xff0c;然后有一个中控区去记录这些地址&#xff0c;然后访问的时候先通过中控区然后再转到相应的缓冲区&am…...

git 总结遇到的问题

git Push 报错 Push failed send-pack: unexpected disconnect while reading sideband packet Total 2269 (delta 418), reused 0 (delta 0), pack-reused 0 the remote end hung up unexpectedly 解决方案&#xff1a;增加 Git 的缓冲区&#xff0c;有时由于数据量大或网络…...