PoolThreadCache 类的结构和源码实现

PoolThreadCache 在 Netty 的内存池中扮演着线程本地缓存的角色。它的主要目的是减少线程在分配内存时对全局 PoolArena 的竞争,通过缓存一部分最近释放的内存块,使得同一线程后续申请相同规格的内存时能够快速获取,从而提高分配效率。

下面我们详细分析其源码:

主要成员变量

// ... existing code ...
final class PoolThreadCache {private static final InternalLogger logger = InternalLoggerFactory.getInstance(PoolThreadCache.class);private static final int INTEGER_SIZE_MINUS_ONE = Integer.SIZE - 1;final PoolArena<byte[]> heapArena; // 关联的堆内存Arenafinal PoolArena<ByteBuffer> directArena; // 关联的直接内存Arena// 针对不同大小规格 (Small/Normal) 和类型 (Heap/Direct) 的内存区域缓存// Small类型的内存通常来自PoolSubpageprivate final MemoryRegionCache<byte[]>[] smallSubPageHeapCaches;private final MemoryRegionCache<ByteBuffer>[] smallSubPageDirectCaches;// Normal类型的内存通常直接来自PoolChunk的Pageprivate final MemoryRegionCache<byte[]>[] normalHeapCaches;private final MemoryRegionCache<ByteBuffer>[] normalDirectCaches;// 分配次数阈值,达到此阈值时触发trim操作,清理缓存private final int freeSweepAllocationThreshold;// 标记缓存是否已被释放,防止重复释放private final AtomicBoolean freed = new AtomicBoolean();@SuppressWarnings("unused") // Field is only here for the finalizer.// 用于在对象被GC回收前,通过finalizer机制尝试释放缓存中的资源private final FreeOnFinalize freeOnFinalize;// 当前线程缓存的分配次数,用于配合freeSweepAllocationThresholdprivate int allocations;// TODO: Test if adding padding helps under contention//private long pad0, pad1, pad2, pad3, pad4, pad5, pad6, pad7;// ... existing code ...
}
  • heapArena 和 directArena: 分别指向该线程缓存关联的堆内存池和直接内存池。线程在缓存未命中时,会向这两个 Arena 申请内存。
  • smallSubPageHeapCachessmallSubPageDirectCachesnormalHeapCachesnormalDirectCaches: 这四个数组是核心的缓存存储结构。它们是 MemoryRegionCache 类型的数组,MemoryRegionCache 内部维护了一个队列,用于存储缓存的内存块信息。
    • smallSubPage...Caches: 用于缓存 "Small" 类型的内存块。这类内存块通常小于一个 Page,由 PoolSubpage 管理。数组的索引对应不同的 elemSize
    • normal...Caches: 用于缓存 "Normal" 类型的内存块。这类内存块通常大于等于一个 Page,直接从 PoolChunk 中分配。数组的索引对应不同的规格大小。
  • freeSweepAllocationThreshold: 这是一个重要的参数。当 PoolThreadCache 的 allocations 计数达到这个阈值时,会触发 trim() 方法,尝试回收一部分缓存的内存,以避免缓存过多导致内存浪费。
  • freed: 一个原子布尔值,确保 free() 方法只被执行一次,防止资源被多次释放。
  • freeOnFinalize: 一个内部类实例,如果启用了 useFinalizer,当 PoolThreadCache 对象被垃圾回收时,其 finalize 方法会被调用,进而调用 PoolThreadCache.free(true) 来释放缓存的资源。这是一种兜底机制。
  • allocations: 记录从该线程缓存成功分配出去的次数。

构造函数 PoolThreadCache(...)

PoolThreadCache.java

