CentralCache中心缓存

news2024/12/30 3:17:21

目录

一.CentralCache基本结构

1.CentralCache任务

2.基本结构

二.函数调用层次结构/.h文件

三.Span和SpanList的封装

Span:大块内存跨度

PAGE_ID _pageId

size_t _objSize

_useCount

SpanList:管理Span的双链表(桶锁)

四.获取大块内存GetOneSpan

五.FetchRangeObj输出内存对象

六.ReleaseListToSpans

MapObjectToSpan

为什么读取基数树映射关系时不需要加锁?

七.基数树代码

单层基数树

双层基数树

八.CentralCache.cpp/.h


一.CentralCache基本结构

1.CentralCache任务

有与ThreadCache相同数量的哈希桶,分别管理一个SpanList,负责从PageCache获取大块内存Span,完成切分后挂到freeList,当ThreadCache对应的桶无内存时,再从对应的freeList切分一段给ThreadCache.

2.基本结构

CentralCache与ThreadCache有两个明显不同的地方.
        首先,ThreadCache是每个线程独享的,而CentralCache是所有线程共享的一个单例,因为每个线程的CentralCache没有内存了都会去找CentralCache,因此在访问CentralCache时是需要加锁的。

  但CentralCache在加锁时并不是将整个CentralCache全部锁上了,CentralCache在加锁时用的是桶锁,也就是说每个桶都有一把锁
此时只有当多个线程同时访问CentralCache的同一个桶时才会存在锁竞争,如果是多个线程同时访问CentralCache的不同桶就不会存在锁竞争。

  CentralCache与ThreadCache的第二个不同之处就是,ThreadCache的每个桶中挂的是一个个切好的内存块,而CentralCache的每个桶中挂的由SpanList管理的Span.

二.函数调用层次结构/.h文件

三.Span和SpanList的封装

Span:大块内存跨度

// 管理多个连续页大块内存跨度结构
struct Span
{
	PAGE_ID _pageId = 0; // 大块内存起始页的页号
	size_t  _n = 0;      // 页的数量

	Span* _next = nullptr;	// 双向链表的结构
	Span* _prev = nullptr;

	size_t _objSize = 0;  // 切好的小对象的大小
	size_t _useCount = 0; // 切好小块内存,被分配给ThreadCache的计数
	void* _freeList = nullptr;  // 切好的小块内存的自由链表

	bool _isUse = false;   // 是否在被使用
};

PAGE_ID _pageId

表示大块内存起始页的页号,由于大块内存可能由好多page组成,因此Span中记录起始页号,方便后续进行内存管理._n为该Span中page的个数

size_t _objSize

Span中_freeList管理的每个小块内存对象的实际大小

_useCount

该Span中的小块内存被分配给ThreadCache使用的个数.初始状态为0
当use_Count再次为0时,代表这个Span中所有被分配出去的小块儿内存都被ThreadCache还回来了,此时可直接将这个Span从还给PageCache
而_isUse就是区分_useCount为0时的2个不同状态的

SpanList:管理Span的双链表(桶锁)

// 带头双向循环链表 
class SpanList
{
public:
	SpanList()
	{
		_head = new Span;
		_head->_next = _head;
		_head->_prev = _head;
	}
	Span* Begin()
	{
		return _head->_next;
	}
	Span* End()
	{
		return _head;
	}
	bool Empty()
	{
		return _head->_next == _head;
	}

	void PushFront(Span* span)
	{
		Insert(Begin(), span);
	}

	Span* PopFront()
	{
		Span* front = _head->_next;
		Erase(front);
		return front;
	}

	void Insert(Span* pos, Span* newSpan)
	{
		assert(pos);
		assert(newSpan);

		Span* prev = pos->_prev;
		// prev newspan pos
		prev->_next = newSpan;
		newSpan->_prev = prev;
		newSpan->_next = pos;
		pos->_prev = newSpan;
	}

