Skip to content

Commit

Permalink
Add Lock (#230)
Browse files Browse the repository at this point in the history
  • Loading branch information
SimonCropp authored Sep 28, 2024
1 parent b7c1d4e commit 6dd6991
Show file tree
Hide file tree
Showing 7 changed files with 293 additions and 2 deletions.
2 changes: 1 addition & 1 deletion apiCount.include.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
**API count: 345**
**API count: 352**
11 changes: 11 additions & 0 deletions api_list.include.md
Original file line number Diff line number Diff line change
Expand Up @@ -568,4 +568,15 @@
* `void NotWhiteSpace(Span<Char>)`


#### Lock

* `void Enter()`
* `Lock/Scope EnterScope()`
* `void Exit()`
* `Boolean get_IsHeldByCurrentThread()`
* `Boolean TryEnter()`
* `Boolean TryEnter(TimeSpan)`
* `Boolean TryEnter(Int32)`


#### TaskCompletionSource
13 changes: 12 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ The package targets `netstandard2.0` and is designed to support the following ru
* `net5.0`, `net6.0`, `net7.0`, `net8.0`, `net9.0`


**API count: 345**<!-- singleLineInclude: apiCount. path: /apiCount.include.md -->
**API count: 352**<!-- singleLineInclude: apiCount. path: /apiCount.include.md -->


**See [Milestones](../../milestones?state=closed) for release notes.**
Expand Down Expand Up @@ -1021,6 +1021,17 @@ The class `Polyfill` includes the following extension methods:
* `void NotWhiteSpace(Span<Char>)`


#### Lock

* `void Enter()`
* `Lock/Scope EnterScope()`
* `void Exit()`
* `Boolean get_IsHeldByCurrentThread()`
* `Boolean TryEnter()`
* `Boolean TryEnter(TimeSpan)`
* `Boolean TryEnter(Int32)`


#### TaskCompletionSource<!-- endInclude -->


Expand Down
7 changes: 7 additions & 0 deletions src/Consume/Consume.cs
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,13 @@ void Dictionary_Methods()
dictionary.TryAdd("key", "value");
}

void Lock_Methods()
{
var locker = new Lock();
var held = locker.IsHeldByCurrentThread;
locker.Enter();
}

