Skip to content

Commit 7a8476f

Browse files
authored
Merge pull request #38 from Cysharp/feature/DisposePooledLogicLooper
Prevent direct disposal or shutdown of Loopers retrieved from LogicLooperPool.
2 parents 46d3678 + 3101464 commit 7a8476f

6 files changed

Lines changed: 282 additions & 15 deletions

File tree

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
1-
namespace Cysharp.Threading;
1+
namespace Cysharp.Threading;
22

3+
/// <summary>
4+
/// Defines a strategy for selecting an <see cref="ILogicLooper"/> instance from a pool of available loopers.
5+
/// </summary>
6+
/// <remarks>Implementations of this interface determine how a looper is chosen from the provided pool, which may
7+
/// affect load distribution, performance, or resource utilization.</remarks>
38
public interface ILogicLooperPoolBalancer
49
{
5-
LogicLooper GetPooledLooper(LogicLooper[] pooledLoopers);
10+
/// <summary>
11+
/// Gets an available <see cref="ILogicLooper"/> instance from the specified pool.
12+
/// </summary>
13+
ILogicLooper GetPooledLooper(ILogicLooper[] pooledLoopers);
614
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
namespace Cysharp.Threading;
2+
3+
/// <summary>
4+
/// Defines a factory for creating <see cref="ILogicLooper"/> instances.
5+
/// </summary>
6+
public interface ILogicLooperPoolLooperFactory
7+
{
8+
/// <summary>
9+
/// Creates a new <see cref="ILogicLooper"/> instance configured to operate with the specified target frame time.
10+
/// </summary>
11+
ILogicLooper Create(TimeSpan targetFrameTime);
12+
}
13+
14+
/// <summary>
15+
/// Provides a default implementation of <see cref="ILogicLooperPoolLooperFactory"/> for creating logic loopers with a
16+
/// specified target frame time.
17+
/// </summary>
18+
public class DefaultLogicLooperPoolLooperFactory : ILogicLooperPoolLooperFactory
19+
{
20+
/// <summary>
21+
/// Gets the singleton instance of the <see cref="ILogicLooperPoolLooperFactory"/> used to create logic looper pool
22+
/// looper objects.
23+
/// </summary>
24+
public static ILogicLooperPoolLooperFactory Instance { get; } = new DefaultLogicLooperPoolLooperFactory();
25+
26+
private DefaultLogicLooperPoolLooperFactory() { }
27+
28+
public ILogicLooper Create(TimeSpan targetFrameTime)
29+
=> new LogicLooper(targetFrameTime);
30+
}

src/LogicLooper/LogicLooperPool.Shared.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using Cysharp.Threading.Internal;
1+
using Cysharp.Threading.Internal;
22

33
namespace Cysharp.Threading;
44

@@ -15,13 +15,19 @@ public sealed partial class LogicLooperPool
1515
/// <param name="targetFrameRate"></param>
1616
/// <param name="looperCount"></param>
1717
/// <param name="balancer"></param>
18-
public static void InitializeSharedPool(int targetFrameRate, int looperCount = 0, ILogicLooperPoolBalancer? balancer = null)
18+
/// <param name="looperFactory"></param>
19+
public static void InitializeSharedPool(int targetFrameRate, int looperCount = 0, ILogicLooperPoolBalancer? balancer = null, ILogicLooperPoolLooperFactory? looperFactory = null)
1920
{
2021
if (looperCount == 0)
2122
{
2223
looperCount = Math.Max(1, Environment.ProcessorCount - 1);
2324
}
2425

25-
Shared = new LogicLooperPool(targetFrameRate, looperCount, balancer ?? RoundRobinLogicLooperPoolBalancer.Instance);
26+
Shared = new LogicLooperPool(
27+
targetFrameRate,
28+
looperCount,
29+
balancer ?? RoundRobinLogicLooperPoolBalancer.Instance,
30+
looperFactory ?? DefaultLogicLooperPoolLooperFactory.Instance
31+
);
2632
}
2733
}

src/LogicLooper/LogicLooperPool.cs

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using Cysharp.Threading.Internal;
1+
using Cysharp.Threading.Internal;
22