	void Erase(Span* pos)
	{
		assert(pos);
		assert(pos != _head);

		Span* prev = pos->_prev;
		Span* next = pos->_next;

		prev->_next = next;
		next->_prev = prev;
	}

private:
	Span* _head;
public:
	std::mutex _mtx; // 桶锁

};

从双链表删除的Span会还给下一层的PageCache,相当于只是把这个Span从双链表中移除,因此不需要对删除的Span进行delete.

四.获取大块内存GetOneSpan

从SpanList获取一个非空的Span,如果没有,就从PageCache获取一个NewSpan并切分好再返回.

// 获取一个非空的span
Span* CentralCache::GetOneSpan(SpanList& list, size_t size)
{
	//1.sList中有span就返回
	Span* span = list.Begin();
	while (span != list.End())
	{
		if (span->_freeList)
			return span;
		else
			span = span->_next;
	}

	// 先把CentralCache的桶锁解掉,这样如果其他线程释放内存对象回来,不会阻塞
	list._mtx.unlock();

	//2.list中没有span,向PageCache申请
	PageCache::GetInstance()->_pageMtx.lock();
	span = PageCache::GetInstance()->NewSpan(SizeClass::NumMovePage(size));
	PageCache::GetInstance()->_pageMtx.unlock();
	span->_isUse = true;
	span->_objSize = size;

	// 对获取到的Span进行切分,不需要加锁,因为此时其他线程访问不到这个Span
	// 计算Span的大块内存的起始地址和大块内存的大小(字节数)
    //这个页的起始地址是 PageId*8*1024,第0页的地址是0,以此类推
	//计算这个span总共有多少个字节,用_n(页数)*8*1024
	char* start = (char*)(span->_pageId << PAGE_SHIFT);
	size_t bytes = span->_n << PAGE_SHIFT;
	char* end = start + bytes;

	//3.对span管理的大块内存进行切分,尾插链接到_freeList
	span->_freeList = start;
	start += size;
	void* tail = span->_freeList;
	int i = 1;
	while (start < end)
	{
		++i;
		NextObj(tail) = start;
		tail = NextObj(tail); // tail = start;
		start += size;
	}
	NextObj(tail) = nullptr;

	//4.将申请到的新的span头插到list,访问共享资源,需要加锁
	list._mtx.lock();
	list.PushFront(span);

	return span;
}

操作单个Span时不用加锁,此时该Span一定没有被使用,但操作SpanList时需要加锁.

五.FetchRangeObj输出内存对象

被ThreadCache中的FetchFromCentralCache所调用,用start和end来标识被ThreadCache取走的一批量内存的首尾2个.

// 从中心缓存获取一定数量的对象给ThreadCache,   输出型参数
size_t CentralCache::FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size)
{
	size_t index = SizeClass::Index(size);
	_spanLists[index]._mtx.lock();

	Span* span = GetOneSpan(_spanLists[index], size);
	assert(span && span->_freeList);

	//1.span中对象充足,则取batchNum个
	//2.span不足,则end指向最后一个
    //从前向后取
	start = end = span->_freeList;
	size_t actualNum = 1;
	for (size_t i = 0; i < batchNum - 1; ++i)
	{
		if (NextObj(end) == nullptr)break;
		++actualNum;
		end = NextObj(end);
	}
	span->_freeList = NextObj(end);
	span->_useCount += actualNum;
	NextObj(end) = nullptr;

	_spanLists[index]._mtx.unlock();

	return actualNum;
}

        由于CentralCache是所有线程共享的,所以在访问CentralCache中的哈希桶时,需要先给对应的哈希桶加上桶锁,在获取到对象后再将桶锁解掉。

  在从CentralCache获取对象时,先是在CentralCache对应的哈希桶中获取到一个非空Span,然后从这个Span的自由链表中取出个对象即可,但可能这个非空的span的自由链表当中对象的个数不足batchNum个,这时该自由链表当中有多少个对象就给多少就行了。

  也就是说,ThreadCache实际从CentralCache获得的对象的个数可能与我们传入的batchNum是不一样的,因此我们需要统计本次申请过程中,ThreadCache实际获取到的对象个数,然后根据该值及时更新这个Span中的小对象被分配给ThreadCache的计数_useCount
        事实上,ThreadCache只要求我们取出1个对象,我们期望取出batchNum是为了整体效率的优化,但是若实际上取出的个数小于batchNum个也是没有问题的.

