From c5fb56e3089dfc0734566395c4aa987ade7e9ec3 Mon Sep 17 00:00:00 2001 From: ArtiomPatov Date: Sun, 25 Sep 2022 17:37:42 +0300 Subject: [PATCH 1/9] Initial commit --- Lazy/Lazy.sln | 22 +++++++ Lazy/Lazy/ILazy.cs | 14 +++++ Lazy/Lazy/Lazy.csproj | 9 +++ Lazy/Lazy/LazyConcurrent.cs | 36 +++++++++++ Lazy/Lazy/LazySerial.cs | 31 ++++++++++ Lazy/LazyTests/LazyTests.cs | 102 ++++++++++++++++++++++++++++++++ Lazy/LazyTests/LazyTests.csproj | 23 +++++++ 7 files changed, 237 insertions(+) create mode 100644 Lazy/Lazy.sln create mode 100644 Lazy/Lazy/ILazy.cs create mode 100644 Lazy/Lazy/Lazy.csproj create mode 100644 Lazy/Lazy/LazyConcurrent.cs create mode 100644 Lazy/Lazy/LazySerial.cs create mode 100644 Lazy/LazyTests/LazyTests.cs create mode 100644 Lazy/LazyTests/LazyTests.csproj diff --git a/Lazy/Lazy.sln b/Lazy/Lazy.sln new file mode 100644 index 0000000..7bf7874 --- /dev/null +++ b/Lazy/Lazy.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lazy", "Lazy\Lazy.csproj", "{93659D18-057E-4FF2-B8C4-25D891741798}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LazyTests", "LazyTests\LazyTests.csproj", "{11D67AE1-D6D2-4898-AA5E-79E50A9737CA}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {93659D18-057E-4FF2-B8C4-25D891741798}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {93659D18-057E-4FF2-B8C4-25D891741798}.Debug|Any CPU.Build.0 = Debug|Any CPU + {93659D18-057E-4FF2-B8C4-25D891741798}.Release|Any CPU.ActiveCfg = Release|Any CPU + {93659D18-057E-4FF2-B8C4-25D891741798}.Release|Any CPU.Build.0 = Release|Any CPU + {11D67AE1-D6D2-4898-AA5E-79E50A9737CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {11D67AE1-D6D2-4898-AA5E-79E50A9737CA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {11D67AE1-D6D2-4898-AA5E-79E50A9737CA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {11D67AE1-D6D2-4898-AA5E-79E50A9737CA}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Lazy/Lazy/ILazy.cs b/Lazy/Lazy/ILazy.cs new file mode 100644 index 0000000..48a04c2 --- /dev/null +++ b/Lazy/Lazy/ILazy.cs @@ -0,0 +1,14 @@ +namespace Lazy; + +/// +/// Defers the execution of given function. Function executes only once and only when Get() is called. +/// +/// Return type of a function. +public interface ILazy +{ + /// + /// Executes function and return the result if Get() is called for the first time. If Get() is called not for the first time, it just returns the result of the first execution. + /// + /// The result of the first function execution. + T? Get(); +} \ No newline at end of file diff --git a/Lazy/Lazy/Lazy.csproj b/Lazy/Lazy/Lazy.csproj new file mode 100644 index 0000000..eb2460e --- /dev/null +++ b/Lazy/Lazy/Lazy.csproj @@ -0,0 +1,9 @@ + + + + net6.0 + enable + enable + + + diff --git a/Lazy/Lazy/LazyConcurrent.cs b/Lazy/Lazy/LazyConcurrent.cs new file mode 100644 index 0000000..cb7292c --- /dev/null +++ b/Lazy/Lazy/LazyConcurrent.cs @@ -0,0 +1,36 @@ +namespace Lazy; + +/// +/// Lazy that can be used concurrently by multiple threads. It guarantees that there won't be any deadlocks or races. +/// +/// Return type of a function. +public class LazyConcurrent : ILazy +{ + public LazyConcurrent(Func func) + { + _func = func; + } + + private bool _isCalculated = false; + + private object _locker = new(); + + private Func _func; + + private T? _result; + + public T? Get() + { + lock (_locker) + { + if (_isCalculated) + { + return _result; + } + + _result = _func(); + _isCalculated = true; + return _result; + } + } +} \ No newline at end of file diff --git a/Lazy/Lazy/LazySerial.cs b/Lazy/Lazy/LazySerial.cs new file mode 100644 index 0000000..572bbe3 --- /dev/null +++ b/Lazy/Lazy/LazySerial.cs @@ -0,0 +1,31 @@ +namespace Lazy; + +/// +/// Lazy that does not support multithreading. +/// +/// Return type of a function. +public class LazySerial : ILazy +{ + public LazySerial(Func func) + { + _func = func; + } + + private bool _isCalculated = false; + + private readonly Func _func; + + private T? _result; + + public T? Get() + { + if (_isCalculated) + { + return _result; + } + + _result = _func(); + _isCalculated = true; + return _result; + } +} \ No newline at end of file diff --git a/Lazy/LazyTests/LazyTests.cs b/Lazy/LazyTests/LazyTests.cs new file mode 100644 index 0000000..aad5e6b --- /dev/null +++ b/Lazy/LazyTests/LazyTests.cs @@ -0,0 +1,102 @@ +namespace LazyTest; + +using NUnit.Framework; +using System.Threading; +using Lazy; + +public class Tests +{ + [Test] + public void SerialLazyExecutesFunctionOnlyOnce() + { + var counter = 0; + ILazy lazy = new LazySerial(() => ++counter); + for (var i = 0; i < 10; i++) + { + Assert.AreEqual(1, lazy.Get()); + } + + Assert.AreEqual(1, counter); + } + + [Test] + public void ConcurrentLazyExecutesFunctionOnlyOnce() + { + var counter = 0; + var threads = new Thread[1000]; + ILazy lazy = new LazyConcurrent(() => Interlocked.Increment(ref counter)); + + for (var i = 0; i < 1000; i++) + { + threads[i] = new Thread(() => + { + for (var _ = 0; _ < 1000; _++) + { + Assert.AreEqual(1, lazy.Get()); + } + }); + } + + foreach (var thread in threads) + { + thread.Start(); + } + + foreach (var thread in threads) + { + thread.Join(); + } + + Assert.AreEqual(1, counter); + } + + [Test] + public void LaziesCanReturnNull() + { + ILazy lazySerial = new LazySerial(() => null); + ILazy lazyConcurrent = new LazyConcurrent(() => null); + + for (var i = 0; i < 5; i++) + { + Assert.IsNull(lazySerial.Get()); + Assert.IsNull(lazyConcurrent.Get()); + } + } + + [Test] + public void MultithreadingDoesNotCauseRaceCondition() + { + var counter = 0; + var threads = new Thread[1000]; + ILazy lazy = new LazyConcurrent(() => + { + for (var j = 0; j < 1000; j++) + { + Interlocked.Increment(ref counter); + } + + return counter; + }); + + for (var i = 0; i < 1000; i++) + { + threads[i] = new Thread(() => + { + for (var _ = 0; _ < 1000; _++) + { + Assert.AreEqual(1000, lazy.Get()); + } + }); + } + + foreach (var thread in threads) + { + thread.Start(); + } + + foreach (var thread in threads) + { + thread.Join(); + } + } +} \ No newline at end of file diff --git a/Lazy/LazyTests/LazyTests.csproj b/Lazy/LazyTests/LazyTests.csproj new file mode 100644 index 0000000..eb49096 --- /dev/null +++ b/Lazy/LazyTests/LazyTests.csproj @@ -0,0 +1,23 @@ + + + + net6.0 + enable + + false + + LazyTest + + + + + + + + + + + + + + From dc68035d4a56413f4519846a7c064ba73ce5878f Mon Sep 17 00:00:00 2001 From: ArtiomPatov Date: Mon, 26 Sep 2022 14:50:24 +0300 Subject: [PATCH 2/9] Refactoring --- Lazy/Lazy/LazyConcurrent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lazy/Lazy/LazyConcurrent.cs b/Lazy/Lazy/LazyConcurrent.cs index cb7292c..92fe001 100644 --- a/Lazy/Lazy/LazyConcurrent.cs +++ b/Lazy/Lazy/LazyConcurrent.cs @@ -11,7 +11,7 @@ public LazyConcurrent(Func func) _func = func; } - private bool _isCalculated = false; + private bool _isCalculated; private object _locker = new(); From f32c4ca5d8eb70e55d045e207a01f3ece3983b67 Mon Sep 17 00:00:00 2001 From: ArtiomPatov Date: Tue, 11 Oct 2022 00:34:51 +0300 Subject: [PATCH 3/9] Refactor --- Lazy/Lazy/Lazy.csproj | 7 +++++++ Lazy/Lazy/LazyConcurrent.cs | 23 ++++++++++++++--------- Lazy/Lazy/LazySerial.cs | 17 +++++++++++------ Lazy/LazyTests/LazyTests.cs | 16 ++++++++-------- Lazy/LazyTests/LazyTests.csproj | 4 ++++ 5 files changed, 44 insertions(+), 23 deletions(-) diff --git a/Lazy/Lazy/Lazy.csproj b/Lazy/Lazy/Lazy.csproj index eb2460e..226575e 100644 --- a/Lazy/Lazy/Lazy.csproj +++ b/Lazy/Lazy/Lazy.csproj @@ -6,4 +6,11 @@ enable + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/Lazy/Lazy/LazyConcurrent.cs b/Lazy/Lazy/LazyConcurrent.cs index 92fe001..8c009f8 100644 --- a/Lazy/Lazy/LazyConcurrent.cs +++ b/Lazy/Lazy/LazyConcurrent.cs @@ -1,24 +1,29 @@ namespace Lazy; /// -/// Lazy that can be used concurrently by multiple threads. It guarantees that there won't be any deadlocks or races. +/// Lazy that can be used safely by multiple threads. It guarantees that there won't be any deadlocks or races. /// /// Return type of a function. public class LazyConcurrent : ILazy { - public LazyConcurrent(Func func) - { - _func = func; - } - private bool _isCalculated; - + private object _locker = new(); - + private Func _func; private T? _result; - + + /// + /// Initializes a new instance of the class. + /// + /// The delegate to be executed lazily. + public LazyConcurrent(Func func) + { + _func = func; + } + + /// public T? Get() { lock (_locker) diff --git a/Lazy/Lazy/LazySerial.cs b/Lazy/Lazy/LazySerial.cs index 572bbe3..4fdf191 100644 --- a/Lazy/Lazy/LazySerial.cs +++ b/Lazy/Lazy/LazySerial.cs @@ -6,17 +6,22 @@ /// Return type of a function. public class LazySerial : ILazy { - public LazySerial(Func func) - { - _func = func; - } - private bool _isCalculated = false; - + private readonly Func _func; private T? _result; + /// + /// Initializes a new instance of the class. + /// + /// The delegate to be executed lazily. + public LazySerial(Func func) + { + _func = func; + } + + /// public T? Get() { if (_isCalculated) diff --git a/Lazy/LazyTests/LazyTests.cs b/Lazy/LazyTests/LazyTests.cs index aad5e6b..6bdc088 100644 --- a/Lazy/LazyTests/LazyTests.cs +++ b/Lazy/LazyTests/LazyTests.cs @@ -1,8 +1,8 @@ namespace LazyTest; -using NUnit.Framework; using System.Threading; using Lazy; +using NUnit.Framework; public class Tests { @@ -18,14 +18,14 @@ public void SerialLazyExecutesFunctionOnlyOnce() Assert.AreEqual(1, counter); } - + [Test] public void ConcurrentLazyExecutesFunctionOnlyOnce() { var counter = 0; var threads = new Thread[1000]; ILazy lazy = new LazyConcurrent(() => Interlocked.Increment(ref counter)); - + for (var i = 0; i < 1000; i++) { threads[i] = new Thread(() => @@ -41,12 +41,12 @@ public void ConcurrentLazyExecutesFunctionOnlyOnce() { thread.Start(); } - + foreach (var thread in threads) { thread.Join(); } - + Assert.AreEqual(1, counter); } @@ -55,7 +55,7 @@ public void LaziesCanReturnNull() { ILazy lazySerial = new LazySerial(() => null); ILazy lazyConcurrent = new LazyConcurrent(() => null); - + for (var i = 0; i < 5; i++) { Assert.IsNull(lazySerial.Get()); @@ -77,7 +77,7 @@ public void MultithreadingDoesNotCauseRaceCondition() return counter; }); - + for (var i = 0; i < 1000; i++) { threads[i] = new Thread(() => @@ -93,7 +93,7 @@ public void MultithreadingDoesNotCauseRaceCondition() { thread.Start(); } - + foreach (var thread in threads) { thread.Join(); diff --git a/Lazy/LazyTests/LazyTests.csproj b/Lazy/LazyTests/LazyTests.csproj index eb49096..f4144df 100644 --- a/Lazy/LazyTests/LazyTests.csproj +++ b/Lazy/LazyTests/LazyTests.csproj @@ -14,6 +14,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + From 2a30ac6615318cbeb3c455aa33b410d1b445fdb2 Mon Sep 17 00:00:00 2001 From: ArtiomPatov Date: Mon, 31 Oct 2022 22:05:36 +0300 Subject: [PATCH 4/9] Refactor --- Lazy/Lazy/LazyConcurrent.cs | 4 ++-- Lazy/Lazy/LazySerial.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Lazy/Lazy/LazyConcurrent.cs b/Lazy/Lazy/LazyConcurrent.cs index 8c009f8..41291ac 100644 --- a/Lazy/Lazy/LazyConcurrent.cs +++ b/Lazy/Lazy/LazyConcurrent.cs @@ -6,9 +6,9 @@ /// Return type of a function. public class LazyConcurrent : ILazy { - private bool _isCalculated; + private readonly object _locker = new(); - private object _locker = new(); + private bool _isCalculated; private Func _func; diff --git a/Lazy/Lazy/LazySerial.cs b/Lazy/Lazy/LazySerial.cs index 4fdf191..216dbb3 100644 --- a/Lazy/Lazy/LazySerial.cs +++ b/Lazy/Lazy/LazySerial.cs @@ -6,10 +6,10 @@ /// Return type of a function. public class LazySerial : ILazy { - private bool _isCalculated = false; - private readonly Func _func; + private bool _isCalculated = false; + private T? _result; /// From 792ba6d205125590e6d4f86916f5e373580d0543 Mon Sep 17 00:00:00 2001 From: ArtiomPatov Date: Mon, 31 Oct 2022 22:30:34 +0300 Subject: [PATCH 5/9] Refactor --- Lazy/LazyTests/LazyTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lazy/LazyTests/LazyTests.cs b/Lazy/LazyTests/LazyTests.cs index 6bdc088..f98a032 100644 --- a/Lazy/LazyTests/LazyTests.cs +++ b/Lazy/LazyTests/LazyTests.cs @@ -4,7 +4,7 @@ namespace LazyTest; using Lazy; using NUnit.Framework; -public class Tests +public class LazyTests { [Test] public void SerialLazyExecutesFunctionOnlyOnce() From 8bb25c5d2b60e49c1aad7a80e595b1fb9ea81108 Mon Sep 17 00:00:00 2001 From: ArtiomPatov Date: Wed, 7 Dec 2022 12:29:44 +0300 Subject: [PATCH 6/9] fix Get() in concurrent lazy --- Lazy/Lazy/LazyConcurrent.cs | 10 ++++++++-- Lazy/Lazy/LazySerial.cs | 3 ++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Lazy/Lazy/LazyConcurrent.cs b/Lazy/Lazy/LazyConcurrent.cs index 41291ac..dfb95ac 100644 --- a/Lazy/Lazy/LazyConcurrent.cs +++ b/Lazy/Lazy/LazyConcurrent.cs @@ -10,7 +10,7 @@ public class LazyConcurrent : ILazy private bool _isCalculated; - private Func _func; + private Func? _func; private T? _result; @@ -26,6 +26,11 @@ public LazyConcurrent(Func func) /// public T? Get() { + if (_isCalculated) + { + return _result; + } + lock (_locker) { if (_isCalculated) @@ -33,7 +38,8 @@ public LazyConcurrent(Func func) return _result; } - _result = _func(); + _result = _func!(); // It cannot be null because argument is not nullable. + _func = null; _isCalculated = true; return _result; } diff --git a/Lazy/Lazy/LazySerial.cs b/Lazy/Lazy/LazySerial.cs index 216dbb3..ae77e20 100644 --- a/Lazy/Lazy/LazySerial.cs +++ b/Lazy/Lazy/LazySerial.cs @@ -6,7 +6,7 @@ /// Return type of a function. public class LazySerial : ILazy { - private readonly Func _func; + private Func? _func; private bool _isCalculated = false; @@ -30,6 +30,7 @@ public LazySerial(Func func) } _result = _func(); + _func = null; _isCalculated = true; return _result; } From c49c19c7cfa2a858358d8d6c94c597cd134d4b57 Mon Sep 17 00:00:00 2001 From: ArtiomPatov Date: Tue, 27 Dec 2022 12:02:23 +0300 Subject: [PATCH 7/9] migrate to .NET 7.0 --- Lazy/Lazy/Lazy.csproj | 3 ++- Lazy/LazyTests/LazyTests.csproj | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Lazy/Lazy/Lazy.csproj b/Lazy/Lazy/Lazy.csproj index 226575e..a03f1ef 100644 --- a/Lazy/Lazy/Lazy.csproj +++ b/Lazy/Lazy/Lazy.csproj @@ -1,9 +1,10 @@ - net6.0 + net7.0 enable enable + 11 diff --git a/Lazy/LazyTests/LazyTests.csproj b/Lazy/LazyTests/LazyTests.csproj index f4144df..7a0f724 100644 --- a/Lazy/LazyTests/LazyTests.csproj +++ b/Lazy/LazyTests/LazyTests.csproj @@ -1,12 +1,14 @@ - net6.0 + net7.0 enable false LazyTest + + 11 From 049d3fcbf0b53856c9b3b569d3284a80e38fc7f8 Mon Sep 17 00:00:00 2001 From: ArtiomPatov Date: Tue, 27 Dec 2022 12:33:47 +0300 Subject: [PATCH 8/9] refactor; update ci --- .github/workflows/ci.yml | 2 +- Lazy/Lazy/LazyConcurrent.cs | 13 +++++++------ Lazy/Lazy/LazySerial.cs | 4 ++-- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b8eaa4e..e3c033d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-dotnet@v1 with: - dotnet-version: '6.x' + dotnet-version: '7.x' - name: Build run: for dir in */; do cd $dir; for sln in *.sln; do dotnet build $sln; cd ..; done; done shell: bash diff --git a/Lazy/Lazy/LazyConcurrent.cs b/Lazy/Lazy/LazyConcurrent.cs index dfb95ac..7777fb4 100644 --- a/Lazy/Lazy/LazyConcurrent.cs +++ b/Lazy/Lazy/LazyConcurrent.cs @@ -3,28 +3,28 @@ /// /// Lazy that can be used safely by multiple threads. It guarantees that there won't be any deadlocks or races. /// -/// Return type of a function. -public class LazyConcurrent : ILazy +/// Return type of a function. +public class LazyConcurrent : ILazy { private readonly object _locker = new(); private bool _isCalculated; - private Func? _func; + private Func? _func; - private T? _result; + private TResult? _result; /// /// Initializes a new instance of the class. /// /// The delegate to be executed lazily. - public LazyConcurrent(Func func) + public LazyConcurrent(Func func) { _func = func; } /// - public T? Get() + public TResult? Get() { if (_isCalculated) { @@ -41,6 +41,7 @@ public LazyConcurrent(Func func) _result = _func!(); // It cannot be null because argument is not nullable. _func = null; _isCalculated = true; + return _result; } } diff --git a/Lazy/Lazy/LazySerial.cs b/Lazy/Lazy/LazySerial.cs index ae77e20..f13401d 100644 --- a/Lazy/Lazy/LazySerial.cs +++ b/Lazy/Lazy/LazySerial.cs @@ -8,7 +8,7 @@ public class LazySerial : ILazy { private Func? _func; - private bool _isCalculated = false; + private bool _isCalculated; private T? _result; @@ -29,7 +29,7 @@ public LazySerial(Func func) return _result; } - _result = _func(); + _result = _func!(); // It cannot be null because argument is not nullable. _func = null; _isCalculated = true; return _result; From 17e96f162a617202bb796e9533e999c7856e24cc Mon Sep 17 00:00:00 2001 From: ArtiomPatov Date: Tue, 27 Dec 2022 17:33:28 +0300 Subject: [PATCH 9/9] make iscalculated field volatile --- Lazy/Lazy/LazyConcurrent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lazy/Lazy/LazyConcurrent.cs b/Lazy/Lazy/LazyConcurrent.cs index 7777fb4..b9dbc38 100644 --- a/Lazy/Lazy/LazyConcurrent.cs +++ b/Lazy/Lazy/LazyConcurrent.cs @@ -8,7 +8,7 @@ public class LazyConcurrent : ILazy { private readonly object _locker = new(); - private bool _isCalculated; + private volatile bool _isCalculated; private Func? _func;