fix(pool): 修复对象池系统中的双重释放和线程安全问题

- 修复 ActiveCount 在双重释放时可能变为负数的问题
- 添加对错误 key 释放的防护和警告日志
- 优化 StringBuilderPool 使用 ConcurrentBag 实现线程安全
- 改进池容量限制逻辑,超过最大容量的对象将被销毁
- 添加完整的单元测试验证双重释放、错误释放和线程安全场景
This commit is contained in:
GeWuYou 2026-02-25 20:34:25 +08:00 committed by gewuyou
parent eb763a9bc4
commit e2cfa7bffa
5 changed files with 204 additions and 51 deletions

View File

@ -46,7 +46,7 @@ public interface IObjectPoolSystem<in TKey, TObject>
/// 设置指定池的最大容量
/// </summary>
/// <param name="key">对象池的键</param>
/// <param name="maxCapacity">最大容量,超过此容量的对象将被销毁而不是放回池中</param>
/// <param name="maxCapacity">池中保留的最大对象数量。超过此数量时,释放的对象将被销毁而不是放回池中。设置为 0 表示无限制。</param>
void SetMaxCapacity(TKey key, int maxCapacity);
/// <summary>

View File

@ -250,6 +250,40 @@ public class ObjectPoolTests
Assert.That(stats.TotalReleased, Is.EqualTo(0));
Assert.That(stats.TotalDestroyed, Is.EqualTo(0));
}
/// <summary>
/// 验证双重释放不会导致 ActiveCount 变为负数
/// </summary>
[Test]
public void Release_Should_Not_Make_ActiveCount_Negative_On_Double_Release()
{
// Arrange
var obj = _pool.Acquire("test");
_pool.Release("test", obj);
// Act - 双重释放
_pool.Release("test", obj);
// Assert
Assert.That(_pool.GetActiveCount("test"), Is.EqualTo(0));
}
/// <summary>
/// 验证使用错误的 key 释放不会影响原 key 的 ActiveCount
/// </summary>
[Test]
public void Release_With_Wrong_Key_Should_Not_Affect_Original_Key_ActiveCount()
{
// Arrange
var obj = _pool.Acquire("key1");
// Act - 使用错误的 key 释放
_pool.Release("key2", obj);
// Assert
Assert.That(_pool.GetActiveCount("key1"), Is.EqualTo(1));
Assert.That(_pool.GetActiveCount("key2"), Is.EqualTo(0));
}
}
/// <summary>

View File

@ -113,4 +113,85 @@ public class StringBuilderPoolTests
// Assert
Assert.That(result, Is.EqualTo("Hello World"));
}
}
/// <summary>
/// 验证 Rent 方法会复用已归还的 StringBuilder 实例
/// </summary>
[Test]
public void Rent_Should_Reuse_Returned_StringBuilder()
{
// Arrange
var sb1 = StringBuilderPool.Rent();
var originalInstance = sb1;
StringBuilderPool.Return(sb1);
// Act
var sb2 = StringBuilderPool.Rent();
// Assert
Assert.That(sb2, Is.SameAs(originalInstance), "应该复用同一个实例");
}
/// <summary>
/// 验证 Rent 方法会确保返回的 StringBuilder 满足最小容量要求
/// </summary>
[Test]
public void Rent_Should_Ensure_Minimum_Capacity()
{
// Arrange
var sb1 = StringBuilderPool.Rent(100);
StringBuilderPool.Return(sb1);
// Act - 请求更大容量
var sb2 = StringBuilderPool.Rent(500);
// Assert
Assert.That(sb2.Capacity, Is.GreaterThanOrEqualTo(500));
}
/// <summary>
/// 验证超过最大保留容量的 StringBuilder 不会被池化
/// </summary>
[Test]
public void Return_Should_Not_Pool_Large_Capacity_StringBuilder()
{
// Arrange
var sb1 = StringBuilderPool.Rent(10000);
StringBuilderPool.Return(sb1);
// Act - 租用新的小容量 StringBuilder
var sb2 = StringBuilderPool.Rent(100);
// Assert
Assert.That(sb2, Is.Not.SameAs(sb1), "大容量实例不应被池化");
}
/// <summary>
/// 验证对象池在多线程环境下的线程安全性
/// </summary>
[Test]
public void Pool_Should_Be_Thread_Safe()
{
// Arrange
const int threadCount = 10;
const int operationsPerThread = 100;
var tasks = new Task[threadCount];
// Act
for (int i = 0; i < threadCount; i++)
{
tasks[i] = Task.Run(() =>
{
for (int j = 0; j < operationsPerThread; j++)
{
var sb = StringBuilderPool.Rent();
sb.Append("Test");
StringBuilderPool.Return(sb);
}
});
}
// Assert
Assert.DoesNotThrow(() => Task.WaitAll(tasks));
}
}

View File