六.ReleaseListToSpans

void CentralCache::ReleaseListToSpans(void* start, size_t size)
{
	size_t index = SizeClass::Index(size);
	SpanList& sList = _spanLists[index];
	sList._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
		if (span->_useCount == 0)
		{
			sList.Erase(span);
			span->_freeList = nullptr;
			span->_next = nullptr;
			span->_prev = nullptr;

			//将span还给PageCache
			sList._mtx.unlock();

			PageCache::GetInstance()->_pageMtx.lock();
			PageCache::GetInstance()->ReleaseSpanToPageCache(span);
			PageCache::GetInstance()->_pageMtx.unlock();

			sList._mtx.lock();
		}

		start = next;
	}

	sList._mtx.unlock();
}

当ThreadCache中某个自由链表太长时,会将自由链表当中的这些对象还给CentralCache中的对应的Span

  但是需要注意的是,还给CentralCache的这些对象不一定都是属于同一个Span的CentralCache中的每个哈希桶当中可能都不止一个Span,因此当我们计算出还回来的对象应该还给CentralCache的哪一个桶后,还需要知道这些对象到底应该还给这个桶当中的哪一个Span

        因为通过对象的地址除以一个page的大小即可得到页号,因此需要再建立PAGE_ID到Span映射关系方便还内存给Span.

 这时当ThreadCache还对象给CentralCache时,就可以依次遍历这些对象,这些对象插入到其对应span的自由链表当中,并且及时更新该span的_useCount计数即可。 
 在ThreadCache还对象给CentralCache的过程中,如果CentralCache中某个span的_useCount减到0时,说明这个span分配出去的对象全部都还回来了,那么此时就可以将这个span再进一步还给PageCache

需要注意,如果要把某个span还给PageCache,我们需要先将这个span从CentralCache对应的双链表中移除,然后再将该span的自由链表置空,因为PageCache中的span是不需要切分成一个个的小对象的,以及该span的前后指针也都应该置空,因为之后要将其插入到PageCache对应的双链表中。但span当中记录的起始页号以及它管理的页数是不能清除的,否则对应内存块就找不到了.(从SpanList中移除)
        并且在CentralCache还span给PageCache时也存在锁的问题,此时需要先将CentralCache中对应的桶锁解掉,然后再加上PageCache的大锁之后才能进入PageCache进行相关操作,当处理完毕回到CentralCache时,除了将PageCache的大锁解掉,还需要立刻获得CentralCache对应的桶锁,然后将还未还完对象继续还给CentralCache中对应的span(相关加锁解锁操作)

MapObjectToSpan

用unordered_map即可,为了效率优化,这里优化成了基数树.

Span* PageCache::MapObjectToSpan(void* obj)
{
	PAGE_ID id = ((PAGE_ID)obj >> PAGE_SHIFT);
	Span* ret = (Span*)_idSpanMap.get(id);
	assert(ret != nullptr);
	return ret;

	//std::unique_lock<std::mutex> lock(_pageMtx);
	//auto ret = _idSpanMap.find(id);
	//if (ret != _idSpanMap.end())
	//{
	//	return ret->second;
	//}
	//else
	//{
	//	assert(false);
	//	return nullptr;
	//}
}

为什么读取基数树映射关系时不需要加锁?

当某个线程在读取映射关系时,可能另外一个线程正在建立其他页号的映射关系,而此时无论我们用的是C++当中的map还是unordered_map,在读取映射关系时都是需要加锁(读写锁)的。

  因为C++中map的底层数据结构是红黑树,unordered_map的底层数据结构是哈希表,而无论是红黑树还是哈希表,当我们在插入数据时其底层的结构都有可能会发生变化。