33
namespace Cysharp.Threading;
44

@@ -7,7 +7,7 @@ namespace Cysharp.Threading;
77
/// </summary>
88
public sealed partial class LogicLooperPool : ILogicLooperPool, IDisposable
99
{
10-
private readonly LogicLooper[] _loopers;
10+
private readonly PooledLogicLooper[] _loopers;
1111
private readonly ILogicLooperPoolBalancer _balancer;
1212
private readonly CancellationTokenSource _shutdownTokenSource = new();
1313

@@ -24,20 +24,44 @@ public LogicLooperPool(int targetFrameRate, int looperCount, ILogicLooperPoolBal
2424
: this(TimeSpan.FromMilliseconds(1000 / (double)targetFrameRate), looperCount, balancer)
2525
{ }
2626

27+
2728
/// <summary>
2829
/// Initialize the looper pool with specified configurations.
2930
/// </summary>
3031
/// <param name="targetFrameTime"></param>
3132
/// <param name="looperCount"></param>
3233
/// <param name="balancer"></param>
3334
public LogicLooperPool(TimeSpan targetFrameTime, int looperCount, ILogicLooperPoolBalancer balancer)
35+
: this(targetFrameTime, looperCount, balancer, DefaultLogicLooperPoolLooperFactory.Instance)
36+
{
37+
}
38+
39+
/// <summary>
40+
/// Initialize the looper pool with specified configurations.
41+
/// </summary>
42+
/// <param name="targetFrameRate"></param>
43+
/// <param name="looperCount"></param>
44+
/// <param name="balancer"></param>
45+
/// <param name="looperFactory"></param>
46+
public LogicLooperPool(int targetFrameRate, int looperCount, ILogicLooperPoolBalancer balancer, ILogicLooperPoolLooperFactory looperFactory)
47+
: this(TimeSpan.FromMilliseconds(1000 / (double)targetFrameRate), looperCount, balancer, looperFactory)
48+
{ }
49+
50+
/// <summary>
51+
/// Initialize the looper pool with specified configurations.
52+
/// </summary>
53+
/// <param name="targetFrameTime"></param>
54+
/// <param name="looperCount"></param>
55+
/// <param name="balancer"></param>
56+
/// <param name="looperFactory"></param>
57+
public LogicLooperPool(TimeSpan targetFrameTime, int looperCount, ILogicLooperPoolBalancer balancer, ILogicLooperPoolLooperFactory looperFactory)
3458
{
3559
if (looperCount <= 0) throw new ArgumentOutOfRangeException(nameof(looperCount), "LooperCount must be more than zero.");
3660

37-
_loopers = new LogicLooper[looperCount];
61+
_loopers = new PooledLogicLooper[looperCount];
3862
for (var i = 0; i < looperCount; i++)
3963
{
40-
_loopers[i] = new LogicLooper(targetFrameTime);
64+
_loopers[i] = new PooledLogicLooper(looperFactory.Create(targetFrameTime));
4165
}
4266
_balancer = balancer ?? throw new ArgumentNullException(nameof(balancer));
4367
}
@@ -77,7 +101,7 @@ public Task RegisterActionAsync<TState>(LogicLooperAsyncActionWithStateDelegate<
77101
/// <inheritdoc />
78102
public async Task ShutdownAsync(TimeSpan shutdownDelay)
79103
{
80-
await Task.WhenAll(_loopers.Select(x => x.ShutdownAsync(shutdownDelay)));
104+
await Task.WhenAll(_loopers.Select(x => x.WrappedLooper.ShutdownAsync(shutdownDelay)));
81105
}
82106

83107
/// <inheritdoc />
@@ -90,12 +114,51 @@ public void Dispose()
90114
{
91115
try
92116
{
93-
looper.Dispose();
117+
looper.WrappedLooper.Dispose();
94118
}
95119
catch
96120
{
97121
}
98122
}
99123
_shutdownTokenSource.Cancel();
100124
}
125+
126+
private class PooledLogicLooper : ILogicLooper
127+
{
128+
private readonly ILogicLooper _looper;
129+
130+
public ILogicLooper WrappedLooper => _looper;
131+
132+
public PooledLogicLooper(ILogicLooper looper)
133+
{
134+
_looper = looper ?? throw new ArgumentNullException(nameof(looper));
135+
}
136+
137+
public int Id => _looper.Id;
138+
public int ApproximatelyRunningActions => _looper.ApproximatelyRunningActions;
139+
public TimeSpan LastProcessingDuration => _looper.LastProcessingDuration;
140+
public double TargetFrameRate => _looper.TargetFrameRate;
141+
public long CurrentFrame => _looper.CurrentFrame;
142+
public Task RegisterActionAsync(LogicLooperActionDelegate loopAction)
143+
=> _looper.RegisterActionAsync(loopAction);
144+
public Task RegisterActionAsync(LogicLooperActionDelegate loopAction, LooperActionOptions options)
145+
=> _looper.RegisterActionAsync(loopAction, options);
146+
public Task RegisterActionAsync<TState>(LogicLooperActionWithStateDelegate<TState> loopAction, TState state)
147+
=> _looper.RegisterActionAsync(loopAction, state);
148+
public Task RegisterActionAsync<TState>(LogicLooperActionWithStateDelegate<TState> loopAction, TState state, LooperActionOptions options)
149+
=> _looper.RegisterActionAsync(loopAction, state, options);
150+
public Task RegisterActionAsync(LogicLooperAsyncActionDelegate loopAction)
151+
=> _looper.RegisterActionAsync(loopAction);
152+
public Task RegisterActionAsync(LogicLooperAsyncActionDelegate loopAction, LooperActionOptions options)
153+
=> _looper.RegisterActionAsync(loopAction, options);
154+
public Task RegisterActionAsync<TState>(LogicLooperAsyncActionWithStateDelegate<TState> loopAction, TState state)
155+
=> _looper.RegisterActionAsync(loopAction, state);
156+
public Task RegisterActionAsync<TState>(LogicLooperAsyncActionWithStateDelegate<TState> loopAction, TState state, LooperActionOptions options)
157+
=> _looper.RegisterActionAsync(loopAction, state, options);
158+
159+
public Task ShutdownAsync(TimeSpan shutdownDelay)
160+
=> throw new NotSupportedException("PooledLogicLooper does not support ShutdownAsync. Use the LogicLooperPool to shutdown all loopers.");
161+
public void Dispose()
162+
{}
163+
}
101164
}

src/LogicLooper/RoundRobinLogicLooperPoolBalancer.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
namespace Cysharp.Threading;
1+
namespace Cysharp.Threading;
22

33
public class RoundRobinLogicLooperPoolBalancer : ILogicLooperPoolBalancer
44
{
@@ -9,7 +9,7 @@ public class RoundRobinLogicLooperPoolBalancer : ILogicLooperPoolBalancer
99
protected RoundRobinLogicLooperPoolBalancer()
1010
{ }
1111

12-
public LogicLooper GetPooledLooper(LogicLooper[] pooledLoopers)
12+
public ILogicLooper GetPooledLooper(ILogicLooper[] pooledLoopers)
1313
{
1414
return pooledLoopers[Interlocked.Increment(ref _index) % pooledLoopers.Length];
1515
}

test/LogicLooper.Test/LogicLooperPoolTest.cs

Lines changed: 162 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using Cysharp.Threading;
1+
using Cysharp.Threading;
22

33
namespace LogicLooper.Test;
44

@@ -55,12 +55,172 @@ public void GetLooper()
5555
Assert.Equal(pool.Loopers[0], pool.GetLooper());
5656
}
5757

58+
[Fact]
59+
public void PooledLogicLooper_Dispose_Noop()
60+
{
61+
var looperFactory = new FakeLogicLooperPoolLooperFactory();
62+
using var pool = new LogicLooperPool(60, 4, RoundRobinLogicLooperPoolBalancer.Instance, looperFactory);
63+
var pooled1 = pool.GetLooper();
64+
var pooled2 = pool.GetLooper();
65+
66+
Assert.Equal(4, looperFactory.CreatedLoopers.Count);
67+
Assert.False(looperFactory.CreatedLoopers[0].IsDisposed);
68+
Assert.False(looperFactory.CreatedLoopers[1].IsDisposed);
69+
Assert.False(looperFactory.CreatedLoopers[2].IsDisposed);
70+
Assert.False(looperFactory.CreatedLoopers[3].IsDisposed);
71+
72+
pooled1.Dispose();
73+
pooled2.Dispose();
74+
75+
Assert.False(looperFactory.CreatedLoopers[0].IsDisposed);
76+
Assert.False(looperFactory.CreatedLoopers[1].IsDisposed);
77+
Assert.False(looperFactory.CreatedLoopers[2].IsDisposed);
78+
Assert.False(looperFactory.CreatedLoopers[3].IsDisposed);
79+
}
80+
81+
[Fact]
82+
public void PooledLogicLooper_DisposeFromLooperPool()
83+
{
84+
var looperFactory = new FakeLogicLooperPoolLooperFactory();
85+
var pool = new LogicLooperPool(60, 4, RoundRobinLogicLooperPoolBalancer.Instance, looperFactory);
86+
87+
Assert.Equal(4, looperFactory.CreatedLoopers.Count);
88+
Assert.False(looperFactory.CreatedLoopers[0].IsDisposed);
89+
Assert.False(looperFactory.CreatedLoopers[1].IsDisposed);
90+
Assert.False(looperFactory.CreatedLoopers[2].IsDisposed);
91+
Assert.False(looperFactory.CreatedLoopers[3].IsDisposed);
92+
93+
pool.Dispose();
94+
95+
Assert.True(looperFactory.CreatedLoopers[0].IsDisposed);
96+
Assert.True(looperFactory.CreatedLoopers[1].IsDisposed);
97+
Assert.True(looperFactory.CreatedLoopers[2].IsDisposed);
98+
Assert.True(looperFactory.CreatedLoopers[3].IsDisposed);
99+
}
100+
101+
[Fact]
102+
public async Task PooledLogicLooper_ShutdownAsync_NotSupported()
103+
{
104+
var looperFactory = new FakeLogicLooperPoolLooperFactory();
105+
using var pool = new LogicLooperPool(60, 4, RoundRobinLogicLooperPoolBalancer.Instance, looperFactory);
106+
var pooled1 = pool.GetLooper();
107+
var pooled2 = pool.GetLooper();
108+
109+
Assert.Equal(4, looperFactory.CreatedLoopers.Count);
110+
Assert.False(looperFactory.CreatedLoopers[0].IsShutdownRequested);
111+
Assert.False(looperFactory.CreatedLoopers[1].IsShutdownRequested);
112+
Assert.False(looperFactory.CreatedLoopers[2].IsShutdownRequested);
113+
Assert.False(looperFactory.CreatedLoopers[3].IsShutdownRequested);
114+
115+
await Assert.ThrowsAsync<NotSupportedException>(() => pooled1.ShutdownAsync(TimeSpan.FromSeconds(1)));
116+
await Assert.ThrowsAsync<NotSupportedException>(() => pooled2.ShutdownAsync(TimeSpan.FromSeconds(1)));
117+
Assert.False(looperFactory.CreatedLoopers[0].IsShutdownRequested);
118+
Assert.False(looperFactory.CreatedLoopers[1].IsShutdownRequested);
119+
Assert.False(looperFactory.CreatedLoopers[2].IsShutdownRequested);
120+
Assert.False(looperFactory.CreatedLoopers[3].IsShutdownRequested);
121+
}
122+
123+
[Fact]
124+
public async Task PooledLogicLooper_ShutdownAsyncFromLooperPool()
125+
{
126+
var looperFactory = new FakeLogicLooperPoolLooperFactory();
127+
using var pool = new LogicLooperPool(60, 4, RoundRobinLogicLooperPoolBalancer.Instance, looperFactory);
128+
var pooled1 = pool.GetLooper();
129+
var pooled2 = pool.GetLooper();
130+
131+
Assert.Equal(4, looperFactory.CreatedLoopers.Count);
132+
Assert.False(looperFactory.CreatedLoopers[0].IsShutdownRequested);
133+
Assert.False(looperFactory.CreatedLoopers[1].IsShutdownRequested);
134+
Assert.False(looperFactory.CreatedLoopers[2].IsShutdownRequested);
135+
Assert.False(looperFactory.CreatedLoopers[3].IsShutdownRequested);
136+
137+
await pool.ShutdownAsync(TimeSpan.FromSeconds(1));
138+
Assert.True(looperFactory.CreatedLoopers[0].IsShutdownRequested);
139+
Assert.True(looperFactory.CreatedLoopers[1].IsShutdownRequested);
140+
Assert.True(looperFactory.CreatedLoopers[2].IsShutdownRequested);
141+
Assert.True(looperFactory.CreatedLoopers[3].IsShutdownRequested);
142+
}
143+
58144
class FakeSequentialLogicLooperPoolBalancer : ILogicLooperPoolBalancer
59145
{
60146
private int _count;
61-
public Cysharp.Threading.LogicLooper GetPooledLooper(Cysharp.Threading.LogicLooper[] pooledLoopers)
147+
public Cysharp.Threading.ILogicLooper GetPooledLooper(Cysharp.Threading.ILogicLooper[] pooledLoopers)
62148
{
63149
return pooledLoopers[_count++ % pooledLoopers.Length];
64150
}
65151
}
152+
153+
class FakeLogicLooperPoolLooperFactory : ILogicLooperPoolLooperFactory
154+
{
155+
public List<LogicLooper> CreatedLoopers { get; } = new();
156+
157+
public ILogicLooper Create(TimeSpan targetFrameTime)
158+
{
159+
var looper = new LogicLooper();
160+
CreatedLoopers.Add(looper);
161+
return looper;
162+
}
163+
164+
public class LogicLooper : ILogicLooper
165+
{
166+
public bool IsDisposed { get; set; }
167+
public bool IsShutdownRequested { get; set; }
168+
169+
public void Dispose()
170+
{
171+
IsDisposed = true;
172+
}
173+
174+
public int Id { get; }
175+
public int ApproximatelyRunningActions { get; }
176+
public TimeSpan LastProcessingDuration { get; }
177+
public double TargetFrameRate { get; }
178+
public long CurrentFrame { get; }
179+
public Task RegisterActionAsync(LogicLooperActionDelegate loopAction)
180+
{
181+
throw new NotImplementedException();
182+
}
183+
184+
public Task RegisterActionAsync(LogicLooperActionDelegate loopAction, LooperActionOptions options)
185+
{
186+
throw new NotImplementedException();
187+
}
188+
189+
public Task RegisterActionAsync<TState>(LogicLooperActionWithStateDelegate<TState> loopAction, TState state)
190+
{
191+
throw new NotImplementedException();
192+
}
193+
194+
public Task RegisterActionAsync<TState>(LogicLooperActionWithStateDelegate<TState> loopAction, TState state, LooperActionOptions options)
195+
{
196+
throw new NotImplementedException();
197+
}
198+
199+
public Task RegisterActionAsync(LogicLooperAsyncActionDelegate loopAction)
200+
{
201+
throw new NotImplementedException();
202+
}
203+
204+
public Task RegisterActionAsync(LogicLooperAsyncActionDelegate loopAction, LooperActionOptions options)
205+
{
206+
throw new NotImplementedException();
207+
}
208+
209+
public Task RegisterActionAsync<TState>(LogicLooperAsyncActionWithStateDelegate<TState> loopAction, TState state)
210+
{
211+
throw new NotImplementedException();
212+
}
213+
214+
public Task RegisterActionAsync<TState>(LogicLooperAsyncActionWithStateDelegate<TState> loopAction, TState state, LooperActionOptions options)
215+
{
216+
throw new NotImplementedException();
217+
}
218+
219+
public Task ShutdownAsync(TimeSpan shutdownDelay)
220+
{
221+
IsShutdownRequested = true;
222+
return Task.CompletedTask;
223+
}
224+
}
225+
}
66226
}

0 commit comments

Comments
 (0)