void Double_Methods()
{
DoublePolyfill.TryParse("1", null, out _);
Expand Down
182 changes: 182 additions & 0 deletions src/Polyfill/Lock.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
// <auto-generated />
#pragma warning disable

#nullable enable

#if !NET9_0_OR_GREATER

namespace System.Threading;

using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Link = System.ComponentModel.DescriptionAttribute;

/// <summary>
/// Provides a way to get mutual exclusion in regions of code between different threads. A lock may be held by one thread at
/// a time.
/// </summary>
/// <remarks>
/// Threads that cannot immediately enter the lock may wait for the lock to be exited or until a specified timeout. A thread
/// that holds a lock may enter the lock repeatedly without exiting it, such as recursively, in which case the thread should
/// eventually exit the lock the same number of times to fully exit the lock and allow other threads to enter the lock.
/// </remarks>
[ExcludeFromCodeCoverage]
[DebuggerNonUserCode]
[Link("https://learn.microsoft.com/en-us/dotnet/api/system.threading.lock")]
#if PolyPublic
public
#endif
class Lock
{
#if (NETCOREAPP) || (NETFRAMEWORK && NET45_OR_GREATER) || (NETSTANDARD)
public bool IsHeldByCurrentThread => Monitor.IsEntered(this);
#endif

/// <summary>
/// Enters the lock. Once the method returns, the calling thread would be the only thread that holds the lock.
/// </summary>
/// <remarks>
/// If the lock cannot be entered immediately, the calling thread waits for the lock to be exited. If the lock is
/// already held by the calling thread, the lock is entered again. The calling thread should exit the lock as many times
/// as it had entered the lock to fully exit the lock and allow other threads to enter the lock.
/// </remarks>
/// <exception cref="LockRecursionException">
/// The lock has reached the limit of recursive enters. The limit is implementation-defined, but is expected to be high
/// enough that it would typically not be reached when the lock is used properly.
/// </exception>
public void Enter() => Monitor.Enter(this);

/// <summary>
/// Tries to enter the lock without waiting. If the lock is entered, the calling thread would be the only thread that
/// holds the lock.
/// </summary>
/// <returns>
/// <code>true</code> if the lock was entered, <code>false</code> otherwise.
/// </returns>
/// <remarks>
/// If the lock cannot be entered immediately, the method returns <code>false</code>. If the lock is already held by the
/// calling thread, the lock is entered again. The calling thread should exit the lock as many times as it had entered
/// the lock to fully exit the lock and allow other threads to enter the lock.
/// </remarks>
/// <exception cref="LockRecursionException">
/// The lock has reached the limit of recursive enters. The limit is implementation-defined, but is expected to be high
/// enough that it would typically not be reached when the lock is used properly.
/// </exception>
public bool TryEnter() => Monitor.TryEnter(this);

/// <summary>
/// Tries to enter the lock, waiting for roughly the specified duration. If the lock is entered, the calling thread
/// would be the only thread that holds the lock.
/// </summary>
/// <param name="timeout">
/// The rough duration for which the method will wait if the lock is not available. The timeout is converted to a number
/// of milliseconds by casting <see cref="TimeSpan.TotalMilliseconds"/> of the timeout to an integer value. A value
/// representing <code>0</code> milliseconds specifies that the method should not wait, and a value representing
/// <see cref="Timeout.Infinite"/> or <code>-1</code> milliseconds specifies that the method should wait indefinitely
/// until the lock is entered.
/// </param>
/// <returns>
/// <code>true</code> if the lock was entered, <code>false</code> otherwise.
/// </returns>
/// <remarks>
/// If the lock cannot be entered immediately, the calling thread waits for roughly the specified duration for the lock
/// to be exited. If the lock is already held by the calling thread, the lock is entered again. The calling thread
/// should exit the lock as many times as it had entered the lock to fully exit the lock and allow other threads to
/// enter the lock.
/// </remarks>
/// <exception cref="ArgumentOutOfRangeException">
/// <paramref name="timeout"/>, after its conversion to an integer millisecond value, represents a value that is less
/// than <code>-1</code> milliseconds or greater than <see cref="int.MaxValue"/> milliseconds.
/// </exception>
/// <exception cref="LockRecursionException">
/// The lock has reached the limit of recursive enters. The limit is implementation-defined, but is expected to be high
/// enough that it would typically not be reached when the lock is used properly.
/// </exception>
public bool TryEnter(TimeSpan timeout) =>
Monitor.TryEnter(this, timeout);

/// <summary>
/// Tries to enter the lock, waiting for roughly the specified duration. If the lock is entered, the calling thread
/// would be the only thread that holds the lock.
/// </summary>
/// <param name="millisecondsTimeout">
/// The rough duration in milliseconds for which the method will wait if the lock is not available. A value of
/// <code>0</code> specifies that the method should not wait, and a value of <see cref="Timeout.Infinite"/> or
/// <code>-1</code> specifies that the method should wait indefinitely until the lock is entered.
/// </param>
/// <returns>
/// <code>true</code> if the lock was entered, <code>false</code> otherwise.
/// </returns>
/// <remarks>
/// If the lock cannot be entered immediately, the calling thread waits for roughly the specified duration for the lock
/// to be exited. If the lock is already held by the calling thread, the lock is entered again. The calling thread
/// should exit the lock as many times as it had entered the lock to fully exit the lock and allow other threads to
/// enter the lock.
/// </remarks>
/// <exception cref="ArgumentOutOfRangeException">
/// <paramref name="millisecondsTimeout"/> is less than <code>-1</code>.
/// </exception>
/// <exception cref="LockRecursionException">
/// The lock has reached the limit of recursive enters. The limit is implementation-defined, but is expected to be high
/// enough that it would typically not be reached when the lock is used properly.
/// </exception>
public bool TryEnter(int millisecondsTimeout) =>
TryEnter(TimeSpan.FromMilliseconds(millisecondsTimeout));

/// <summary>
/// Exits the lock.
/// </summary>
/// <remarks>
/// If the calling thread holds the lock multiple times, such as recursively, the lock is exited only once. The
/// calling thread should ensure that each enter is matched with an exit.
/// </remarks>
/// <exception cref="SynchronizationLockException">
/// The calling thread does not hold the lock.
/// </exception>
public void Exit() => Monitor.Exit(this);

/// <summary>
/// Enters the lock and returns a <see cref="Scope"/> that may be disposed to exit the lock. Once the method returns,
/// the calling thread would be the only thread that holds the lock. This method is intended to be used along with a
/// language construct that would automatically dispose the <see cref="Scope"/>, such as with the C# <code>using</code>
/// statement.
/// </summary>
/// <returns>
/// A <see cref="Scope"/> that may be disposed to exit the lock.
/// </returns>
/// <remarks>
/// If the lock cannot be entered immediately, the calling thread waits for the lock to be exited. If the lock is
/// already held by the calling thread, the lock is entered again. The calling thread should exit the lock, such as by
/// disposing the returned <see cref="Scope"/>, as many times as it had entered the lock to fully exit the lock and
/// allow other threads to enter the lock.
/// </remarks>
/// <exception cref="LockRecursionException">
/// The lock has reached the limit of recursive enters. The limit is implementation-defined, but is expected to be high
/// enough that it would typically not be reached when the lock is used properly.
/// </exception>
public Scope EnterScope()
{
Enter();
return new Scope(this);
}

/// <summary>
/// A disposable structure that is returned by <see cref="EnterScope()"/>, which when disposed, exits the lock.
/// </summary>
public readonly ref struct Scope(Lock owner)
{
/// <summary>
/// Exits the lock.
/// </summary>
/// <remarks>
/// If the calling thread holds the lock multiple times, such as recursively, the lock is exited only once. The
/// calling thread should ensure that each enter is matched with an exit.
/// </remarks>
/// <exception cref="SynchronizationLockException">
/// The calling thread does not hold the lock.
/// </exception>
public void Dispose() => owner.Exit();
}
}

#endif
1 change: 1 addition & 0 deletions src/Tests/BuildApiTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ public void Run()
WriteHelper(types, "ULongPolyfill", writer, ref count);
WriteHelper(types, "UShortPolyfill", writer, ref count);
WriteHelper(types, "Guard", writer, ref count);
WriteHelper(types, "Lock", writer, ref count);
WriteType(nameof(TaskCompletionSource), writer, ref count);

count += types.Count(_ => _.Key.EndsWith("Attribute"));
Expand Down
79 changes: 79 additions & 0 deletions src/Tests/LockTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
[TestFixture]
public class LockTests
{
[Test]
public void Enter()
{
var locker = new Lock();

Assert.False(locker.IsHeldByCurrentThread);
locker.Enter();
Assert.True(locker.IsHeldByCurrentThread);
}

[Test]
public void TryEnter()
{
var locker = new Lock();

Assert.False(locker.IsHeldByCurrentThread);
Assert.True(locker.TryEnter());
Assert.True(locker.IsHeldByCurrentThread);
}

[Test]
public void TryEnter_Timeout()
{
var locker = new Lock();

Assert.False(locker.IsHeldByCurrentThread);
Assert.True(locker.TryEnter(100));
Assert.True(locker.IsHeldByCurrentThread);
}

[Test]
public void Exit()
{
var locker = new Lock();

Assert.False(locker.IsHeldByCurrentThread);
locker.Enter();
Assert.True(locker.IsHeldByCurrentThread);
locker.Exit();
Assert.False(locker.IsHeldByCurrentThread);
}

[Test]
public void EnterScope()
{
var locker = new Lock();

Assert.False(locker.IsHeldByCurrentThread);
using (locker.EnterScope())
{
Assert.True(locker.IsHeldByCurrentThread);
}

Assert.False(locker.IsHeldByCurrentThread);
}

[Test]
public void EnterScope_Layered()
{
var locker = new Lock();

Assert.False(locker.IsHeldByCurrentThread);
using (locker.EnterScope())
{
Assert.True(locker.IsHeldByCurrentThread);
using (locker.EnterScope())
{
Assert.True(locker.IsHeldByCurrentThread);
}

Assert.True(locker.IsHeldByCurrentThread);
}

Assert.False(locker.IsHeldByCurrentThread);
}
}

0 comments on commit 6dd6991

Please sign in to comment.