比如红黑树在插入数据时可能会引起树的旋转,而哈希表在插入数据时可能会引起哈希表扩容。此时要避免出现数据不一致的问题,就不能让插入操作和读取操作同时进行,因此我们在读取映射关系的时候是需要加锁的。 
 而对于基数树来说就不一样了,基数树的空间一旦开辟好了就不会发生变化,因此无论什么时候去读取某个页的映射,都是对应在一个固定的位置进行读取的
        并且我们不会同时对同一个页进行读取映射和建立映射的操作,因为我们只有在释放对象时才需要读取映射,而建立映射的操作都是在PageCache中进行的。
        也就是说,读取映射时读取的都是对应Span的_useCount !=0的页,而建立映射时建立的都是对应Span的_useCount == 0的页,所以说我们不会同时对同一个页进行读取映射和建立映射的操作。

七.基数树代码

使用基数树时间效率(不用加锁)和空间上都能进行有效优化

#pragma once
#include"Common.h"

// 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));
		//对于1层基数树,提前用SystemAlloc开好空间
		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) {
			return NULL;
		}
		return array_[k];
	}

	// 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;
	}
};

// Two-level radix tree
template <int BITS>
class TCMalloc_PageMap2 {
private:
	// Put 32 entries in the root and (2^BITS)/32 entries in each leaf.
	static const int ROOT_BITS = 5;
	static const int ROOT_LENGTH = 1 << ROOT_BITS;

	static const int LEAF_BITS = BITS - ROOT_BITS;
	static const int LEAF_LENGTH = 1 << LEAF_BITS;

	// Leaf node
	struct Leaf {
		void* values[LEAF_LENGTH];
	};

	Leaf* root_[ROOT_LENGTH];             // Pointers to 32 child nodes
	void* (*allocator_)(size_t);          // Memory allocator

public:
	typedef uintptr_t Number;

	//explicit TCMalloc_PageMap2(void* (*allocator)(size_t)) {
	explicit TCMalloc_PageMap2() {
		//allocator_ = allocator;
		memset(root_, 0, sizeof(root_));

		//构造函数提前开好空间
		PreallocateMoreMemory();
	}

	void* get(Number k) const {
		const Number i1 = k >> LEAF_BITS;
		const Number i2 = k & (LEAF_LENGTH - 1);
		if ((k >> BITS) > 0 || root_[i1] == NULL) {
			return NULL;
		}
		return root_[i1]->values[i2];
	}

	void set(Number k, void* v) {
		const Number i1 = k >> LEAF_BITS;
		const Number i2 = k & (LEAF_LENGTH - 1);
		ASSERT(i1 < ROOT_LENGTH);
		root_[i1]->values[i2] = v;
	}

	bool Ensure(Number start, size_t n) {
		for (Number key = start; key <= start + n - 1;) {
			const Number i1 = key >> LEAF_BITS;

			// Check for overflow
			if (i1 >= ROOT_LENGTH)
				return false;

			// Make 2nd level node if necessary
			if (root_[i1] == NULL) {
				//Leaf* leaf = reinterpret_cast<Leaf*>((*allocator_)(sizeof(Leaf)));
				//if (leaf == NULL) return false;
				static ObjectPool<Leaf>	leafPool;
				Leaf* leaf = (Leaf*)leafPool.New();

				memset(leaf, 0, sizeof(*leaf));
				root_[i1] = leaf;
			}

			// Advance key past whatever is covered by this leaf node
			key = ((key >> LEAF_BITS) + 1) << LEAF_BITS;
		}
		return true;
	}

	void PreallocateMoreMemory() {
		// Allocate enough to keep track of all possible pages
		Ensure(0, 1 << BITS);
	}
};

// Three-level radix tree
template <int BITS>
class TCMalloc_PageMap3 {
private:
	// How many bits should we consume at each interior level
	static const int INTERIOR_BITS = (BITS + 2) / 3; // Round-up
	static const int INTERIOR_LENGTH = 1 << INTERIOR_BITS;