@ -1,4 +1,5 @@
using GFramework.Core.Abstractions.pool;
using System.Diagnostics;
using GFramework.Core.Abstractions.pool;
using GFramework.Core.system;
namespace GFramework.Core.pool;
@ -11,49 +12,6 @@ namespace GFramework.Core.pool;
public abstract class AbstractObjectPoolSystem<TKey, TObject>
: AbstractSystem, IObjectPoolSystem<TKey, TObject> where TObject : IPoolableObject where TKey : notnull
{
/// <summary>
/// 池信息类,用于管理对象池的核心数据结构和统计信息。
/// 包含对象栈、容量限制以及各类操作的统计计数。
/// </summary>
protected class PoolInfo
{
/// <summary>
/// 对象栈,用于存储可复用的对象实例。
/// </summary>
public Stack<TObject> Stack { get; } = new();
/// <summary>
/// 池的最大容量限制,超过此数量时将不再创建新对象。
/// </summary>
public int MaxCapacity { get; set; }
/// <summary>
/// 总共创建的对象数量统计。
/// </summary>
public int TotalCreated { get; set; }
/// <summary>
/// 总共从池中获取的对象数量统计。
/// </summary>
public int TotalAcquired { get; set; }
/// <summary>
/// 总共归还到池中的对象数量统计。
/// </summary>
public int TotalReleased { get; set; }
/// <summary>
/// 总共销毁的对象数量统计。
/// </summary>
public int TotalDestroyed { get; set; }
/// <summary>
/// 当前活跃(正在使用)的对象数量统计。
/// </summary>
public int ActiveCount { get; set; }
}
/// <summary>
/// 存储对象池的字典,键为池标识,值为池信息
/// </summary>
@ -105,7 +63,19 @@ public abstract class AbstractObjectPoolSystem<TKey, TObject>
}
poolInfo.TotalReleased++;
poolInfo.ActiveCount--;
// 防止 ActiveCount 变为负数
if (poolInfo.ActiveCount > 0)
{
poolInfo.ActiveCount--;
}
else
{
// 记录警告:检测到可能的双重释放或错误释放
Debug.WriteLine(
$"[ObjectPool] Warning: Attempting to release object for key '{key}' " +
$"but ActiveCount is already 0. Possible double-release or incorrect key.");
}
// 检查容量限制
if (poolInfo.MaxCapacity > 0 && poolInfo.Stack.Count >= poolInfo.MaxCapacity)
@ -161,7 +131,7 @@ public abstract class AbstractObjectPoolSystem<TKey, TObject>
/// 设置指定池的最大容量
/// </summary>
/// <param name="key">对象池的键</param>
/// <param name="maxCapacity">最大容量,超过此容量的对象将被销毁而不是放回池中</param>
/// <param name="maxCapacity">池中保留的最大对象数量。超过此数量时,释放的对象将被销毁而不是放回池中。设置为 0 表示无限制。</param>
public void SetMaxCapacity(TKey key, int maxCapacity)
{
if (!Pools.TryGetValue(key, out var poolInfo))
@ -242,4 +212,47 @@ public abstract class AbstractObjectPoolSystem<TKey, TObject>
{
Clear();
}
/// <summary>
/// 池信息类,用于管理对象池的核心数据结构和统计信息。
/// 包含对象栈、容量限制以及各类操作的统计计数。
/// </summary>
protected class PoolInfo
{
/// <summary>
/// 对象栈,用于存储可复用的对象实例。
/// </summary>
public Stack<TObject> Stack { get; } = new();
/// <summary>
/// 池中保留的最大对象数量。当释放对象时,如果池中对象数已达到此限制,
/// 对象将被销毁而不是放回池中。设置为 0 表示无限制。
/// </summary>
public int MaxCapacity { get; set; }
/// <summary>
/// 总共创建的对象数量统计。
/// </summary>
public int TotalCreated { get; set; }
/// <summary>
/// 总共从池中获取的对象数量统计。
/// </summary>
public int TotalAcquired { get; set; }
/// <summary>
/// 总共归还到池中的对象数量统计。
/// </summary>
public int TotalReleased { get; set; }
/// <summary>
/// 总共销毁的对象数量统计。
/// </summary>
public int TotalDestroyed { get; set; }
/// <summary>
/// 当前活跃(正在使用)的对象数量统计。
/// </summary>
public int ActiveCount { get; set; }
}
}

View File

@ -1,3 +1,4 @@
using System.Collections.Concurrent;
using System.Text;
namespace GFramework.Core.pool;
@ -10,11 +11,18 @@ public static class StringBuilderPool
private const int DefaultCapacity = 256;
private const int MaxRetainedCapacity = 4096;
// 使用 ConcurrentBag 实现线程安全的对象池
private static readonly ConcurrentBag<StringBuilder> Pool = new();
/// <summary>
/// 从池中租用一个 StringBuilder
/// </summary>
/// <param name="capacity">初始容量,默认为 256</param>
/// <returns>StringBuilder 实例</returns>
/// <remarks>
/// 优先从池中获取可复用的实例,如果池为空则创建新实例。
/// 使用完毕后应调用 <see cref="Return"/> 方法归还到池中以便复用。
/// </remarks>
/// <example>
/// <code>
/// var sb = StringBuilderPool.Rent();
@ -32,14 +40,29 @@ public static class StringBuilderPool
/// </example>
public static StringBuilder Rent(int capacity = DefaultCapacity)
{
var sb = new StringBuilder(capacity);
return sb;
if (Pool.TryTake(out var sb))
{
// 从池中获取到实例,确保容量满足需求
if (sb.Capacity < capacity)
{
sb.Capacity = capacity;
}
return sb;
}
// 池为空,创建新实例
return new StringBuilder(capacity);
}
/// <summary>
/// 将 StringBuilder 归还到池中
/// </summary>
/// <param name="builder">要归还的 StringBuilder</param>
/// <remarks>
/// 如果 StringBuilder 的容量超过 <see cref="MaxRetainedCapacity"/>
/// 则不会放回池中,而是直接丢弃以避免保留过大的对象。
/// </remarks>
/// <example>
/// <code>
/// var sb = StringBuilderPool.Rent();
@ -64,7 +87,9 @@ public static class StringBuilderPool
return;
}
// 清空内容并放回池中
builder.Clear();
Pool.Add(builder);
}
/// <summary>
@ -108,4 +133,4 @@ public static class StringBuilderPool
Return(Value);
}
}
}
}