Skip to content

Commit

Permalink
Merge pull request #482 from controlflow/master
Browse files Browse the repository at this point in the history
SpinWaitEx: overload to avoid allocations, annotations
  • Loading branch information
ulex authored May 27, 2024
2 parents 429cb32 + ed338b5 commit 3f609d1
Show file tree
Hide file tree
Showing 2 changed files with 96 additions and 38 deletions.
37 changes: 34 additions & 3 deletions rd-net/Lifetimes/Annotations/CodeAnnotations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -434,12 +434,43 @@ public PublicAPIAttribute([NotNull] string comment)
}

/// <summary>
/// Tells code analysis engine if the parameter is completely handled when the invoked method is on stack.
/// If the parameter is a delegate, indicates that delegate is executed while the method is executed.
/// Tells the code analysis engine if the parameter is completely handled when the invoked method is on stack.
/// If the parameter is a delegate, indicates that the delegate can only be invoked during method execution
/// (the delegate can be invoked zero or multiple times, but not stored to some field and invoked later,
/// when the containing method is no longer on the execution stack).
/// If the parameter is an enumerable, indicates that it is enumerated while the method is executed.
/// If <see cref="RequireAwait"/> is true, the attribute will only take effect
/// if the method invocation is located under the <c>await</c> expression.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter)]
internal sealed class InstantHandleAttribute : Attribute { }
internal sealed class InstantHandleAttribute : Attribute
{
/// <summary>
/// Requires the method invocation to be used under the <c>await</c> expression for this attribute to take effect.
/// Can be used for delegate/enumerable parameters of <c>async</c> methods.
/// </summary>
public bool RequireAwait { get; set; }
}

/// <summary>
/// This annotation allows enforcing allocation-less usage patterns of delegates for performance-critical APIs.
/// When this annotation is applied to the parameter of a delegate type,
/// the IDE checks the input argument of this parameter:
/// * When a lambda expression or anonymous method is passed as an argument, the IDE verifies that the passed closure
/// has no captures of the containing local variables and the compiler is able to cache the delegate instance
/// to avoid heap allocations. Otherwise, a warning is produced.
/// * The IDE warns when the method name or local function name is passed as an argument because this always results
/// in heap allocation of the delegate instance.
/// </summary>
/// <remarks>
/// In C# 9.0+ code, the IDE will also suggest annotating the anonymous functions with the <c>static</c> modifier
/// to make use of the similar analysis provided by the language/compiler.
/// </remarks>
[AttributeUsage(AttributeTargets.Parameter)]
internal sealed class RequireStaticDelegateAttribute : Attribute
{
public bool IsError { get; set; }
}

/// <summary>
/// Indicates that a method does not make any observable state changes.
Expand Down
97 changes: 62 additions & 35 deletions rd-net/Lifetimes/Threading/SpinWaitEx.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,76 +3,99 @@
using JetBrains.Annotations;
using JetBrains.Lifetimes;