	// How many bits should we consume at leaf level
	static const int LEAF_BITS = BITS - 2 * INTERIOR_BITS;
	static const int LEAF_LENGTH = 1 << LEAF_BITS;

	// Interior node
	struct Node {
		Node* ptrs[INTERIOR_LENGTH];
	};

	// Leaf node
	struct Leaf {
		void* values[LEAF_LENGTH];
	};

	Node* root_;                          // Root of radix tree
	void* (*allocator_)(size_t);          // Memory allocator

	Node* NewNode() {
		Node* result = reinterpret_cast<Node*>((*allocator_)(sizeof(Node)));
		if (result != NULL) {
			memset(result, 0, sizeof(*result));
		}
		return result;
	}

public:
	typedef uintptr_t Number;

	explicit TCMalloc_PageMap3(void* (*allocator)(size_t)) {
		allocator_ = allocator;
		root_ = NewNode();
	}

	void* get(Number k) const {
		const Number i1 = k >> (LEAF_BITS + INTERIOR_BITS);
		const Number i2 = (k >> LEAF_BITS) & (INTERIOR_LENGTH - 1);
		const Number i3 = k & (LEAF_LENGTH - 1);
		if ((k >> BITS) > 0 ||
			root_->ptrs[i1] == NULL || root_->ptrs[i1]->ptrs[i2] == NULL) {
			return NULL;
		}
		return reinterpret_cast<Leaf*>(root_->ptrs[i1]->ptrs[i2])->values[i3];
	}

	void set(Number k, void* v) {
		ASSERT(k >> BITS == 0);
		const Number i1 = k >> (LEAF_BITS + INTERIOR_BITS);
		const Number i2 = (k >> LEAF_BITS) & (INTERIOR_LENGTH - 1);
		const Number i3 = k & (LEAF_LENGTH - 1);
		reinterpret_cast<Leaf*>(root_->ptrs[i1]->ptrs[i2])->values[i3] = v;
	}

	bool Ensure(Number start, size_t n) {
		for (Number key = start; key <= start + n - 1;) {
			const Number i1 = key >> (LEAF_BITS + INTERIOR_BITS);
			const Number i2 = (key >> LEAF_BITS) & (INTERIOR_LENGTH - 1);

			// Check for overflow
			if (i1 >= INTERIOR_LENGTH || i2 >= INTERIOR_LENGTH)
				return false;

			// Make 2nd level node if necessary
			if (root_->ptrs[i1] == NULL) {
				Node* n = NewNode();
				if (n == NULL) return false;
				root_->ptrs[i1] = n;
			}

			// Make leaf node if necessary
			if (root_->ptrs[i1]->ptrs[i2] == NULL) {
				Leaf* leaf = reinterpret_cast<Leaf*>((*allocator_)(sizeof(Leaf)));
				if (leaf == NULL) return false;
				memset(leaf, 0, sizeof(*leaf));
				root_->ptrs[i1]->ptrs[i2] = reinterpret_cast<Node*>(leaf);
			}

			// Advance key past whatever is covered by this leaf node
			key = ((key >> LEAF_BITS) + 1) << LEAF_BITS;
		}
		return true;
	}

	void PreallocateMoreMemory() {
	}
};

单层基数树

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

双层基数树

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

  比如用前5个比特位在基数树的第一层进行映射,映射后得到对应的第二层,然后用剩下的比特位在基数树的第二层进行映射,映射后最终得到该页号对应的span指针。
        二层基数树相比一层基数树的好处就是,一层基数树必须一开始就把整个完整空间的数组开辟出来,而二层基数树一开始时只需将第一层的数组开辟出来,当需要进行某一页号映射时再开辟对应的第二层的数组就行了。

八.CentralCache.cpp/.h

#include "CentralCache.h"
#include "PageCache.h"

CentralCache CentralCache::_sInst;

