Skip to content

Commit 3f609d1

Browse files
authored
Merge pull request #482 from controlflow/master
SpinWaitEx: overload to avoid allocations, annotations
2 parents 429cb32 + ed338b5 commit 3f609d1

File tree

2 files changed

+96
-38
lines changed

2 files changed

+96
-38
lines changed

rd-net/Lifetimes/Annotations/CodeAnnotations.cs

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -434,12 +434,43 @@ public PublicAPIAttribute([NotNull] string comment)
434434
}
435435

436436
/// <summary>
437-
/// Tells code analysis engine if the parameter is completely handled when the invoked method is on stack.
438-
/// If the parameter is a delegate, indicates that delegate is executed while the method is executed.
437+
/// Tells the code analysis engine if the parameter is completely handled when the invoked method is on stack.
438+
/// If the parameter is a delegate, indicates that the delegate can only be invoked during method execution
439+
/// (the delegate can be invoked zero or multiple times, but not stored to some field and invoked later,
440+
/// when the containing method is no longer on the execution stack).
439441
/// If the parameter is an enumerable, indicates that it is enumerated while the method is executed.
442+
/// If <see cref="RequireAwait"/> is true, the attribute will only take effect
443+
/// if the method invocation is located under the <c>await</c> expression.
440444
/// </summary>
441445
[AttributeUsage(AttributeTargets.Parameter)]
442-
internal sealed class InstantHandleAttribute : Attribute { }
446+
internal sealed class InstantHandleAttribute : Attribute
447+
{
448+
/// <summary>
449+
/// Requires the method invocation to be used under the <c>await</c> expression for this attribute to take effect.
450+
/// Can be used for delegate/enumerable parameters of <c>async</c> methods.
451+
/// </summary>
452+
public bool RequireAwait { get; set; }
453+
}
454+
455+
/// <summary>
456+
/// This annotation allows enforcing allocation-less usage patterns of delegates for performance-critical APIs.
457+
/// When this annotation is applied to the parameter of a delegate type,
458+
/// the IDE checks the input argument of this parameter:
459+
/// * When a lambda expression or anonymous method is passed as an argument, the IDE verifies that the passed closure
460+
/// has no captures of the containing local variables and the compiler is able to cache the delegate instance
461+
/// to avoid heap allocations. Otherwise, a warning is produced.
462+
/// * The IDE warns when the method name or local function name is passed as an argument because this always results
463+
/// in heap allocation of the delegate instance.
464+
/// </summary>
465+
/// <remarks>
466+
/// In C# 9.0+ code, the IDE will also suggest annotating the anonymous functions with the <c>static</c> modifier
467+
/// to make use of the similar analysis provided by the language/compiler.
468+
/// </remarks>
469+
[AttributeUsage(AttributeTargets.Parameter)]
470+
internal sealed class RequireStaticDelegateAttribute : Attribute
471+
{
472+
public bool IsError { get; set; }
473+
}
443474

444475
/// <summary>
445476
/// Indicates that a method does not make any observable state changes.

rd-net/Lifetimes/Threading/SpinWaitEx.cs

Lines changed: 62 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,76 +3,99 @@
33
using JetBrains.Annotations;
44
using JetBrains.Lifetimes;
55