namespace JetBrains.Threading
{
{
/// <summary>
/// Extensions for <see cref="SpinWait"/> static methods.
/// </summary>
public static class SpinWaitEx
{

/// <summary>
/// Spins while <paramref name="condition"/> is false.
/// Spins while <paramref name="condition"/> is false.
/// </summary>
/// <param name="condition">Stops spinning when condition is true</param>
[PublicAPI]
public static void SpinUntil(Func<bool> condition)
public static void SpinUntil([InstantHandle] Func<bool> condition)
{
SpinUntil(Lifetime.Eternal, TimeSpan.MaxValue, condition);
}

/// <summary>
/// Spins while <paramref name="lifetime"/> is alive and <paramref name="condition"/> is false.
/// Spins while <paramref name="lifetime"/> is alive and <paramref name="condition"/> is false.
/// </summary>
/// <param name="lifetime">Stops spinning and return <c>false</c> when lifetime is no more alive</param>
/// <param name="condition">Stops spinning and return <c>false</c> when condition is true</param>
/// <returns><c>false</c> if <paramref name="lifetime"/> is not alive or canceled during spinning.
/// Otherwise <c>true</c> (when <paramref name="condition"/> returns true)</returns>
/// <returns>
/// <c>false</c> if <paramref name="lifetime"/> is not alive or canceled during spinning.
/// Otherwise, <c>true</c> (when <paramref name="condition"/> returns true)
/// </returns>
[PublicAPI]
public static bool SpinUntil(Lifetime lifetime, Func<bool> condition)
public static bool SpinUntil(Lifetime lifetime, [InstantHandle] Func<bool> condition)
{
return SpinUntil(lifetime, TimeSpan.MaxValue, condition);
}

/// <summary>
/// Spins while <paramref name="timeout"/> is not elapsed and <paramref name="condition"/> is false.
/// Spins while <paramref name="timeout"/> is not elapsed and <paramref name="condition"/> is false.
/// </summary>
/// <param name="timeout">Stops spinning and return <c>false</c> when timeout is alive</param>
/// <param name="condition">Stops spinning and return <c>false</c> when condition is true</param>
/// <returns><c>false</c> if <paramref name="timeout"/> is zero or elapsed during spinning.
/// Otherwise <c>true</c> (when <paramref name="condition"/> returns true)</returns>
/// <returns>
/// <c>false</c> if <paramref name="timeout"/> is zero or elapsed during spinning.
/// Otherwise, <c>true</c> (when <paramref name="condition"/> returns true)
/// </returns>
[PublicAPI]
public static bool SpinUntil(TimeSpan timeout, Func<bool> condition)
public static bool SpinUntil(TimeSpan timeout, [InstantHandle] Func<bool> condition)
{
return SpinUntil(Lifetime.Eternal, (long)timeout.TotalMilliseconds, condition);
}

/// <summary>
/// Spins while <paramref name="lifetime"/> is alive, <paramref name="timeout"/> is not elapsed and <paramref name="condition"/> is false.
/// Spins while <paramref name="lifetime"/> is alive, <paramref name="timeout"/> is not elapsed and <paramref name="condition"/> is false.
/// </summary>
/// <param name="lifetime">Stops spinning and return <c>false</c> when lifetime is no more alive</param>
/// <param name="timeout">Stops spinning and return <c>false</c> when timeout is alive</param>
/// <param name="condition">Stops spinning and return <c>false</c> when condition is true</param>
/// <returns><c>false</c> if <paramref name="lifetime"/> is not alive or canceled during spinning, <paramref name="timeout"/> is zero or elapsed during spinning.
/// Otherwise <c>true</c> (when <paramref name="condition"/> returns true)</returns>
/// <returns>
/// <c>false</c> if <paramref name="lifetime"/> is not alive or canceled during spinning, <paramref name="timeout"/> is zero
/// or elapsed during spinning. Otherwise, <c>true</c> (when <paramref name="condition"/> returns true)
/// </returns>
[PublicAPI]
public static bool SpinUntil(Lifetime lifetime, TimeSpan timeout, Func<bool> condition)
public static bool SpinUntil(Lifetime lifetime, TimeSpan timeout, [InstantHandle] Func<bool> condition)
{
return SpinUntil(lifetime, (long)timeout.TotalMilliseconds, condition);
}



/// <summary>
/// Spins while <paramref name="lifetime"/> is alive, <paramref name="timeoutMs"/> is not elapsed and <paramref name="condition"/> is false.
/// </summary>
/// <param name="lifetime">Stops spinning and return <c>false</c> when lifetime is no more alive</param>
/// <param name="timeoutMs">Stops spinning and return <c>false</c> when timeout is alive</param>
/// <param name="condition">Stops spinning and return <c>false</c> when condition is true</param>
/// <returns>
/// <c>false</c> if <paramref name="lifetime"/> is not alive or canceled during spinning, <paramref name="timeoutMs"/> is zero
/// or elapsed during spinning. Otherwise, <c>true</c> (when <paramref name="condition"/> returns true)
/// </returns>
[PublicAPI]
public static bool SpinUntil(Lifetime lifetime, long timeoutMs, [InstantHandle] Func<bool> condition)
{
return SpinUntil(lifetime, timeoutMs, state: condition, static condition => condition());
}

/// <summary>
/// Spins while <paramref name="lifetime"/> is alive, <paramref name="timeoutMs"/> is not elapsed and <paramref name="condition"/> is false.
/// Spins while <paramref name="lifetime"/> is alive, <paramref name="timeoutMs"/> is not elapsed and <paramref name="condition"/> is false.
/// </summary>
/// <param name="lifetime">Stops spinning and return <c>false</c> when lifetime is no more alive</param>
/// <param name="timeoutMs">Stops spinning and return <c>false</c> when timeout is alive</param>
/// <param name="condition">Stops spinning and return <c>false</c> when condition is true</param>
/// <returns><c>false</c> if <paramref name="lifetime"/> is not alive or canceled during spinning, <paramref name="timeoutMs"/> is zero or elapsed during spinning.
/// Otherwise <c>true</c> (when <paramref name="condition"/> returns true)</returns>
/// <param name="state">State to pass into delegate to avoid closures</param>
/// <returns>
/// <c>false</c> if <paramref name="lifetime"/> is not alive or canceled during spinning, <paramref name="timeoutMs"/> is zero
/// or elapsed during spinning. Otherwise, <c>true</c> (when <paramref name="condition"/> returns true)
/// </returns>
[PublicAPI]
public static bool SpinUntil(Lifetime lifetime, long timeoutMs, Func<bool> condition)
public static bool SpinUntil<TState>(
Lifetime lifetime, long timeoutMs, TState state, [InstantHandle, RequireStaticDelegate] Func<TState, bool> condition)
{
#if !NET35
var s = new SpinWait();
Expand All @@ -84,9 +107,9 @@ public static bool SpinUntil(Lifetime lifetime, long timeoutMs, Func<bool> condi
if (!lifetime.IsAlive)
return false;

if (condition())
if (condition(state))
return true;

if (Environment.TickCount - start > timeoutMs)
return false;

Expand All @@ -95,27 +118,31 @@ public static bool SpinUntil(Lifetime lifetime, long timeoutMs, Func<bool> condi
#else
Thread.Sleep(0);
#endif
}
}
}


#if !NET35
/// <summary>
/// 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.
/// 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.
/// Only <paramref name="lifetime"/> cancellation could immediately return execution from delay.
/// 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.
/// 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.
/// Only <paramref name="lifetime"/> cancellation could immediately return execution from delay.
/// </summary>
/// <param name="lifetime">Stops spinning and return <c>false</c> when lifetime is no more alive</param>
/// <param name="timeoutMs">Stops spinning and return <c>false</c> when timeout is alive</param>
/// <param name="delayBetweenChecksMs">Interval to delay</param>
/// <param name="condition">Stops spinning and return <c>false</c> when condition is true</param>
/// <returns><c>false</c> if <paramref name="lifetime"/> is not alive or canceled during spinning, <paramref name="timeoutMs"/> is zero or elapsed during spinning.
/// Otherwise <c>true</c> (when <paramref name="condition"/> returns true)</returns>
/// <returns>
/// <c>false</c> if <paramref name="lifetime"/> is not alive or canceled during spinning, <paramref name="timeoutMs"/> is zero
/// or elapsed during spinning. Otherwise, <c>true</c> (when <paramref name="condition"/> returns true)
/// </returns>
[PublicAPI]
public static async System.Threading.Tasks.Task<bool> SpinUntilAsync(Lifetime lifetime, long timeoutMs, int delayBetweenChecksMs, Func<bool> condition)
public static async System.Threading.Tasks.Task<bool> SpinUntilAsync(
Lifetime lifetime, long timeoutMs, int delayBetweenChecksMs, [InstantHandle(RequireAwait = true)] Func<bool> condition)
{
var start = Environment.TickCount;

while (true)
{
if (!lifetime.IsAlive || Environment.TickCount - start > timeoutMs)
Expand Down

0 comments on commit 3f609d1

Please sign in to comment.