// 获取一个非空的span
Span* CentralCache::GetOneSpan(SpanList& list, size_t size)
{
	//1.sList中有span就返回
	Span* span = list.Begin();
	while (span != list.End())
	{
		if (span->_freeList)
			return span;
		else
			span = span->_next;
	}

	// 先把central cache的桶锁解掉,这样如果其他线程释放内存对象回来,不会阻塞
	list._mtx.unlock();

	//2.list中没有span,向PageCache申请
	PageCache::GetInstance()->_pageMtx.lock();
	span = PageCache::GetInstance()->NewSpan(SizeClass::NumMovePage(size));
	PageCache::GetInstance()->_pageMtx.unlock();
	span->_isUse = true;
	span->_objSize = size;

	// 对获取span进行切分,不需要加锁,因为这会其他线程访问不到这个span
	// 计算span的大块内存的起始地址和大块内存的大小(字节数)
	char* start = (char*)(span->_pageId << PAGE_SHIFT);
	size_t bytes = span->_n << PAGE_SHIFT;
	char* end = start + bytes;

	//3.对span管理的大块内存进行切分,尾插链接到_freeList
	span->_freeList = start;
	start += size;
	void* tail = span->_freeList;
	int i = 1;
	while (start < end)
	{
		++i;
		NextObj(tail) = start;
		tail = NextObj(tail); // tail = start;
		start += size;
	}
	NextObj(tail) = nullptr;

	//4.将申请到的新的span头插到list,访问共享资源,需要加锁
	list._mtx.lock();
	list.PushFront(span);

	return span;
}

// 从中心缓存获取一定数量的对象给thread cache
size_t CentralCache::FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size)
{
	size_t index = SizeClass::Index(size);
	_spanLists[index]._mtx.lock();

	Span* span = GetOneSpan(_spanLists[index], size);
	assert(span && span->_freeList);

	//1.span中对象充足,则取batchNum个
	//2.span不足,则end指向最后一个
	start = end = span->_freeList;
	size_t actualNum = 1;
	for (size_t i = 0; i < batchNum - 1; ++i)
	{
		if (NextObj(end) == nullptr)break;
		++actualNum;
		end = NextObj(end);
	}
	span->_freeList = NextObj(end);
	span->_useCount += actualNum;
	NextObj(end) = nullptr;

	_spanLists[index]._mtx.unlock();

	return actualNum;
}

void CentralCache::ReleaseListToSpans(void* start, size_t size)
{
	size_t index = SizeClass::Index(size);
	SpanList& sList = _spanLists[index];
	sList._mtx.lock();
	//_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
		if (span->_useCount == 0)
		{
			sList.Erase(span);
			span->_freeList = nullptr;
			span->_next = nullptr;
			span->_prev = nullptr;

			//将span还给PageCache
			sList._mtx.unlock();

			PageCache::GetInstance()->_pageMtx.lock();
			PageCache::GetInstance()->ReleaseSpanToPageCache(span);
			PageCache::GetInstance()->_pageMtx.unlock();

			sList._mtx.lock();
		}

		start = next;
	}

	sList._mtx.unlock();
}
#pragma once
#include "Common.h"

// 单例模式
class CentralCache
{
public:
	static CentralCache* GetInstance()
	{
		return &_sInst;
	}

	// 获取一个非空的span
	Span* GetOneSpan(SpanList& list, size_t byte_size);
	// 从中心缓存获取一定数量的对象给thread cache
	size_t FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size);
	// 将一定数量的对象释放到span跨度
	void ReleaseListToSpans(void* start, size_t byte_size);
private:
	SpanList _spanLists[NFREELIST];

private:
	CentralCache(){}
	CentralCache(const CentralCache&) = delete;
	static CentralCache _sInst;
};


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

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

相关文章

C语言作业笔记

1. 要找俩个数使其相加等于一个数&#xff0c;那么俩个数从头尾出发&#xff0c;先动一边&#xff0c;假设是尾先动&#xff0c;一开始俩个数相加大于sum&#xff08;小于的话就动头&#xff09;&#xff0c;那么总有一时刻俩数相加小于sum&#xff0c;则就在那一刻停下来&…

MySQL高可用(MHA高可用)