6-
76
namespace JetBrains.Threading
8-
{
7+
{
98
/// <summary>
109
/// Extensions for <see cref="SpinWait"/> static methods.
1110
/// </summary>
1211
public static class SpinWaitEx
1312
{
14-
1513
/// <summary>
16-
/// Spins while <paramref name="condition"/> is false.
14+
/// Spins while <paramref name="condition"/> is false.
1715
/// </summary>
1816
/// <param name="condition">Stops spinning when condition is true</param>
1917
[PublicAPI]
20-
public static void SpinUntil(Func<bool> condition)
18+
public static void SpinUntil([InstantHandle] Func<bool> condition)
2119
{
2220
SpinUntil(Lifetime.Eternal, TimeSpan.MaxValue, condition);
2321
}
2422

2523
/// <summary>
26-
/// Spins while <paramref name="lifetime"/> is alive and <paramref name="condition"/> is false.
24+
/// Spins while <paramref name="lifetime"/> is alive and <paramref name="condition"/> is false.
2725
/// </summary>
2826
/// <param name="lifetime">Stops spinning and return <c>false</c> when lifetime is no more alive</param>
2927
/// <param name="condition">Stops spinning and return <c>false</c> when condition is true</param>
30-
/// <returns><c>false</c> if <paramref name="lifetime"/> is not alive or canceled during spinning.
31-
/// Otherwise <c>true</c> (when <paramref name="condition"/> returns true)</returns>
28+
/// <returns>
29+
/// <c>false</c> if <paramref name="lifetime"/> is not alive or canceled during spinning.
30+
/// Otherwise, <c>true</c> (when <paramref name="condition"/> returns true)
31+
/// </returns>
3232
[PublicAPI]
33-
public static bool SpinUntil(Lifetime lifetime, Func<bool> condition)
33+
public static bool SpinUntil(Lifetime lifetime, [InstantHandle] Func<bool> condition)
3434
{
3535
return SpinUntil(lifetime, TimeSpan.MaxValue, condition);
3636
}
3737

3838
/// <summary>
39-
/// Spins while <paramref name="timeout"/> is not elapsed and <paramref name="condition"/> is false.
39+
/// Spins while <paramref name="timeout"/> is not elapsed and <paramref name="condition"/> is false.
4040
/// </summary>
4141
/// <param name="timeout">Stops spinning and return <c>false</c> when timeout is alive</param>
4242
/// <param name="condition">Stops spinning and return <c>false</c> when condition is true</param>
43-
/// <returns><c>false</c> if <paramref name="timeout"/> is zero or elapsed during spinning.
44-
/// Otherwise <c>true</c> (when <paramref name="condition"/> returns true)</returns>
43+
/// <returns>
44+
/// <c>false</c> if <paramref name="timeout"/> is zero or elapsed during spinning.
45+
/// Otherwise, <c>true</c> (when <paramref name="condition"/> returns true)
46+
/// </returns>
4547
[PublicAPI]
46-
public static bool SpinUntil(TimeSpan timeout, Func<bool> condition)
48+
public static bool SpinUntil(TimeSpan timeout, [InstantHandle] Func<bool> condition)
4749
{
4850
return SpinUntil(Lifetime.Eternal, (long)timeout.TotalMilliseconds, condition);
4951
}
50-
52+
5153
/// <summary>
52-
/// Spins while <paramref name="lifetime"/> is alive, <paramref name="timeout"/> is not elapsed and <paramref name="condition"/> is false.
54+
/// Spins while <paramref name="lifetime"/> is alive, <paramref name="timeout"/> is not elapsed and <paramref name="condition"/> is false.
5355
/// </summary>
5456
/// <param name="lifetime">Stops spinning and return <c>false</c> when lifetime is no more alive</param>
5557
/// <param name="timeout">Stops spinning and return <c>false</c> when timeout is alive</param>
5658
/// <param name="condition">Stops spinning and return <c>false</c> when condition is true</param>
57-
/// <returns><c>false</c> if <paramref name="lifetime"/> is not alive or canceled during spinning, <paramref name="timeout"/> is zero or elapsed during spinning.
58-
/// Otherwise <c>true</c> (when <paramref name="condition"/> returns true)</returns>
59+
/// <returns>
60+
/// <c>false</c> if <paramref name="lifetime"/> is not alive or canceled during spinning, <paramref name="timeout"/> is zero
61+
/// or elapsed during spinning. Otherwise, <c>true</c> (when <paramref name="condition"/> returns true)
62+
/// </returns>
5963
[PublicAPI]
60-
public static bool SpinUntil(Lifetime lifetime, TimeSpan timeout, Func<bool> condition)
64+
public static bool SpinUntil(Lifetime lifetime, TimeSpan timeout, [InstantHandle] Func<bool> condition)
6165
{
6266
return SpinUntil(lifetime, (long)timeout.TotalMilliseconds, condition);
6367
}
64-
65-
68+
69+
/// <summary>
70+
/// Spins while <paramref name="lifetime"/> is alive, <paramref name="timeoutMs"/> is not elapsed and <paramref name="condition"/> is false.
71+
/// </summary>
72+
/// <param name="lifetime">Stops spinning and return <c>false</c> when lifetime is no more alive</param>
73+
/// <param name="timeoutMs">Stops spinning and return <c>false</c> when timeout is alive</param>
74+
/// <param name="condition">Stops spinning and return <c>false</c> when condition is true</param>
75+
/// <returns>
76+
/// <c>false</c> if <paramref name="lifetime"/> is not alive or canceled during spinning, <paramref name="timeoutMs"/> is zero
77+
/// or elapsed during spinning. Otherwise, <c>true</c> (when <paramref name="condition"/> returns true)
78+
/// </returns>
79+
[PublicAPI]
80+
public static bool SpinUntil(Lifetime lifetime, long timeoutMs, [InstantHandle] Func<bool> condition)
81+
{
82+
return SpinUntil(lifetime, timeoutMs, state: condition, static condition => condition());
83+
}
84+
6685
/// <summary>
67-
/// Spins while <paramref name="lifetime"/> is alive, <paramref name="timeoutMs"/> is not elapsed and <paramref name="condition"/> is false.
86+
/// Spins while <paramref name="lifetime"/> is alive, <paramref name="timeoutMs"/> is not elapsed and <paramref name="condition"/> is false.
6887
/// </summary>
6988
/// <param name="lifetime">Stops spinning and return <c>false</c> when lifetime is no more alive</param>
7089
/// <param name="timeoutMs">Stops spinning and return <c>false</c> when timeout is alive</param>
7190
/// <param name="condition">Stops spinning and return <c>false</c> when condition is true</param>
72-
/// <returns><c>false</c> if <paramref name="lifetime"/> is not alive or canceled during spinning, <paramref name="timeoutMs"/> is zero or elapsed during spinning.
73-
/// Otherwise <c>true</c> (when <paramref name="condition"/> returns true)</returns>
91+
/// <param name="state">State to pass into delegate to avoid closures</param>
92+
/// <returns>
93+
/// <c>false</c> if <paramref name="lifetime"/> is not alive or canceled during spinning, <paramref name="timeoutMs"/> is zero
94+
/// or elapsed during spinning. Otherwise, <c>true</c> (when <paramref name="condition"/> returns true)
95+
/// </returns>
7496
[PublicAPI]
75-
public static bool SpinUntil(Lifetime lifetime, long timeoutMs, Func<bool> condition)
97+
public static bool SpinUntil<TState>(
98+
Lifetime lifetime, long timeoutMs, TState state, [InstantHandle, RequireStaticDelegate] Func<TState, bool> condition)
7699
{
77100
#if !NET35
78101
var s = new SpinWait();
@@ -84,9 +107,9 @@ public static bool SpinUntil(Lifetime lifetime, long timeoutMs, Func<bool> condi
84107
if (!lifetime.IsAlive)
85108
return false;
86109

87-
if (condition())
110+
if (condition(state))
88111
return true;
89-
112+
90113
if (Environment.TickCount - start > timeoutMs)
91114
return false;
92115

@@ -95,27 +118,31 @@ public static bool SpinUntil(Lifetime lifetime, long timeoutMs, Func<bool> condi
95118
#else
96119
Thread.Sleep(0);
97120
#endif
98-
}
121+
}
99122
}
100123

101-
102124
#if !NET35
103125
/// <summary>
104-
/// Spins in ASYNC manner (not consuming thread or CPU resources) while <paramref name="lifetime"/> is alive, <paramref name="timeoutMs"/> is not elapsed and <paramref name="condition"/> is false.
105-
/// Sleeps in async fashion (using <see cref="System.Threading.Tasks.Task.Delay(System.TimeSpan, CancellationToken)"/> for <paramref name="delayBetweenChecksMs"/> each time between <paramref name="condition"/> check.
106-
/// Only <paramref name="lifetime"/> cancellation could immediately return execution from delay.
126+
/// Spins in ASYNC manner (not consuming thread or CPU resources) while <paramref name="lifetime"/> is alive,
127+
/// <paramref name="timeoutMs"/> is not elapsed and <paramref name="condition"/> is false.
128+
/// Sleeps in async fashion (using <see cref="System.Threading.Tasks.Task.Delay(System.TimeSpan, CancellationToken)"/>
129+
/// for <paramref name="delayBetweenChecksMs"/> each time between <paramref name="condition"/> check.
130+
/// Only <paramref name="lifetime"/> cancellation could immediately return execution from delay.
107131
/// </summary>
108132
/// <param name="lifetime">Stops spinning and return <c>false</c> when lifetime is no more alive</param>
109133
/// <param name="timeoutMs">Stops spinning and return <c>false</c> when timeout is alive</param>
110134
/// <param name="delayBetweenChecksMs">Interval to delay</param>
111135
/// <param name="condition">Stops spinning and return <c>false</c> when condition is true</param>
112-
/// <returns><c>false</c> if <paramref name="lifetime"/> is not alive or canceled during spinning, <paramref name="timeoutMs"/> is zero or elapsed during spinning.
113-
/// Otherwise <c>true</c> (when <paramref name="condition"/> returns true)</returns>
136+
/// <returns>
137+
/// <c>false</c> if <paramref name="lifetime"/> is not alive or canceled during spinning, <paramref name="timeoutMs"/> is zero
138+
/// or elapsed during spinning. Otherwise, <c>true</c> (when <paramref name="condition"/> returns true)
139+
/// </returns>
114140
[PublicAPI]
115-
public static async System.Threading.Tasks.Task<bool> SpinUntilAsync(Lifetime lifetime, long timeoutMs, int delayBetweenChecksMs, Func<bool> condition)
141+
public static async System.Threading.Tasks.Task<bool> SpinUntilAsync(
142+
Lifetime lifetime, long timeoutMs, int delayBetweenChecksMs, [InstantHandle(RequireAwait = true)] Func<bool> condition)
116143
{
117144
var start = Environment.TickCount;
118-
145+
119146
while (true)
120147
{
121148
if (!lifetime.IsAlive || Environment.TickCount - start > timeoutMs)

0 commit comments

Comments
 (0)