// ... existing code ...PoolThreadCache(PoolArena<byte[]> heapArena, PoolArena<ByteBuffer> directArena,int smallCacheSize, int normalCacheSize, int maxCachedBufferCapacity,int freeSweepAllocationThreshold, boolean useFinalizer) {checkPositiveOrZero(maxCachedBufferCapacity, "maxCachedBufferCapacity");this.freeSweepAllocationThreshold = freeSweepAllocationThreshold;this.heapArena = heapArena;this.directArena = directArena;if (directArena != null) {// 创建直接内存的Small和Normal类型的缓存数组smallSubPageDirectCaches = createSubPageCaches(smallCacheSize, directArena.sizeClass.nSubpages);normalDirectCaches = createNormalCaches(normalCacheSize, maxCachedBufferCapacity, directArena);// 增加Arena中线程缓存的计数directArena.numThreadCaches.getAndIncrement();} else {// No directArea is configured so just null out all cachessmallSubPageDirectCaches = null;normalDirectCaches = null;}if (heapArena != null) {// 创建堆内存的Small和Normal类型的缓存数组smallSubPageHeapCaches = createSubPageCaches(smallCacheSize, heapArena.sizeClass.nSubpages);normalHeapCaches = createNormalCaches(normalCacheSize, maxCachedBufferCapacity, heapArena);// 增加Arena中线程缓存的计数heapArena.numThreadCaches.getAndIncrement();} else {// No heapArea is configured so just null out all cachessmallSubPageHeapCaches = null;normalHeapCaches = null;}// Only check if there are caches in use.// 如果配置了任何缓存,则freeSweepAllocationThreshold必须大于0if ((smallSubPageDirectCaches != null || normalDirectCaches != null|| smallSubPageHeapCaches != null || normalHeapCaches != null)&& freeSweepAllocationThreshold < 1) {throw new IllegalArgumentException("freeSweepAllocationThreshold: "+ freeSweepAllocationThreshold + " (expected: > 0)");}// 根据useFinalizer参数决定是否创建FreeOnFinalize实例freeOnFinalize = useFinalizer ? new FreeOnFinalize(this) : null;}// ... existing code ...

构造函数的主要工作是初始化各个成员变量,特别是根据传入的参数创建不同类型的 MemoryRegionCache 数组。

  • smallCacheSizenormalCacheSize: 分别定义了 Small 类型和 Normal 类型缓存区域中 MemoryRegionCache 队列的大小。
  • maxCachedBufferCapacity: 定义了可以被缓存的 Buffer 的最大容量。超过这个容量的 Buffer 不会被缓存。
  • directArena.sizeClass.nSubpages: 这个值决定了 smallSubPageDirectCaches 数组的大小,即支持多少种不同规格的 Small 类型直接内存缓存。
  • directArena.numThreadCaches.getAndIncrement(): 每当一个 PoolThreadCache 关联到一个 PoolArena 时,会增加 PoolArena 内部的线程缓存计数器。

createSubPageCaches

PoolThreadCache.java