什么是 MHA MHA&#xff08;MasterHigh Availability&#xff09;是一套优秀的MySQL高可用环境下故障切换和主从复制的软件。 MHA 的出现就是解决MySQL 单点的问题。 MySQL故障切换过程中&#xff0c;MHA能做到0-30秒内自动完成故障切换操作。 MHA能在故障切换的过程中最大…

机器学习与模式识别_清华大学出版社

contents 前言第1章 绪论1.1 引言1.2 基本术语1.3 假设空间1.4 归纳偏好1.5 发展历程1.6 应用现状 第2章 模型评估与选择2.1 经验误差与过拟合2.2 评估方法2.3 性能度量2.3.1 回归任务2.3.2 分类任务 2.4 比较检验2.5 偏差与方差2.5.1 偏差-方差分解2.5.2 偏差-方差窘境 第3章 …

In Ictu Oculi: Exposing AI Created Fake Videos by Detecting Eye Blinking

文章目录 In Ictu Oculi: Exposing AI Created Fake Videos by Detecting Eye Blinking背景关键点内容预处理Long-Term Recurrent CNNsLSTM-RNN模型训练实验data启示In Ictu Oculi: Exposing AI Created Fake Videos by Detecting Eye Blinking 会议:2018 IEEE International…

用Vue3和Rough.js绘制一个交互式3D图

本文由ScriptEcho平台提供技术支持 项目地址&#xff1a;传送门 基于Rough.js和GSAP创建交互式SVG图形卡片 应用场景 本代码适用于需要创建动态交互式SVG图形卡片的场景&#xff0c;例如网页设计、数据可视化和交互式艺术作品。 基本功能 该代码利用Rough.js和GSAP库&…

数据分析入门指南:从基础概念到实际应用(一)

随着数字化时代的来临&#xff0c;数据分析在企业的日常运营中扮演着越来越重要的角色。从感知型企业到数据应用系统的演进&#xff0c;数据驱动的业务、智能优化的业务以及数智化转型成为了企业追求的目标。在这一过程中&#xff0c;数据分析不仅是技术的运用&#xff0c;更是…

在 PostgreSQL 中强制执行连接顺序#postgresql认证

让我们首先创建一些表&#xff1a; PgSQL plan# SELECT CREATE TABLE x || id || (id int) FROM generate_series(1, 5) AS id;?column? --------------------------CREATE TABLE x1 (id int)CREATE TABLE x2 (id int)CREATE TABLE x3 (id int)CREATE TABLE…

Richtek立锜科技车规级器件选型

芯片按照应用场景&#xff0c;通常可以分为消费级、工业级、车规级和军工级四个等级&#xff0c;其要求依次为军工>车规>工业>消费。 所谓“车规级元器件”--即通过AEC-Q认证 汽车不同于消费级产品&#xff0c;会运行在户外、高温、高寒、潮湿等苛刻的环境&#xff0c…

首获IF就高达13分!各刊潜力无限——爱思唯尔2024首获IF期刊大盘点!

【SciencePub学术】爱思唯尔&#xff08;Elsevier&#xff09;是一家全球知名的国际性学术出版公司&#xff0c;总部位于荷兰阿姆斯特丹。该公司主要出版科学、技术和医学领域的学术期刊和书籍&#xff0c;涵盖了广泛的学科领域&#xff0c;如生命科学、物理科学、社会科学等。…

Python面向对象编程中的继承及其应用

目录 1. 继承的基本概念 2. 继承的语法 3. 继承的应用场景 4. 使用示例&#xff1a;汽车销售系统 5. 总结 继承是面向对象编程中的一个重要概念&#xff0c;它允许我们根据已有类创建新类&#xff0c;并继承已有类的属性和方法。在本文中&#xff0c;我们将学习Python中的…

c++习题09-分离整数的各个数

目录 一&#xff0c;题目 二&#xff0c;思路 三&#xff0c;代码 一&#xff0c;题目 二&#xff0c;思路 一开始我想到的是将简单容易输出的1000以内的数先进行相应的运算&#xff0c;再输出之后再对1000以上的数字进行判断&#xff08;主要还是想先将很大的数变小&#x…

