diff --git a/apiCount.include.md b/apiCount.include.md index ae3dc8a..acd1420 100644 --- a/apiCount.include.md +++ b/apiCount.include.md @@ -1 +1 @@ -**API count: 345** \ No newline at end of file +**API count: 352** \ No newline at end of file diff --git a/api_list.include.md b/api_list.include.md index 1b4cc85..8119c03 100644 --- a/api_list.include.md +++ b/api_list.include.md @@ -568,4 +568,15 @@ * `void NotWhiteSpace(Span)` +#### Lock + + * `void Enter()` + * `Lock/Scope EnterScope()` + * `void Exit()` + * `Boolean get_IsHeldByCurrentThread()` + * `Boolean TryEnter()` + * `Boolean TryEnter(TimeSpan)` + * `Boolean TryEnter(Int32)` + + #### TaskCompletionSource diff --git a/readme.md b/readme.md index e9edc7b..af2c2e6 100644 --- a/readme.md +++ b/readme.md @@ -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** +**API count: 352** **See [Milestones](../../milestones?state=closed) for release notes.** @@ -1021,6 +1021,17 @@ The class `Polyfill` includes the following extension methods: * `void NotWhiteSpace(Span)` +#### Lock + + * `void Enter()` + * `Lock/Scope EnterScope()` + * `void Exit()` + * `Boolean get_IsHeldByCurrentThread()` + * `Boolean TryEnter()` + * `Boolean TryEnter(TimeSpan)` + * `Boolean TryEnter(Int32)` + + #### TaskCompletionSource diff --git a/src/Consume/Consume.cs b/src/Consume/Consume.cs index 1317803..4630501 100644 --- a/src/Consume/Consume.cs +++ b/src/Consume/Consume.cs @@ -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 _); diff --git a/src/Polyfill/Lock.cs b/src/Polyfill/Lock.cs new file mode 100644 index 0000000..31addb5 --- /dev/null +++ b/src/Polyfill/Lock.cs @@ -0,0 +1,182 @@ +// +#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; + +/// +/// 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. +/// +/// +/// 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. +/// +[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 + + /// + /// Enters the lock. Once the method returns, the calling thread would be the only thread that holds the lock. + /// + /// + /// 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. + /// + /// + /// 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. + /// + public void Enter() => Monitor.Enter(this); + + /// + /// Tries to enter the lock without waiting. If the lock is entered, the calling thread would be the only thread that + /// holds the lock. + /// + /// + /// true if the lock was entered, false otherwise. + /// + /// + /// If the lock cannot be entered immediately, the method returns false. 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. + /// + /// + /// 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. + /// + public bool TryEnter() => Monitor.TryEnter(this); + + /// + /// 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. + /// + /// + /// 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 of the timeout to an integer value. A value + /// representing 0 milliseconds specifies that the method should not wait, and a value representing + /// or -1 milliseconds specifies that the method should wait indefinitely + /// until the lock is entered. + /// + /// + /// true if the lock was entered, false otherwise. + /// + /// + /// 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. + /// + /// + /// , after its conversion to an integer millisecond value, represents a value that is less + /// than -1 milliseconds or greater than milliseconds. + /// + /// + /// 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. + /// + public bool TryEnter(TimeSpan timeout) => + Monitor.TryEnter(this, timeout); + + /// + /// 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. + /// + /// + /// The rough duration in milliseconds for which the method will wait if the lock is not available. A value of + /// 0 specifies that the method should not wait, and a value of or + /// -1 specifies that the method should wait indefinitely until the lock is entered. + /// + /// + /// true if the lock was entered, false otherwise. + /// + /// + /// 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. + /// + /// + /// is less than -1. + /// + /// + /// 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. + /// + public bool TryEnter(int millisecondsTimeout) => + TryEnter(TimeSpan.FromMilliseconds(millisecondsTimeout)); + + /// + /// Exits the lock. + /// + /// + /// 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. + /// + /// + /// The calling thread does not hold the lock. + /// + public void Exit() => Monitor.Exit(this); + + /// + /// Enters the lock and returns a 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 , such as with the C# using + /// statement. + /// + /// + /// A that may be disposed to exit the lock. + /// + /// + /// 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 , as many times as it had entered the lock to fully exit the lock and + /// allow other threads to enter the lock. + /// + /// + /// 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. + /// + public Scope EnterScope() + { + Enter(); + return new Scope(this); + } + + /// + /// A disposable structure that is returned by , which when disposed, exits the lock. + /// + public readonly ref struct Scope(Lock owner) + { + /// + /// Exits the lock. + /// + /// + /// 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. + /// + /// + /// The calling thread does not hold the lock. + /// + public void Dispose() => owner.Exit(); + } +} + +#endif \ No newline at end of file diff --git a/src/Tests/BuildApiTest.cs b/src/Tests/BuildApiTest.cs index e051e62..e81076c 100644 --- a/src/Tests/BuildApiTest.cs +++ b/src/Tests/BuildApiTest.cs @@ -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")); diff --git a/src/Tests/LockTests.cs b/src/Tests/LockTests.cs new file mode 100644 index 0000000..34d3078 --- /dev/null +++ b/src/Tests/LockTests.cs @@ -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); + } +} \ No newline at end of file