// ... existing code ...
private static <T> MemoryRegionCache<T>[] createSubPageCaches(int cacheSize, int numCaches) {if (cacheSize > 0 && numCaches > 0) {@SuppressWarnings("unchecked")MemoryRegionCache<T>[] cache = new MemoryRegionCache[numCaches];for (int i = 0; i < cache.length; i++) {// TODO: maybe use cacheSize / cache.length// 显式类型实参 T 可被替换为 <>cache[i] = new SubPageMemoryRegionCache<>(cacheSize);}return cache;} else {return null;}
}

这个方法用于创建 SubPageMemoryRegionCache 数组。

numCaches 通常是 arena.sizeClass.nSubpages,表示支持的 Small 类型规格数量。每个 SubPageMemoryRegionCache 实例的内部队列大小由 cacheSize 决定。

createNormalCaches

// ... existing code ...
@SuppressWarnings("unchecked")
private static <T> MemoryRegionCache<T>[] createNormalCaches(int cacheSize, int maxCachedBufferCapacity, PoolArena<T> area) {if (cacheSize > 0 && maxCachedBufferCapacity > 0) {int max = Math.min(area.sizeClass.chunkSize, maxCachedBufferCapacity);// Create as many normal caches as we support based on how many sizeIdx we have and what the upper// bound is that we want to cache in general.List<MemoryRegionCache<T>> cache = new ArrayList<MemoryRegionCache<T>>() ;// 从nSubpages开始,因为之前的sizeIdx是为Small类型保留的// area.sizeClass.sizeIdx2size(idx) <= max 确保只为不超过maxCachedBufferCapacity的规格创建缓存for (int idx = area.sizeClass.nSubpages; idx < area.sizeClass.nSizes &&area.sizeClass.sizeIdx2size(idx) <= max; idx++) {// 显式类型实参 T 可被替换为 <>cache.add(new NormalMemoryRegionCache<>(cacheSize));}return cache.toArray(new MemoryRegionCache[0]);} else {return null;}
}

这个方法用于创建 NormalMemoryRegionCache 数组。

它会遍历 PoolArena 的 SizeClasses 中定义的 Normal 类型的规格,但只为那些大小不超过 maxCachedBufferCapacity (且不超过 chunkSize) 的规格创建缓存。

内存分配方法

  • allocateSmall(PoolArena<?> area, PooledByteBuf<?> buf, int reqCapacity, int sizeIdx)

  • allocateNormal(PoolArena<?> area, PooledByteBuf<?> buf, int reqCapacity, int sizeIdx) 这两个方法分别用于分配 Small 和 Normal 类型的内存。它们首先通过 cacheForSmall 或 cacheForNormal 找到对应的 MemoryRegionCache,然后调用通用的 allocate 方法。

  • allocate(MemoryRegionCache<?> cache, PooledByteBuf buf, int reqCapacity)

    PoolThreadCache.java

    // ... existing code ...
    @SuppressWarnings({ "unchecked", "rawtypes" })
    private boolean allocate(MemoryRegionCache<?> cache, PooledByteBuf buf, int reqCapacity) {if (cache == null) {// no cache found so just return false herereturn false;}// 尝试从指定的MemoryRegionCache分配boolean allocated = cache.allocate(buf, reqCapacity, this);// 如果分配成功,并且总分配次数达到阈值if (++ allocations >= freeSweepAllocationThreshold) {allocations = 0; // 重置计数器trim(); // 执行trim操作,清理缓存}return allocated;
    }
    

这是实际执行从缓存分配的逻辑:

1. 如果找不到对应的 MemoryRegionCache (例如,该规格的缓存未启用或请求的 sizeIdx 超出范围),则返回 false

2. 调用 cache.allocate(buf, reqCapacity, this) 尝试从该 MemoryRegionCache 的队列中取出一个缓存的 Entry 并用它初始化 buf

3. 如果分配成功 (allocated 为 true),则 allocations 计数器加1。

4. 检查 allocations 是否达到 freeSweepAllocationThreshold。如果是,则将 allocations 重置为0,并调用 trim() 方法来清理所有缓存区域中不活跃的条目。

添加到缓存方法 add(...)

PoolThreadCache.java

// ... existing code ...@SuppressWarnings({ "unchecked", "rawtypes" })boolean add(PoolArena<?> area, PoolChunk chunk, ByteBuffer nioBuffer,long handle, int normCapacity, SizeClass sizeClass) {// 根据normCapacity计算sizeIdxint sizeIdx = area.sizeClass.size2SizeIdx(normCapacity);// 根据area类型(Heap/Direct)、sizeIdx和sizeClass(Small/Normal)获取对应的MemoryRegionCacheMemoryRegionCache<?> cache = cache(area, sizeIdx, sizeClass);if (cache == null) {return false; // 找不到合适的缓存区域}if (freed.get()) { // 如果缓存已被标记为释放,则不再添加return false;}// 调用MemoryRegionCache的add方法return cache.add(chunk, nioBuffer, handle, normCapacity);}// ... existing code ...

当一个 PooledByteBuf 被释放时,如果满足一定条件(例如,它的大小适合缓存,且其来源的 PoolArena 允许缓存),PoolArena 会尝试调用此方法将其对应的内存块信息(chunkhandlenormCapacity 等)添加到当前线程的 PoolThreadCache 中。

  1. 计算 sizeIdx
  2. 通过 cache(area, sizeIdx, sizeClass) 方法定位到具体的 MemoryRegionCache
  3. 如果缓存已被释放 (freed.get() 为 true),则不添加。
  4. 调用 MemoryRegionCache.add(...) 将内存块信息封装成 Entry 对象并尝试放入其内部队列。

缓存检索方法

  • cache(PoolArena<?> area, int sizeIdx, SizeClass sizeClass): 根据 sizeClass (Normal 或 Small) 调用 cacheForNormal 或 cacheForSmall
  • cacheForSmall(PoolArena<?> area, int sizeIdx): 判断 area 是堆内存还是直接内存,然后从 smallSubPageHeapCaches 或 smallSubPageDirectCaches 中获取缓存。
  • cacheForNormal(PoolArena<?> area, int sizeIdx): 类似 cacheForSmall,但操作的是 normalHeapCaches 和 normalDirectCaches。注意这里 idx = sizeIdx - area.sizeClass.nSubpages,因为 sizeIdx 是全局的,而 Normal 类型的缓存在数组中的索引需要减去 Small 类型的规格数量。
  • cache(MemoryRegionCache<T>[] cache, int sizeIdx): 简单的数组访问,并进行边界检查。

这些方法共同构成了从缓存数组中定位特定 MemoryRegionCache 的逻辑。

释放资源方法 free(boolean finalizer)

PoolThreadCache.java

// ... existing code ...void free(boolean finalizer) {// As free() may be called either by the finalizer or by FastThreadLocal.onRemoval(...) we need to ensure// we only call this one time.// 使用AtomicBoolean确保free操作只执行一次if (freed.compareAndSet(false, true)) {if (freeOnFinalize != null) {// Help GC: this can race with a finalizer thread, but will be null out regardlessfreeOnFinalize.cache = null; // 解除FreeOnFinalize对PoolThreadCache的引用}// 依次释放所有类型的缓存区域int numFreed = free(smallSubPageDirectCaches, finalizer) +free(normalDirectCaches, finalizer) +free(smallSubPageHeapCaches, finalizer) +free(normalHeapCaches, finalizer);if (numFreed > 0 && logger.isDebugEnabled()) {logger.debug("Freed {} thread-local buffer(s) from thread: {}", numFreed,Thread.currentThread().getName());}// 递减Arena中的线程缓存计数if (directArena != null) {directArena.numThreadCaches.getAndDecrement();}if (heapArena != null) {heapArena.numThreadCaches.getAndDecrement();}}}private static int free(MemoryRegionCache<?>[] caches, boolean finalizer) {if (caches == null) {return 0;}int numFreed = 0;for (MemoryRegionCache<?> c: caches) {numFreed += free(c, finalizer); // 遍历释放数组中的每个MemoryRegionCache}return numFreed;}private static int free(MemoryRegionCache<?> cache, boolean finalizer) {if (cache == null) {return 0;}return cache.free(finalizer); // 调用MemoryRegionCache的free方法}// ... existing code ...

当线程结束或者 PooledByteBufAllocator 关闭时,会调用此方法来释放 PoolThreadCache 中缓存的所有内存块。

  • freed.compareAndSet(false, true) 保证了此方法体内的逻辑只执行一次。
  • finalizer 参数指示这次释放是否由 finalizer 机制触发。如果是,MemoryRegionCache.freeEntry 的行为会有所不同(主要是为了避免在 finalizer 线程中执行可能导致死锁或复杂状态的操作)。
  • 它会遍历所有四种缓存数组,并调用每个 MemoryRegionCache 实例的 free(finalizer) 方法,该方法会清空其内部队列并将所有缓存的 Entry 代表的内存块归还给 PoolArena
  • 最后,递减关联 PoolArena 中的 numThreadCaches 计数。

整理缓存方法 trim()

PoolThreadCache.java

// ... existing code ...void trim() {trim(smallSubPageDirectCaches);trim(normalDirectCaches);trim(smallSubPageHeapCaches);trim(normalHeapCaches);}private static void trim(MemoryRegionCache<?>[] caches) {if (caches == null) {return;}for (MemoryRegionCache<?> c: caches) {trim(c);}}private static void trim(MemoryRegionCache<?> cache) {if (cache == null) {return;}cache.trim(); // 调用MemoryRegionCache的trim方法}// ... existing code ...

当 PoolThreadCache 的 allocations 达到 freeSweepAllocationThreshold 时被调用。它会遍历所有缓存数组,并调用每个 MemoryRegionCache 实例的 trim() 方法。MemoryRegionCache.trim() 会根据其自身的分配情况和队列大小,决定是否释放一部分缓存的 Entry

SubPageMemoryRegionCache<T> 和 NormalMemoryRegionCache<T> 

这两个类都继承自抽象的 MemoryRegionCache<T>。它们的主要区别在于构造时传入的 SizeClass (Small 或 Normal) 以及它们如何实现 initBuf 方法:

  • SubPageMemoryRegionCache.initBuf(...) 调用 chunk.initBufWithSubpage(...),用于从 PoolSubpage 初始化 PooledByteBuf
  • NormalMemoryRegionCache.initBuf(...) 调用 chunk.initBuf(...),用于从 PoolChunk 的 Page 初始化 PooledByteBuf

MemoryRegionCache<T> 

abstract static class

这是线程缓存的核心数据结构之一,代表特定规格内存的缓存区域。

  • size: 缓存队列的容量,是2的幂次方。
  • queuePlatformDependent.newFixedMpscUnpaddedQueue(this.size) 创建的一个多生产者单消费者队列 (MPSC),用于存储缓存的 Entry<T> 对象。由于 PoolThreadCache 是线程本地的,这里的“多生产者”实际上是指其他线程释放内存并尝试将内存块添加到这个线程的缓存中(虽然 Netty 的设计主要是当前线程释放的内存回到当前线程的缓存),而“单消费者”就是当前线程自己从缓存中分配内存。
  • sizeClass: 标记这个缓存区域是用于 Small 还是 Normal 类型的内存。
  • allocations: 记录从这个特定 MemoryRegionCache 分配出去的次数,用于其自身的 trim() 逻辑。
  • add(PoolChunk<T> chunk, ...): 创建一个新的 Entry 对象(通过 RECYCLER 获取),设置好 chunkhandle 等信息,然后尝试将其加入 queue。如果队列已满 (offer 返回 false),则立即回收这个 Entry 对象。
  • allocate(PooledByteBuf<T> buf, ...): 从 queue 中取出一个 Entry (poll)。如果队列为空,返回 false。否则,调用抽象方法 initBuf 用取出的 Entry 中的信息来初始化 buf,然后回收 Entry 对象,并增加 allocations 计数。
  • free(int max, boolean finalizer): 从队列中移除最多 max 个 Entry,并对每个 Entry 调用 freeEntry
  • trim(): 计算当前队列中可以释放的 Entry 数量(基于 size - allocations),然后调用 free(free, false) 来释放它们。allocations 在这里代表了近期从该缓存区域成功分配的次数,如果这个数字远小于队列的容量 size,说明缓存利用率不高,可以进行清理。
  • freeEntry(Entry entry, boolean finalizer): 这是将缓存的内存块真正归还给 PoolArena 的地方。它获取 Entry 中的 chunkhandlenioBuffernormCapacity。如果不是由 finalizer 触发,它会先回收 Entry 对象本身,然后调用 chunk.arena.free(chunk, entry.nioBuffer, handle, normCapacity, this) 将内存块归还给 Arena。如果是 finalizer 触发,则只归还内存块,不立即回收 Entry (避免在 finalizer 中操作 Recycler 可能引发的问题)。

Entry<T> 

一个简单的 POJO,用于封装缓存的内存块信息 (PoolChunkByteBuffer nioBufferlong handleint normCapacity)。它通过 Netty 的 Recycler 进行对象池管理,以减少 Entry 对象自身的创建和销毁开销。

// ... existing code ...private static final ObjectPool<Entry<?>> RECYCLER = ObjectPool.newPool(new ObjectCreator<Entry<?>>() {@SuppressWarnings("unchecked")@Overridepublic Entry<?> newObject(Handle<Entry<?>> handle) {return new Entry(handle);}});static final class Entry<T> {final EnhancedHandle<Entry<?>> recyclerHandle; // Recycler的句柄PoolChunk<T> chunk;ByteBuffer nioBuffer; // 缓存的NIO ByteBuffer,主要用于Direct Bufferlong handle = -1;     // 内存块在PoolChunk中的句柄int normCapacity;     // 规格化容量Entry(Handle<Entry<?>> recyclerHandle) {this.recyclerHandle = (EnhancedHandle<Entry<?>>) recyclerHandle;}void recycle() {chunk = null;nioBuffer = null;handle = -1;recyclerHandle.recycle(this);}// "Unguarded" version of recycle() that must only be used when we are sure that the Entry is not double-recycled.// This is the case when we obtained the Entry from the queue and add it to the cache again.void unguardedRecycle() {chunk = null;nioBuffer = null;handle = -1;recyclerHandle.unguardedRecycle(this);}}@SuppressWarnings("rawtypes")private static Entry newEntry(PoolChunk<?> chunk, ByteBuffer nioBuffer, long handle, int normCapacity) {Entry entry = RECYCLER.get(); // 从Recycler获取Entry对象entry.chunk = chunk;entry.nioBuffer = nioBuffer;entry.handle = handle;entry.normCapacity = normCapacity;return entry;}
// ... existing code ...

FreeOnFinalize 

一个简单的包装类,其 finalize() 方法会调用 PoolThreadCache.free(true)。这是为了在 PoolThreadCache 对象本身被 GC 回收时,能够尝试释放其占用的缓存资源,作为一种安全保障。

完全移动到 Java9+ 后, 会使用  java.lang.ref.Cleaner

// ... existing code ...
// Used to free the cache via a finalizer. This is just a best effort and should only be used if the
// ThreadLocal is not removed via FastThreadLocal.onRemoval(...) as this is the preferred way to free the cache.
private static final class FreeOnFinalize {private PoolThreadCache cache;FreeOnFinalize(PoolThreadCache cache) {this.cache = cache;}@Overrideprotected void finalize() throws Throwable {try {super.finalize();} finally {PoolThreadCache cache = this.cache;// this can race with a non-finalizer thread calling free: regardless who wins, the cache will be// null outthis.cache = null;if (cache != null) {// We must only call free if the cache was not null before, which means it was not freed before// by an explicit call to PoolThreadCache.free().//// We must use true as parameter which indicates that we were called from a finalizer.cache.free(true);}}}
}
}

总结

PoolThreadCache 通过精心设计的缓存结构和回收策略,有效地提升了 Netty 内存分配的性能。它利用线程本地性避免了锁竞争,并通过 MemoryRegionCache 对不同规格的内存进行细粒度管理。

freeSweepAllocationThreshold 和 trim 机制确保了缓存在提供性能优势的同时,不会无限制地消耗内存。内部类如 MemoryRegionCache 和 Entry 的设计,以及 Recycler 的使用,都体现了 Netty 对性能和资源管理的极致追求。

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

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

相关文章

Linux中的阻塞信号与信号原理

在Linux操作系统中&#xff0c;信号&#xff08;Signal&#xff09;是进程间通信和进程控制的核心机制之一。信号是一种异步通知机制&#xff0c;可以向进程发送异步事件通知&#xff0c;以便进程能够处理系统级别的事件。本文将详细探讨Linux中的信号原理&#xff0c;重点讲解…

QT学习教程(三十五)

事件处理&#xff08;- Event Processingn&#xff09; 事件是视窗系统或者Qt 本身在各种不同的情况下产生的。当用户点击或者释放鼠标&#xff0c;键盘时&#xff0c;一个鼠标事件或者键盘事件就产生了。当窗口第一次显示时&#xff0c;一个绘制事件会产生告诉新可见的窗口绘…

【Dify 案例】【MCP实战】【三】【超级美食家】

接上次的超级助理,我们这一期给出一个超级美食家 首先:我的MCP要申请一个key ` 我们来看看这个MCP服务怎么使用呢。`https://modelscope.cn/mcp/servers/@worryzyy/howtocook-mcp插件里面需要配置 {"mcpServers":{"amap-amap-sse":{"url":&qu…

4.文件管理(文本、日志、Excel表)

目录 1.文本 2.日志 3.Excel表 1.文本 using System.Text;namespace (自己创建的一个类) {/// <summary>/// 配置文件*.ini读写器。/// </summary>public class IniFile{[System.Runtime.InteropServices.DllImport("kernel32")]private static ex…

Java 包装类详解

什么是包装类 Java包装类&#xff08;Wrapper Classes&#xff09;是将8种基本数据类型封装成对象的类&#xff0c;位于java.lang包中。每个基本数据类型都有对应的包装类&#xff1a; byte → Byteshort → Shortint → Integerlong → Longfloat → Floatdouble → Doublec…

阿里云ACP认证-数据仓库

数据仓库 Kappa架构&#xff1a;将实时和离线代码统一&#xff08;优化lambda架构&#xff09;&#xff0c;但是不好修正数据&#xff0c;开发周期长&#xff0c;成本浪费&#xff0c;对于历史数据的高吞吐量力不从心 原一代数据仓库&#xff1a; 离线&#xff1a;hivemaxcom…

WebRTC(五):TURN协议

TURN&#xff08;Traversal Using Relays around NAT&#xff09;协议是一个网络协议&#xff0c;旨在解决 NAT&#xff08;网络地址转换&#xff09;和防火墙 环境下的 UDP/TCP通信问题。它通常与 STUN 和 ICE 协议一起使用&#xff0c;广泛应用于 WebRTC、SIP 和视频会议等实…

Python 的内置函数 hasattr

Python 内建函数列表 > Python 的内置函数 hasattr Python 的内置函数 hasattr() 用于检查一个对象是否具有指定的属性或方法。该函数的语法为&#xff1a; hasattr(object, name)参数说明&#xff1a; object&#xff1a;要检查的对象&#xff0c;可以是任何 Python 对象…

docker使用技巧之把扩展卷命名变成有意义

背景 之前使用别人的镜像之后&#xff0c;启动docker后发出现了一堆看不懂名称的扩展卷 eg&#xff1a;集群查看 扩展卷查看 这个时候如果有很多集群需要清理扩展卷就很麻烦&#xff0c;不知道是哪个集群的 操作步骤 可以实现的分析&#xff1a;这个扩展卷的信息应该是和…

《博物通书》《博物新编》与满清历史篡改

《博物新编》作为近代西方科技输入中国的首部著作&#xff0c;其问世犹如一颗投入平静湖面的巨石&#xff0c;在 19 世纪中期的中国激起层层涟漪&#xff0c;对中国近代科学发展产生了多维度、深层次的影响。它不仅是知识传播的载体&#xff0c;更是推动中国科学从传统走向近代…

【入门】【例18.1】 睡眠

| 时间限制&#xff1a;C/C 1000MS&#xff0c;其他语言 2000MS 内存限制&#xff1a;C/C 64MB&#xff0c;其他语言 128MB 难度&#xff1a;中等 分数&#xff1a;100 OI排行榜得分&#xff1a;12(0.1分数2难度) 出题人&#xff1a;root | 描述 一个人只有每天睡眠时间到达 8…

DAY 38 Dataset和Dataloader类

知识点回顾&#xff1a; Dataset类的__getitem__和__len__方法&#xff08;本质是python的特殊方法&#xff09;Dataloader类minist手写数据集的了解 作业&#xff1a;了解下cifar数据集&#xff0c;尝试获取其中一张图片 import torch import torch.nn as nn import torch.o…

【Kubernetes】以LOL的视角打开K8s

前言 对于大部分后端程序员乃至于非后端程序员来说&#xff0c;在当前的云原生时代&#xff0c;Kubernetes&#xff08;后称K8s&#xff09;都是绕不开的一项技术&#xff1b;同时&#xff0c;对于这个时代的程序员来说&#xff0c;“英雄联盟”&#xff08;后称LOL&#xff0…

UE5 游戏模板 —— FirstShootGame

UE5 游戏模板 —— FirstShootGame 前言一、GameMode二、组件1.ShooterPickUpComponent单播多播 2.ShooterWeaponComponent附着武器开火 3.小结4.ShooterProjectile初始化碰撞受击检测 三、Character初始化输入移动 总结 前言 有了前两个俯视角游戏的基础让我们来看看相对复杂…

国家级与省级(不含港澳台)标准地图服务网站汇总

在先前的文章中&#xff0c;介绍了部分省级的标准地图服务网站可以下载各个区县近几年、不同要素的标准地图&#xff08;链接&#xff1a;国家与省市县 标准地图服务网站 审图号地图下载&#xff09;&#xff0c;但是当时只汇总了部分省级的标准地图服务网站。 这两天看到了一个…

前端开发面试题总结-vue3框架篇(一)

文章目录 Vue3高频问答一、vue2/vue3中常用的构建工具和脚手架分别是什么? 有什么区别?二、请说一说vue2和vue3的区别&#xff1f;三、请说一说vue2和vue3响应式原理的区别&#xff1f;四、vue3 如何定义响应式数据?五、说一说你对vue3中的setup函数?六、说一说vue3中的路由…

【LLM06---相对位置编码】

文章目录 相对位置编码经典式XLNET式T5式DeBERTa式 相对位置编码 上一节我们介绍了绝对位置编码&#xff0c;这一节我们来看相对位置编码&#xff0c;也就是用相对位置信息来表示&#xff0c;之前每一个token的位置式通过一个绝对的位置向量来表示的&#xff0c;现在我们在计算…

纯跟踪算法本质解密:航向角偏差=预瞄角?数学证明与工程实践

定义关键问题 在深入纯跟踪算法核心前&#xff0c;必须澄清一对容易被混淆但至关重要的概念&#xff1a; 概念坐标系物理意义计算方式航向角偏差(α_global)全局坐标系车辆航向与预瞄点方向的夹角预瞄点方位角 - 车辆航向角预瞄角(α_body)车身坐标系预瞄点相对于车辆纵轴的夹…

自动驾驶叉车在仓库环境中是否安全?

随着自动驾驶叉车的兴起&#xff0c;仓库运营持续演进。叉车自动化技术的引入使仓库设施变得更快、更安全且更具成本效益。然而一个关键问题依然存在&#xff1a;它们在繁忙的仓库环境中是否安全&#xff1f; 一 、什么是自动驾驶叉车&#xff1f; 自动驾驶叉车&#xff0c;也…

Neo4j操作指南:修改节点数据与新增节点属性

Neo4j操作指南&#xff1a;修改节点数据与新增节点属性 引言 Neo4j作为领先的图数据库&#xff0c;提供了灵活的数据操作方式。在实际应用中&#xff0c;我们经常需要修改已有节点的数据或为节点添加新属性。本文将详细介绍如何使用Cypher查询语言在Neo4j中完成这些操作&…