Java数字化产科管理系统源码,多家医院应用案例,可直接上项目

Java数字化产科管理系统源码&#xff0c;多家医院应用案例&#xff0c;可直接上项目 数字化产科管理平台系统是什么&#xff1f;该系统由产品包涵门诊管理、住院管理、统计管理、移动服务四大模块&#xff0c;新版本涵盖围产全过程&#xff0c;从建档、首检、复检、住院(待产、…

防爆智能手机如何解决危险环境下通信难题?

在化工厂、石油行业、矿山等危险环境中&#xff0c;通信安全一直是难题。传统手机因不具备防爆功能&#xff0c;可能引发火花、爆炸等安全风险&#xff0c;让工作人员在关键时刻难以及时沟通。但如今&#xff0c;防爆智能手机的出现彻底改变了这一现状&#xff01; 安全通信&am…

【WebGIS干货分享】Webgis 面试题-浙江中海达

1、Cesium 中有几种拾取坐标的方式&#xff0c;分别介绍 Cesium 是一个用于创建 3D 地球和地理空间应用的 JavaScript 库。在 Cesium 中&#xff0c;你可以使用不同的方式来拾取坐标&#xff0c;以便与地球或地图上的对象进行交 互。以下是 Cesium 中几种常见的拾取坐标的方式…

静态路由的配置

5.3静态路由 静态路由由网络管理员手动配置&#xff0c;配置方便&#xff0c;对系统要求低&#xff0c;适用于拓扑结构简单并且稳定的小型网络。缺点是不能自动适应网络拓扑的变化&#xff0c;需要人工干预。 5.3.1静态路由实验 1、实验需求 ① 掌握路由表的概念&#xff1…

服务器上VMWare Workstation虚拟机声卡支持

问题&#xff1a;联想服务器没有声卡&#xff0c;Windows 服务器安装了VMWare Workstation&#xff0c;里面的Windows 11虚拟机&#xff0c;我远程桌面上来&#xff0c;没有声卡&#xff0c;但是我想做 声音方面的测试就没办法。 解决办法&#xff1a; 服务器主机上安装虚拟机…

【3分钟准备前端面试】vue3

目录 Vue3比vue2有什么优势vue3升级了哪些重要功能生命周期变化Options APIComposition APIreftoRef和toRefstoReftoRefsHooks (代码复用)Vue3 script setupsetupdefineProps和defineEmitsdefineExposeVue3比vue2有什么优势 性能更好体积更小更好的TS支持更好的代码组织更好的逻…

CAAC无人机执照:视距内驾驶员与超视距驾驶员区别详解

CAAC无人机执照中的视距内驾驶员与超视距驾驶员在多个方面存在显著的区别。以下是详细的对比和解释&#xff1a; 1. 定义与操作范围&#xff1a; - 视距内驾驶员&#xff08;驾驶员证&#xff09;&#xff1a;操作无人机时&#xff0c;无人机必须在操控员的视线范围内&#xff…

什么是JavaScript中的箭头函数(arrow functions)?

聚沙成塔每天进步一点点 本文回顾 ⭐ 专栏简介什么是JavaScript中的箭头函数&#xff08;arrow functions&#xff09;&#xff1f;1. 引言2. 箭头函数的语法2.1 基本语法2.2 示例 3. 箭头函数的特点3.1 简洁的语法3.2 没有this绑定3.3 不能用作构造函数3.4 没有arguments对象3…

二百四十二、Hive——Hive的动态分区表出现day=__HIVE_DEFAULT_PARTITION__分区

一、目的 Hive的DWD层动态分区表的分区出现day__HIVE_DEFAULT_PARTITION__&#xff0c;有点懵&#xff0c;而且表中数据的day字段也显示__HIVE_DEFAULT_PARTITION__ 1、DWD层动态分区表的分区 __HIVE_DEFAULT_PARTITION__ 2、DWD层分区字段day数据 __HIVE_DEFAULT_PARTITION…