From 0f23114d2f2b1f5abad0abd35b7ac78f10c6354e Mon Sep 17 00:00:00 2001 From: Andrew Kalinin Date: Mon, 13 Oct 2025 20:51:21 +0300 Subject: [PATCH 01/14] add: an unfinished solution --- MyThreadPoolTask/MyThreadPoolTask.sln | 25 +++ MyThreadPoolTask/MyThreadPoolTask/IMyTask.cs | 17 ++ .../MyThreadPoolTask/MyThreadPool.cs | 192 ++++++++++++++++++ .../MyThreadPoolTask/MyThreadPoolTask.csproj | 10 + MyThreadPoolTask/MyThreadPoolTask/Program.cs | 1 + 5 files changed, 245 insertions(+) create mode 100644 MyThreadPoolTask/MyThreadPoolTask.sln create mode 100644 MyThreadPoolTask/MyThreadPoolTask/IMyTask.cs create mode 100644 MyThreadPoolTask/MyThreadPoolTask/MyThreadPool.cs create mode 100644 MyThreadPoolTask/MyThreadPoolTask/MyThreadPoolTask.csproj create mode 100644 MyThreadPoolTask/MyThreadPoolTask/Program.cs diff --git a/MyThreadPoolTask/MyThreadPoolTask.sln b/MyThreadPoolTask/MyThreadPoolTask.sln new file mode 100644 index 0000000..eeab820 --- /dev/null +++ b/MyThreadPoolTask/MyThreadPoolTask.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.13.35806.99 d17.13 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyThreadPoolTask", "MyThreadPoolTask\MyThreadPoolTask.csproj", "{A7D8D770-5056-44C2-98D0-5973DBA29CA9}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A7D8D770-5056-44C2-98D0-5973DBA29CA9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A7D8D770-5056-44C2-98D0-5973DBA29CA9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A7D8D770-5056-44C2-98D0-5973DBA29CA9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A7D8D770-5056-44C2-98D0-5973DBA29CA9}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {CBA5D183-FDFD-4BAB-8782-23CE4B80DF14} + EndGlobalSection +EndGlobal diff --git a/MyThreadPoolTask/MyThreadPoolTask/IMyTask.cs b/MyThreadPoolTask/MyThreadPoolTask/IMyTask.cs new file mode 100644 index 0000000..b38c4d7 --- /dev/null +++ b/MyThreadPoolTask/MyThreadPoolTask/IMyTask.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MyThreadPoolTask +{ + public interface IMyTask + { + bool IsCompleted { get; } + + TResult Result { get; } + + IMyTask ContinueWith(Func next); + } +} diff --git a/MyThreadPoolTask/MyThreadPoolTask/MyThreadPool.cs b/MyThreadPoolTask/MyThreadPoolTask/MyThreadPool.cs new file mode 100644 index 0000000..efb785f --- /dev/null +++ b/MyThreadPoolTask/MyThreadPoolTask/MyThreadPool.cs @@ -0,0 +1,192 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MyThreadPoolTask +{ + public class MyThreadPool + { + private readonly Thread[] workers; + + private readonly Queue taskQueue = new(); + + private readonly object queueLock = new(); + + private readonly CancellationTokenSource cancellationTokenSource = new(); + + + public MyThreadPool(int threadCount) + { + if (threadCount <= 0) + { + throw new ArgumentOutOfRangeException($"Количество потоков {threadCount} должно быть больше нуля"); + } + + this.workers = new Thread[threadCount]; + for (int i = 0; i < threadCount; ++i) + { + this.workers[i] = new Thread(WorkerLoop) + { + IsBackground = true + }; + + workers[i].Start(); + } + } + + private void WorkerLoop() + { + while (!this.cancellationTokenSource.IsCancellationRequested) + { + Action? taskAction = null; + lock (this.queueLock) + { + while (this.taskQueue.Count == 0) + { + if (this.cancellationTokenSource.IsCancellationRequested) + { + return; + } + + Monitor.Wait(this.queueLock); + + if (this.cancellationTokenSource.IsCancellationRequested && this.taskQueue.Count == 0) + { + return; + } + } + + taskAction = this.taskQueue.Dequeue(); + } + + taskAction?.Invoke(); + } + } + + private void EnqueueAction(Action action) + { + lock (this.queueLock) + { + if (this.cancellationTokenSource.IsCancellationRequested) + { + throw new InvalidOperationException(); + } + + this.taskQueue.Enqueue(action); + + Monitor.Pulse(this.queueLock); + } + } + + private class MyTask : IMyTask + { + private readonly MyThreadPool pool; + private Func func; + private readonly object taskLock = new(); + private readonly ManualResetEventSlim ResultReady = new(false); + + private TResult? result; + private AggregateException? exception; + private volatile bool isCompleted; + + private readonly List followUpActions = new(); + + public MyTask(Func func, MyThreadPool pool) + { + ArgumentNullException.ThrowIfNull(func); + this.func = func; + this.pool = pool; + } + public bool IsCompleted => this.isCompleted; + + public TResult Result + { + get + { + this.ResultReady.Wait(); + + if (exception is not null) + { + throw this.exception; + } + return this.result; + } + } + + public IMyTask ContinueWith(Func next) + { + ArgumentNullException.ThrowIfNull(next); + + if (this.pool.cancellationTokenSource.IsCancellationRequested) + { + throw new InvalidOperationException(); + } + + var nextTask = new MyTask(() => + { + var sourceResult = this.Result; + return next(sourceResult); + }, pool); + + lock (this.taskLock) + { + if (this.pool.cancellationTokenSource.IsCancellationRequested) + { + throw new InvalidOperationException(); + } + + if (this.IsCompleted) + { + this.pool.EnqueueAction(nextTask.Execute); + } + else + { + this.followUpActions.Add(nextTask.Execute); + } + } + + return nextTask; + } + + internal void Execute() + { + try + { + if (this.func != null) + { + this.result = this.func(); + } + } + catch (Exception exception) + { + this.exception = new AggregateException(exception); + } + finally + { + lock (this.taskLock) + { + this.func = null; + this.isCompleted = true; + + this.ResultReady.Set(); + + foreach (var action in this.followUpActions) + { + try + { + this.pool.EnqueueAction(action); + } + catch(InvalidOperationException) + { + + } + } + this.followUpActions.Clear(); + } + } + } + }; + } +} \ No newline at end of file diff --git a/MyThreadPoolTask/MyThreadPoolTask/MyThreadPoolTask.csproj b/MyThreadPoolTask/MyThreadPoolTask/MyThreadPoolTask.csproj new file mode 100644 index 0000000..fd4bd08 --- /dev/null +++ b/MyThreadPoolTask/MyThreadPoolTask/MyThreadPoolTask.csproj @@ -0,0 +1,10 @@ + + + + Exe + net9.0 + enable + enable + + + diff --git a/MyThreadPoolTask/MyThreadPoolTask/Program.cs b/MyThreadPoolTask/MyThreadPoolTask/Program.cs new file mode 100644 index 0000000..837131c --- /dev/null +++ b/MyThreadPoolTask/MyThreadPoolTask/Program.cs @@ -0,0 +1 @@ +Console.WriteLine("Hello, World!"); \ No newline at end of file From e4989078b846ea29f76fc566abbf086ad290f6de Mon Sep 17 00:00:00 2001 From: Andrew Kalinin Date: Tue, 14 Oct 2025 23:56:39 +0300 Subject: [PATCH 02/14] add: added the missing methods to the solution --- .../MyThreadPool.Tests.csproj | 23 +++++ .../MyThreadPool.Tests/MyThreadPoolTests.cs | 11 +++ MyThreadPoolTask/MyThreadPoolTask.sln | 8 +- .../MyThreadPoolTask/MyThreadPool.cs | 90 +++++++++++++------ 4 files changed, 104 insertions(+), 28 deletions(-) create mode 100644 MyThreadPoolTask/MyThreadPool.Tests/MyThreadPool.Tests.csproj create mode 100644 MyThreadPoolTask/MyThreadPool.Tests/MyThreadPoolTests.cs diff --git a/MyThreadPoolTask/MyThreadPool.Tests/MyThreadPool.Tests.csproj b/MyThreadPoolTask/MyThreadPool.Tests/MyThreadPool.Tests.csproj new file mode 100644 index 0000000..2055aa0 --- /dev/null +++ b/MyThreadPoolTask/MyThreadPool.Tests/MyThreadPool.Tests.csproj @@ -0,0 +1,23 @@ + + + + net9.0 + latest + enable + enable + false + + + + + + + + + + + + + + + diff --git a/MyThreadPoolTask/MyThreadPool.Tests/MyThreadPoolTests.cs b/MyThreadPoolTask/MyThreadPool.Tests/MyThreadPoolTests.cs new file mode 100644 index 0000000..9841c96 --- /dev/null +++ b/MyThreadPoolTask/MyThreadPool.Tests/MyThreadPoolTests.cs @@ -0,0 +1,11 @@ +namespace MyThreadPool.Tests +{ + public class Tests + { + [Test] + public void Submit_Task_ReturnCorrectResult() + { + Assert.Pass(); + } + } +} diff --git a/MyThreadPoolTask/MyThreadPoolTask.sln b/MyThreadPoolTask/MyThreadPoolTask.sln index eeab820..3bd0912 100644 --- a/MyThreadPoolTask/MyThreadPoolTask.sln +++ b/MyThreadPoolTask/MyThreadPoolTask.sln @@ -1,10 +1,12 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 -VisualStudioVersion = 17.13.35806.99 d17.13 +VisualStudioVersion = 17.13.35806.99 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyThreadPoolTask", "MyThreadPoolTask\MyThreadPoolTask.csproj", "{A7D8D770-5056-44C2-98D0-5973DBA29CA9}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyThreadPool.Tests", "MyThreadPool.Tests\MyThreadPool.Tests.csproj", "{FB5D58A1-E130-4D85-BF0B-388E393ED867}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -15,6 +17,10 @@ Global {A7D8D770-5056-44C2-98D0-5973DBA29CA9}.Debug|Any CPU.Build.0 = Debug|Any CPU {A7D8D770-5056-44C2-98D0-5973DBA29CA9}.Release|Any CPU.ActiveCfg = Release|Any CPU {A7D8D770-5056-44C2-98D0-5973DBA29CA9}.Release|Any CPU.Build.0 = Release|Any CPU + {FB5D58A1-E130-4D85-BF0B-388E393ED867}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FB5D58A1-E130-4D85-BF0B-388E393ED867}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FB5D58A1-E130-4D85-BF0B-388E393ED867}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FB5D58A1-E130-4D85-BF0B-388E393ED867}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/MyThreadPoolTask/MyThreadPoolTask/MyThreadPool.cs b/MyThreadPoolTask/MyThreadPoolTask/MyThreadPool.cs index efb785f..4036e74 100644 --- a/MyThreadPoolTask/MyThreadPoolTask/MyThreadPool.cs +++ b/MyThreadPoolTask/MyThreadPoolTask/MyThreadPool.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace MyThreadPoolTask +namespace MyThreadPoolTask { public class MyThreadPool { @@ -16,6 +10,8 @@ public class MyThreadPool private readonly CancellationTokenSource cancellationTokenSource = new(); + private volatile bool isShutdown = false; + public MyThreadPool(int threadCount) { @@ -65,21 +61,55 @@ private void WorkerLoop() } } - private void EnqueueAction(Action action) + public IMyTask Submit(Func func) + { + if (this.isShutdown) + { + throw new InvalidOperationException("Пул остановлен"); + } + ArgumentNullException.ThrowIfNull(func, nameof(func)); + + var task = new MyTask(func, this); + this.EnqueueAction(task.Execute); + return task; + } + internal void EnqueueAction(Action action) { lock (this.queueLock) { - if (this.cancellationTokenSource.IsCancellationRequested) + if (this.isShutdown) { - throw new InvalidOperationException(); + throw new InvalidOperationException("Пул останолен"); } - this.taskQueue.Enqueue(action); + this.taskQueue.Enqueue(action); Monitor.Pulse(this.queueLock); } } + public void Shutdown() + { + if (this.cancellationTokenSource.IsCancellationRequested && this.isShutdown) + { + return; + } + + this.isShutdown = true; + this.cancellationTokenSource.Cancel(); + lock (this.queueLock) + { + Monitor.PulseAll(this.queueLock); + } + + foreach (var workerThread in this.workers) + { + workerThread.Join(); + } + + this.cancellationTokenSource.Dispose(); + } + private class MyTask : IMyTask { private readonly MyThreadPool pool; @@ -96,6 +126,7 @@ private class MyTask : IMyTask public MyTask(Func func, MyThreadPool pool) { ArgumentNullException.ThrowIfNull(func); + ArgumentNullException.ThrowIfNull(pool); this.func = func; this.pool = pool; } @@ -124,40 +155,45 @@ public IMyTask ContinueWith(Func ne throw new InvalidOperationException(); } - var nextTask = new MyTask(() => - { - var sourceResult = this.Result; - return next(sourceResult); - }, pool); - lock (this.taskLock) { - if (this.pool.cancellationTokenSource.IsCancellationRequested) + if (this.pool.cancellationTokenSource.IsCancellationRequested && this.pool.isShutdown) { throw new InvalidOperationException(); } + var nextTask = new MyTask(() => + { + var currentResult = this.Result; + return next(currentResult); + }, this.pool); + if (this.IsCompleted) { - this.pool.EnqueueAction(nextTask.Execute); + try + { + this.pool.EnqueueAction(nextTask.Execute); + } + catch (InvalidOperationException) + { + throw new InvalidOperationException("Пул остановлен"); + } } + else { this.followUpActions.Add(nextTask.Execute); } - } - return nextTask; + return nextTask; + } } internal void Execute() { try { - if (this.func != null) - { - this.result = this.func(); - } + this.result = this.func(); } catch (Exception exception) { @@ -178,7 +214,7 @@ internal void Execute() { this.pool.EnqueueAction(action); } - catch(InvalidOperationException) + catch (InvalidOperationException) { } @@ -187,6 +223,6 @@ internal void Execute() } } } - }; + } } } \ No newline at end of file From ea3f21bdffe188608df513a92dfbcc345336a67f Mon Sep 17 00:00:00 2001 From: Andrew Kalinin Date: Wed, 15 Oct 2025 00:06:13 +0300 Subject: [PATCH 03/14] delete test project --- .../MyThreadPool.Tests.csproj | 23 ------------------- .../MyThreadPool.Tests/MyThreadPoolTests.cs | 11 --------- 2 files changed, 34 deletions(-) delete mode 100644 MyThreadPoolTask/MyThreadPool.Tests/MyThreadPool.Tests.csproj delete mode 100644 MyThreadPoolTask/MyThreadPool.Tests/MyThreadPoolTests.cs diff --git a/MyThreadPoolTask/MyThreadPool.Tests/MyThreadPool.Tests.csproj b/MyThreadPoolTask/MyThreadPool.Tests/MyThreadPool.Tests.csproj deleted file mode 100644 index 2055aa0..0000000 --- a/MyThreadPoolTask/MyThreadPool.Tests/MyThreadPool.Tests.csproj +++ /dev/null @@ -1,23 +0,0 @@ - - - - net9.0 - latest - enable - enable - false - - - - - - - - - - - - - - - diff --git a/MyThreadPoolTask/MyThreadPool.Tests/MyThreadPoolTests.cs b/MyThreadPoolTask/MyThreadPool.Tests/MyThreadPoolTests.cs deleted file mode 100644 index 9841c96..0000000 --- a/MyThreadPoolTask/MyThreadPool.Tests/MyThreadPoolTests.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace MyThreadPool.Tests -{ - public class Tests - { - [Test] - public void Submit_Task_ReturnCorrectResult() - { - Assert.Pass(); - } - } -} From ccd4e0f1acbd3bafb4abb07b715651747f80ba20 Mon Sep 17 00:00:00 2001 From: Andrew Kalinin Date: Wed, 15 Oct 2025 00:54:19 +0300 Subject: [PATCH 04/14] test: add some tests --- .../MyThreadPool.Tests/MyThreadPoolTests.cs | 11 ----- .../MyThreadPoolTask.Tests.csproj} | 4 ++ .../MyThreadPoolTask.Tests/ThreadPoolTests.cs | 46 +++++++++++++++++++ MyThreadPoolTask/MyThreadPoolTask.sln | 10 ++-- 4 files changed, 55 insertions(+), 16 deletions(-) delete mode 100644 MyThreadPoolTask/MyThreadPool.Tests/MyThreadPoolTests.cs rename MyThreadPoolTask/{MyThreadPool.Tests/MyThreadPool.Tests.csproj => MyThreadPoolTask.Tests/MyThreadPoolTask.Tests.csproj} (86%) create mode 100644 MyThreadPoolTask/MyThreadPoolTask.Tests/ThreadPoolTests.cs diff --git a/MyThreadPoolTask/MyThreadPool.Tests/MyThreadPoolTests.cs b/MyThreadPoolTask/MyThreadPool.Tests/MyThreadPoolTests.cs deleted file mode 100644 index 9841c96..0000000 --- a/MyThreadPoolTask/MyThreadPool.Tests/MyThreadPoolTests.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace MyThreadPool.Tests -{ - public class Tests - { - [Test] - public void Submit_Task_ReturnCorrectResult() - { - Assert.Pass(); - } - } -} diff --git a/MyThreadPoolTask/MyThreadPool.Tests/MyThreadPool.Tests.csproj b/MyThreadPoolTask/MyThreadPoolTask.Tests/MyThreadPoolTask.Tests.csproj similarity index 86% rename from MyThreadPoolTask/MyThreadPool.Tests/MyThreadPool.Tests.csproj rename to MyThreadPoolTask/MyThreadPoolTask.Tests/MyThreadPoolTask.Tests.csproj index 2055aa0..e462fd7 100644 --- a/MyThreadPoolTask/MyThreadPool.Tests/MyThreadPool.Tests.csproj +++ b/MyThreadPoolTask/MyThreadPoolTask.Tests/MyThreadPoolTask.Tests.csproj @@ -16,6 +16,10 @@ + + + + diff --git a/MyThreadPoolTask/MyThreadPoolTask.Tests/ThreadPoolTests.cs b/MyThreadPoolTask/MyThreadPoolTask.Tests/ThreadPoolTests.cs new file mode 100644 index 0000000..605907e --- /dev/null +++ b/MyThreadPoolTask/MyThreadPoolTask.Tests/ThreadPoolTests.cs @@ -0,0 +1,46 @@ +using MyThreadPoolTask; + +namespace MyThreadPoolTask.Tests +{ + public class ThreadPoolTests + { + private MyThreadPool threadPool; + + [SetUp] + public void Setup() + { + this.threadPool = new MyThreadPool(5); + } + + [TearDown] + public void TearDown() + { + this.threadPool.Shutdown(); + } + + [Test] + public void Submit_SinglTask_ReturnCorrectResult() + { + var task = this.threadPool.Submit(() => 42 * 2); + Assert.That(task.Result, Is.EqualTo(84)); + Assert.That(task.IsCompleted, Is.True); + } + + [Test] + public void Submit_MultiplyTask_ReturnCorrectResult() + { + var tasks = new List>(); + for (int i = 0; i < 10; ++i) + { + int local = i; + tasks.Add(this.threadPool.Submit(() => local * 2)); + } + + for (int i = 0; i < 10; ++i) + { + Assert.That(tasks[i].Result, Is.EqualTo(i * 2)); + Assert.That(tasks[i].IsCompleted, Is.True); + } + } + } +} diff --git a/MyThreadPoolTask/MyThreadPoolTask.sln b/MyThreadPoolTask/MyThreadPoolTask.sln index 3bd0912..cfab85d 100644 --- a/MyThreadPoolTask/MyThreadPoolTask.sln +++ b/MyThreadPoolTask/MyThreadPoolTask.sln @@ -5,7 +5,7 @@ VisualStudioVersion = 17.13.35806.99 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyThreadPoolTask", "MyThreadPoolTask\MyThreadPoolTask.csproj", "{A7D8D770-5056-44C2-98D0-5973DBA29CA9}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyThreadPool.Tests", "MyThreadPool.Tests\MyThreadPool.Tests.csproj", "{FB5D58A1-E130-4D85-BF0B-388E393ED867}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyThreadPoolTask.Tests", "MyThreadPoolTask.Tests\MyThreadPoolTask.Tests.csproj", "{2F44C96D-7958-4B1A-ADD6-AF479A3026AE}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -17,10 +17,10 @@ Global {A7D8D770-5056-44C2-98D0-5973DBA29CA9}.Debug|Any CPU.Build.0 = Debug|Any CPU {A7D8D770-5056-44C2-98D0-5973DBA29CA9}.Release|Any CPU.ActiveCfg = Release|Any CPU {A7D8D770-5056-44C2-98D0-5973DBA29CA9}.Release|Any CPU.Build.0 = Release|Any CPU - {FB5D58A1-E130-4D85-BF0B-388E393ED867}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FB5D58A1-E130-4D85-BF0B-388E393ED867}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FB5D58A1-E130-4D85-BF0B-388E393ED867}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FB5D58A1-E130-4D85-BF0B-388E393ED867}.Release|Any CPU.Build.0 = Release|Any CPU + {2F44C96D-7958-4B1A-ADD6-AF479A3026AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2F44C96D-7958-4B1A-ADD6-AF479A3026AE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2F44C96D-7958-4B1A-ADD6-AF479A3026AE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2F44C96D-7958-4B1A-ADD6-AF479A3026AE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 5166637593078fd9ac7ab16d77713cf5b32d8439 Mon Sep 17 00:00:00 2001 From: Andrew Kalinin Date: Thu, 16 Oct 2025 01:16:07 +0300 Subject: [PATCH 05/14] completed the task --- .../MyThreadPoolTask.Tests/.editorconfig | 129 ++++++++++++++ .../MyThreadPoolTask.Tests.csproj | 8 + .../MyThreadPoolTask.Tests/ThreadPoolTests.cs | 79 +++++++-- .../MyThreadPoolTask.Tests/stylecop.json | 9 + .../MyThreadPoolTask/.editorconfig | 112 ++++++++++++ MyThreadPoolTask/MyThreadPoolTask/IMyTask.cs | 24 ++- .../MyThreadPoolTask/MyThreadPool.cs | 161 +++++++++++------- .../MyThreadPoolTask/MyThreadPoolTask.csproj | 11 ++ MyThreadPoolTask/MyThreadPoolTask/Program.cs | 6 +- .../MyThreadPoolTask/stylecop.json | 9 + 10 files changed, 470 insertions(+), 78 deletions(-) create mode 100644 MyThreadPoolTask/MyThreadPoolTask.Tests/.editorconfig create mode 100644 MyThreadPoolTask/MyThreadPoolTask.Tests/stylecop.json create mode 100644 MyThreadPoolTask/MyThreadPoolTask/.editorconfig create mode 100644 MyThreadPoolTask/MyThreadPoolTask/stylecop.json diff --git a/MyThreadPoolTask/MyThreadPoolTask.Tests/.editorconfig b/MyThreadPoolTask/MyThreadPoolTask.Tests/.editorconfig new file mode 100644 index 0000000..504dac8 --- /dev/null +++ b/MyThreadPoolTask/MyThreadPoolTask.Tests/.editorconfig @@ -0,0 +1,129 @@ +[*.cs] + +# SA1600: Elements should be documented +dotnet_diagnostic.SA1600.severity = none + +# Отключить SA0001 +[*.cs] +dotnet_diagnostic.SA0001.severity = none + +[*.cs] +#### Стили именования #### + +# Правила именования + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Спецификации символов + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Стили именования + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case +csharp_style_throw_expression = true:suggestion +csharp_style_prefer_null_check_over_type_check = true:suggestion +csharp_indent_labels = one_less_than_current +csharp_prefer_simple_default_expression = true:suggestion + +[*.vb] +#### Стили именования #### + +# Правила именования + +dotnet_naming_rule.interface_should_be_начинается_с_i.severity = suggestion +dotnet_naming_rule.interface_should_be_начинается_с_i.symbols = interface +dotnet_naming_rule.interface_should_be_начинается_с_i.style = начинается_с_i + +dotnet_naming_rule.типы_should_be_всечастиспрописнойбуквы.severity = suggestion +dotnet_naming_rule.типы_should_be_всечастиспрописнойбуквы.symbols = типы +dotnet_naming_rule.типы_should_be_всечастиспрописнойбуквы.style = всечастиспрописнойбуквы + +dotnet_naming_rule.не_являющиеся_полем_члены_should_be_всечастиспрописнойбуквы.severity = suggestion +dotnet_naming_rule.не_являющиеся_полем_члены_should_be_всечастиспрописнойбуквы.symbols = не_являющиеся_полем_члены +dotnet_naming_rule.не_являющиеся_полем_члены_should_be_всечастиспрописнойбуквы.style = всечастиспрописнойбуквы + +# Спецификации символов + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.типы.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.типы.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected +dotnet_naming_symbols.типы.required_modifiers = + +dotnet_naming_symbols.не_являющиеся_полем_члены.applicable_kinds = property, event, method +dotnet_naming_symbols.не_являющиеся_полем_члены.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected +dotnet_naming_symbols.не_являющиеся_полем_члены.required_modifiers = + +# Стили именования + +dotnet_naming_style.начинается_с_i.required_prefix = I +dotnet_naming_style.начинается_с_i.required_suffix = +dotnet_naming_style.начинается_с_i.word_separator = +dotnet_naming_style.начинается_с_i.capitalization = pascal_case + +dotnet_naming_style.всечастиспрописнойбуквы.required_prefix = +dotnet_naming_style.всечастиспрописнойбуквы.required_suffix = +dotnet_naming_style.всечастиспрописнойбуквы.word_separator = +dotnet_naming_style.всечастиспрописнойбуквы.capitalization = pascal_case + +dotnet_naming_style.всечастиспрописнойбуквы.required_prefix = +dotnet_naming_style.всечастиспрописнойбуквы.required_suffix = +dotnet_naming_style.всечастиспрописнойбуквы.word_separator = +dotnet_naming_style.всечастиспрописнойбуквы.capitalization = pascal_case + +[*.{cs,vb}] +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion +dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion +dotnet_style_namespace_match_folder = true:suggestion +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +indent_size = 4 \ No newline at end of file diff --git a/MyThreadPoolTask/MyThreadPoolTask.Tests/MyThreadPoolTask.Tests.csproj b/MyThreadPoolTask/MyThreadPoolTask.Tests/MyThreadPoolTask.Tests.csproj index 01bbd6e..64e1ef8 100644 --- a/MyThreadPoolTask/MyThreadPoolTask.Tests/MyThreadPoolTask.Tests.csproj +++ b/MyThreadPoolTask/MyThreadPoolTask.Tests/MyThreadPoolTask.Tests.csproj @@ -14,12 +14,20 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/MyThreadPoolTask/MyThreadPoolTask.Tests/ThreadPoolTests.cs b/MyThreadPoolTask/MyThreadPoolTask.Tests/ThreadPoolTests.cs index 605907e..6c4cf9e 100644 --- a/MyThreadPoolTask/MyThreadPoolTask.Tests/ThreadPoolTests.cs +++ b/MyThreadPoolTask/MyThreadPoolTask.Tests/ThreadPoolTests.cs @@ -1,26 +1,25 @@ -using MyThreadPoolTask; +// +// Copyright (c) Kalinin Andrew. All rights reserved. +// namespace MyThreadPoolTask.Tests { + using System.Collections.Concurrent; + public class ThreadPoolTests { - private MyThreadPool threadPool; - - [SetUp] - public void Setup() - { - this.threadPool = new MyThreadPool(5); - } + private MyThreadPool? threadPool; [TearDown] public void TearDown() { - this.threadPool.Shutdown(); + this.threadPool?.Shutdown(); } [Test] public void Submit_SinglTask_ReturnCorrectResult() { + this.threadPool = new MyThreadPool(4); var task = this.threadPool.Submit(() => 42 * 2); Assert.That(task.Result, Is.EqualTo(84)); Assert.That(task.IsCompleted, Is.True); @@ -29,6 +28,7 @@ public void Submit_SinglTask_ReturnCorrectResult() [Test] public void Submit_MultiplyTask_ReturnCorrectResult() { + this.threadPool = new MyThreadPool(5); var tasks = new List>(); for (int i = 0; i < 10; ++i) { @@ -42,5 +42,64 @@ public void Submit_MultiplyTask_ReturnCorrectResult() Assert.That(tasks[i].IsCompleted, Is.True); } } + + [Test] + public void Submit_AfterShutdown_ShouldThrowsException() + { + this.threadPool = new MyThreadPool(2); + this.threadPool.Shutdown(); + Assert.Throws(() => this.threadPool.Submit(() => 2313)); + } + + [Test] + public void Submit_TaskThrowsException_ResultThrowsAggregateException() + { + this.threadPool = new MyThreadPool(2); + var task = this.threadPool.Submit(() => throw new InvalidOperationException()); + var exception = Assert.Throws(() => { var x = task.Result; }); + Assert.That(exception.InnerException, Is.InstanceOf()); + Assert.That(task.IsCompleted, Is.True); + } + + [Test] + public void ContinueWith_ThreeTasks_ShouldReturnCorrectResult() + { + this.threadPool = new MyThreadPool(4); + var firstTask = this.threadPool.Submit(() => 10); + var secondTask = firstTask.ContinueWith(result => result * 4); + var thirdTask = secondTask.ContinueWith(result => result.ToString()); + var finalResult = thirdTask.Result; + Assert.That(finalResult, Is.EqualTo("40")); + Assert.That(firstTask.IsCompleted, Is.True); + Assert.That(secondTask.IsCompleted, Is.True); + Assert.That(thirdTask.IsCompleted, Is.True); + } + + [Test] + public void ThreadPool_NumberOfThreads_ShouldUsetheSpecifiedAmount() + { + const int threadCount = 5; + this.threadPool = new MyThreadPool(threadCount); + var threadId = new ConcurrentDictionary(); + var startSignal = new ManualResetEventSlim(false); + var tasks = new List>(); + + for (int i = 0; i < threadCount; ++i) + { + var task = this.threadPool.Submit(() => + { + threadId.TryAdd(Thread.CurrentThread.ManagedThreadId, true); + startSignal.Wait(); + return true; + }); + tasks.Add(task); + } + + Thread.Sleep(700); + + Assert.That(threadId.Count, Is.EqualTo(threadCount)); + + startSignal.Set(); + } } -} +} \ No newline at end of file diff --git a/MyThreadPoolTask/MyThreadPoolTask.Tests/stylecop.json b/MyThreadPoolTask/MyThreadPoolTask.Tests/stylecop.json new file mode 100644 index 0000000..30fa636 --- /dev/null +++ b/MyThreadPoolTask/MyThreadPoolTask.Tests/stylecop.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", + "settings": { + "documentationRules": { + "companyName": "Kalinin Andrew", + "copyrightText": "Copyright (c) {companyName}. All rights reserved." + } + } +} \ No newline at end of file diff --git a/MyThreadPoolTask/MyThreadPoolTask/.editorconfig b/MyThreadPoolTask/MyThreadPoolTask/.editorconfig new file mode 100644 index 0000000..8fcdb90 --- /dev/null +++ b/MyThreadPoolTask/MyThreadPoolTask/.editorconfig @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MyThreadPoolTask +{ + class _ + { + } +} + +# Отключить SA0001 +[*.cs] +dotnet_diagnostic.SA0001.severity = none + +[*.cs] +#### Стили именования #### + +# Правила именования + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Спецификации символов + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Стили именования + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +[*.vb] +#### Стили именования #### + +# Правила именования + +dotnet_naming_rule.interface_should_be_начинается_с_i.severity = suggestion +dotnet_naming_rule.interface_should_be_начинается_с_i.symbols = interface +dotnet_naming_rule.interface_should_be_начинается_с_i.style = начинается_с_i + +dotnet_naming_rule.типы_should_be_всечастиспрописнойбуквы.severity = suggestion +dotnet_naming_rule.типы_should_be_всечастиспрописнойбуквы.symbols = типы +dotnet_naming_rule.типы_should_be_всечастиспрописнойбуквы.style = всечастиспрописнойбуквы + +dotnet_naming_rule.не_являющиеся_полем_члены_should_be_всечастиспрописнойбуквы.severity = suggestion +dotnet_naming_rule.не_являющиеся_полем_члены_should_be_всечастиспрописнойбуквы.symbols = не_являющиеся_полем_члены +dotnet_naming_rule.не_являющиеся_полем_члены_should_be_всечастиспрописнойбуквы.style = всечастиспрописнойбуквы + +# Спецификации символов + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.типы.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.типы.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected +dotnet_naming_symbols.типы.required_modifiers = + +dotnet_naming_symbols.не_являющиеся_полем_члены.applicable_kinds = property, event, method +dotnet_naming_symbols.не_являющиеся_полем_члены.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected +dotnet_naming_symbols.не_являющиеся_полем_члены.required_modifiers = + +# Стили именования + +dotnet_naming_style.начинается_с_i.required_prefix = I +dotnet_naming_style.начинается_с_i.required_suffix = +dotnet_naming_style.начинается_с_i.word_separator = +dotnet_naming_style.начинается_с_i.capitalization = pascal_case + +dotnet_naming_style.всечастиспрописнойбуквы.required_prefix = +dotnet_naming_style.всечастиспрописнойбуквы.required_suffix = +dotnet_naming_style.всечастиспрописнойбуквы.word_separator = +dotnet_naming_style.всечастиспрописнойбуквы.capitalization = pascal_case + +dotnet_naming_style.всечастиспрописнойбуквы.required_prefix = +dotnet_naming_style.всечастиспрописнойбуквы.required_suffix = +dotnet_naming_style.всечастиспрописнойбуквы.word_separator = +dotnet_naming_style.всечастиспрописнойбуквы.capitalization = pascal_case diff --git a/MyThreadPoolTask/MyThreadPoolTask/IMyTask.cs b/MyThreadPoolTask/MyThreadPoolTask/IMyTask.cs index b38c4d7..3055e8a 100644 --- a/MyThreadPoolTask/MyThreadPoolTask/IMyTask.cs +++ b/MyThreadPoolTask/MyThreadPoolTask/IMyTask.cs @@ -1,17 +1,31 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +// +// Copyright (c) Kalinin Andrew. All rights reserved. +// namespace MyThreadPoolTask { + /// + /// Represents a task that returns a result and supports continuations. + /// + /// Type of task completion result. public interface IMyTask { + /// + /// Gets a value indicating whether the task is completed. + /// bool IsCompleted { get; } + /// + /// Gets the result of the task completion. + /// TResult Result { get; } + /// + /// Creates a continuation that runs after the current task is completed. + /// + /// Continuation result type. + /// A continuation function that accepts the result of the current task and returns a new value. + /// A new task that represents a continuation. IMyTask ContinueWith(Func next); } } diff --git a/MyThreadPoolTask/MyThreadPoolTask/MyThreadPool.cs b/MyThreadPoolTask/MyThreadPoolTask/MyThreadPool.cs index 4036e74..422ea44 100644 --- a/MyThreadPoolTask/MyThreadPoolTask/MyThreadPool.cs +++ b/MyThreadPoolTask/MyThreadPoolTask/MyThreadPool.cs @@ -1,5 +1,12 @@ -namespace MyThreadPoolTask +// +// Copyright (c) Kalinin Andrew. All rights reserved. +// + +namespace MyThreadPoolTask { + /// + /// A thread pool with a fixed number of threads to complete tasks. + /// public class MyThreadPool { private readonly Thread[] workers; @@ -12,7 +19,12 @@ public class MyThreadPool private volatile bool isShutdown = false; - + /// + /// Initializes a new instance of the class. + /// + /// Number of threads in the pool. + /// It is thrown if threadCount is less than or equal to 0. + /// public MyThreadPool(int threadCount) { if (threadCount <= 0) @@ -23,74 +35,44 @@ public MyThreadPool(int threadCount) this.workers = new Thread[threadCount]; for (int i = 0; i < threadCount; ++i) { - this.workers[i] = new Thread(WorkerLoop) + this.workers[i] = new Thread(this.WorkerLoop) { - IsBackground = true + IsBackground = true, }; - workers[i].Start(); - } - } - - private void WorkerLoop() - { - while (!this.cancellationTokenSource.IsCancellationRequested) - { - Action? taskAction = null; - lock (this.queueLock) - { - while (this.taskQueue.Count == 0) - { - if (this.cancellationTokenSource.IsCancellationRequested) - { - return; - } - - Monitor.Wait(this.queueLock); - - if (this.cancellationTokenSource.IsCancellationRequested && this.taskQueue.Count == 0) - { - return; - } - } - - taskAction = this.taskQueue.Dequeue(); - } - - taskAction?.Invoke(); + this.workers[i].Start(); } } + /// + /// Adds a task to the thread pool for execution. + /// + /// Task result type. + /// Function to execute. + /// Task object that allows tracking execution and getting result. + /// + /// Thrown when pool is shutdown. + /// public IMyTask Submit(Func func) { if (this.isShutdown) { throw new InvalidOperationException("Пул остановлен"); } + ArgumentNullException.ThrowIfNull(func, nameof(func)); var task = new MyTask(func, this); this.EnqueueAction(task.Execute); return task; } - internal void EnqueueAction(Action action) - { - lock (this.queueLock) - { - if (this.isShutdown) - { - throw new InvalidOperationException("Пул останолен"); - } - - this.taskQueue.Enqueue(action); - - Monitor.Pulse(this.queueLock); - } - } + /// + /// Stops all threads after completing current tasks. + /// public void Shutdown() { - if (this.cancellationTokenSource.IsCancellationRequested && this.isShutdown) + if (this.cancellationTokenSource.IsCancellationRequested) { return; } @@ -110,19 +92,68 @@ public void Shutdown() this.cancellationTokenSource.Dispose(); } + /// + /// Method for adding action to execution queue. + /// + /// Action to execute. + /// Thrown when pool is shutdown. + /// + internal void EnqueueAction(Action action) + { + lock (this.queueLock) + { + if (this.isShutdown) + { + throw new InvalidOperationException("Пул останолен"); + } + + this.taskQueue.Enqueue(action); + + Monitor.Pulse(this.queueLock); + } + } + + private void WorkerLoop() + { + while (!this.cancellationTokenSource.IsCancellationRequested) + { + Action? taskAction = null; + lock (this.queueLock) + { + while (this.taskQueue.Count == 0) + { + if (this.cancellationTokenSource.IsCancellationRequested) + { + return; + } + + Monitor.Wait(this.queueLock); + + if (this.cancellationTokenSource.IsCancellationRequested && this.taskQueue.Count == 0) + { + return; + } + } + + taskAction = this.taskQueue.Dequeue(); + } + + taskAction?.Invoke(); + } + } + private class MyTask : IMyTask { private readonly MyThreadPool pool; - private Func func; private readonly object taskLock = new(); - private readonly ManualResetEventSlim ResultReady = new(false); + private readonly ManualResetEventSlim resultReady = new(false); + private readonly List followUpActions = new(); + private Func? func; private TResult? result; private AggregateException? exception; private volatile bool isCompleted; - private readonly List followUpActions = new(); - public MyTask(Func func, MyThreadPool pool) { ArgumentNullException.ThrowIfNull(func); @@ -130,19 +161,21 @@ public MyTask(Func func, MyThreadPool pool) this.func = func; this.pool = pool; } + public bool IsCompleted => this.isCompleted; public TResult Result { get { - this.ResultReady.Wait(); + this.resultReady.Wait(); - if (exception is not null) + if (this.exception is not null) { throw this.exception; } - return this.result; + + return this.result!; } } @@ -162,11 +195,13 @@ public IMyTask ContinueWith(Func ne throw new InvalidOperationException(); } - var nextTask = new MyTask(() => + var nextTask = new MyTask( + () => { var currentResult = this.Result; return next(currentResult); - }, this.pool); + }, + this.pool); if (this.IsCompleted) { @@ -179,7 +214,6 @@ public IMyTask ContinueWith(Func ne throw new InvalidOperationException("Пул остановлен"); } } - else { this.followUpActions.Add(nextTask.Execute); @@ -193,7 +227,10 @@ internal void Execute() { try { - this.result = this.func(); + if (this.func is not null) + { + this.result = this.func(); + } } catch (Exception exception) { @@ -206,7 +243,7 @@ internal void Execute() this.func = null; this.isCompleted = true; - this.ResultReady.Set(); + this.resultReady.Set(); foreach (var action in this.followUpActions) { @@ -216,9 +253,9 @@ internal void Execute() } catch (InvalidOperationException) { - } } + this.followUpActions.Clear(); } } diff --git a/MyThreadPoolTask/MyThreadPoolTask/MyThreadPoolTask.csproj b/MyThreadPoolTask/MyThreadPoolTask/MyThreadPoolTask.csproj index fd4bd08..d764014 100644 --- a/MyThreadPoolTask/MyThreadPoolTask/MyThreadPoolTask.csproj +++ b/MyThreadPoolTask/MyThreadPoolTask/MyThreadPoolTask.csproj @@ -7,4 +7,15 @@ enable + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + diff --git a/MyThreadPoolTask/MyThreadPoolTask/Program.cs b/MyThreadPoolTask/MyThreadPoolTask/Program.cs index 837131c..ad0c575 100644 --- a/MyThreadPoolTask/MyThreadPoolTask/Program.cs +++ b/MyThreadPoolTask/MyThreadPoolTask/Program.cs @@ -1 +1,5 @@ -Console.WriteLine("Hello, World!"); \ No newline at end of file +// +// Copyright (c) Kalinin Andrew. All rights reserved. +// + +Console.WriteLine("Hello World"); \ No newline at end of file diff --git a/MyThreadPoolTask/MyThreadPoolTask/stylecop.json b/MyThreadPoolTask/MyThreadPoolTask/stylecop.json new file mode 100644 index 0000000..30fa636 --- /dev/null +++ b/MyThreadPoolTask/MyThreadPoolTask/stylecop.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", + "settings": { + "documentationRules": { + "companyName": "Kalinin Andrew", + "copyrightText": "Copyright (c) {companyName}. All rights reserved." + } + } +} \ No newline at end of file From 9fd2b083d3a95ee72fe9c85dcb953dad326dd553 Mon Sep 17 00:00:00 2001 From: Andrew Kalinin Date: Sun, 28 Dec 2025 23:02:35 +0300 Subject: [PATCH 06/14] refactor: file-scoped namespace now --- .../MyThreadPoolTask.Tests/ThreadPoolTests.cs | 159 ++++---- MyThreadPoolTask/MyThreadPoolTask/IMyTask.cs | 41 +- .../MyThreadPoolTask/MyThreadPool.cs | 367 +++++++++--------- 3 files changed, 282 insertions(+), 285 deletions(-) diff --git a/MyThreadPoolTask/MyThreadPoolTask.Tests/ThreadPoolTests.cs b/MyThreadPoolTask/MyThreadPoolTask.Tests/ThreadPoolTests.cs index 6c4cf9e..6d71f16 100644 --- a/MyThreadPoolTask/MyThreadPoolTask.Tests/ThreadPoolTests.cs +++ b/MyThreadPoolTask/MyThreadPoolTask.Tests/ThreadPoolTests.cs @@ -2,104 +2,103 @@ // Copyright (c) Kalinin Andrew. All rights reserved. // -namespace MyThreadPoolTask.Tests +namespace MyThreadPoolTask.Tests; + +using System.Collections.Concurrent; + +public class ThreadPoolTests { - using System.Collections.Concurrent; + private MyThreadPool? threadPool; - public class ThreadPoolTests + [TearDown] + public void TearDown() { - private MyThreadPool? threadPool; + this.threadPool?.Shutdown(); + } - [TearDown] - public void TearDown() - { - this.threadPool?.Shutdown(); - } + [Test] + public void Submit_SinglTask_ReturnCorrectResult() + { + this.threadPool = new MyThreadPool(4); + var task = this.threadPool.Submit(() => 42 * 2); + Assert.That(task.Result, Is.EqualTo(84)); + Assert.That(task.IsCompleted, Is.True); + } - [Test] - public void Submit_SinglTask_ReturnCorrectResult() + [Test] + public void Submit_MultiplyTask_ReturnCorrectResult() + { + this.threadPool = new MyThreadPool(5); + var tasks = new List>(); + for (int i = 0; i < 10; ++i) { - this.threadPool = new MyThreadPool(4); - var task = this.threadPool.Submit(() => 42 * 2); - Assert.That(task.Result, Is.EqualTo(84)); - Assert.That(task.IsCompleted, Is.True); + int local = i; + tasks.Add(this.threadPool.Submit(() => local * 2)); } - [Test] - public void Submit_MultiplyTask_ReturnCorrectResult() + for (int i = 0; i < 10; ++i) { - this.threadPool = new MyThreadPool(5); - var tasks = new List>(); - for (int i = 0; i < 10; ++i) - { - int local = i; - tasks.Add(this.threadPool.Submit(() => local * 2)); - } - - for (int i = 0; i < 10; ++i) - { - Assert.That(tasks[i].Result, Is.EqualTo(i * 2)); - Assert.That(tasks[i].IsCompleted, Is.True); - } + Assert.That(tasks[i].Result, Is.EqualTo(i * 2)); + Assert.That(tasks[i].IsCompleted, Is.True); } + } - [Test] - public void Submit_AfterShutdown_ShouldThrowsException() - { - this.threadPool = new MyThreadPool(2); - this.threadPool.Shutdown(); - Assert.Throws(() => this.threadPool.Submit(() => 2313)); - } + [Test] + public void Submit_AfterShutdown_ShouldThrowsException() + { + this.threadPool = new MyThreadPool(2); + this.threadPool.Shutdown(); + Assert.Throws(() => this.threadPool.Submit(() => 2313)); + } - [Test] - public void Submit_TaskThrowsException_ResultThrowsAggregateException() - { - this.threadPool = new MyThreadPool(2); - var task = this.threadPool.Submit(() => throw new InvalidOperationException()); - var exception = Assert.Throws(() => { var x = task.Result; }); - Assert.That(exception.InnerException, Is.InstanceOf()); - Assert.That(task.IsCompleted, Is.True); - } + [Test] + public void Submit_TaskThrowsException_ResultThrowsAggregateException() + { + this.threadPool = new MyThreadPool(2); + var task = this.threadPool.Submit(() => throw new InvalidOperationException()); + var exception = Assert.Throws(() => { var x = task.Result; }); + Assert.That(exception.InnerException, Is.InstanceOf()); + Assert.That(task.IsCompleted, Is.True); + } - [Test] - public void ContinueWith_ThreeTasks_ShouldReturnCorrectResult() - { - this.threadPool = new MyThreadPool(4); - var firstTask = this.threadPool.Submit(() => 10); - var secondTask = firstTask.ContinueWith(result => result * 4); - var thirdTask = secondTask.ContinueWith(result => result.ToString()); - var finalResult = thirdTask.Result; - Assert.That(finalResult, Is.EqualTo("40")); - Assert.That(firstTask.IsCompleted, Is.True); - Assert.That(secondTask.IsCompleted, Is.True); - Assert.That(thirdTask.IsCompleted, Is.True); - } + [Test] + public void ContinueWith_ThreeTasks_ShouldReturnCorrectResult() + { + this.threadPool = new MyThreadPool(4); + var firstTask = this.threadPool.Submit(() => 10); + var secondTask = firstTask.ContinueWith(result => result * 4); + var thirdTask = secondTask.ContinueWith(result => result.ToString()); + var finalResult = thirdTask.Result; + Assert.That(finalResult, Is.EqualTo("40")); + Assert.That(firstTask.IsCompleted, Is.True); + Assert.That(secondTask.IsCompleted, Is.True); + Assert.That(thirdTask.IsCompleted, Is.True); + } - [Test] - public void ThreadPool_NumberOfThreads_ShouldUsetheSpecifiedAmount() - { - const int threadCount = 5; - this.threadPool = new MyThreadPool(threadCount); - var threadId = new ConcurrentDictionary(); - var startSignal = new ManualResetEventSlim(false); - var tasks = new List>(); + [Test] + public void ThreadPool_NumberOfThreads_ShouldUsetheSpecifiedAmount() + { + const int threadCount = 5; + this.threadPool = new MyThreadPool(threadCount); + var threadId = new ConcurrentDictionary(); + var startSignal = new ManualResetEventSlim(false); + var tasks = new List>(); - for (int i = 0; i < threadCount; ++i) + for (int i = 0; i < threadCount; ++i) + { + var task = this.threadPool.Submit(() => { - var task = this.threadPool.Submit(() => - { - threadId.TryAdd(Thread.CurrentThread.ManagedThreadId, true); - startSignal.Wait(); - return true; - }); - tasks.Add(task); - } + threadId.TryAdd(Thread.CurrentThread.ManagedThreadId, true); + startSignal.Wait(); + return true; + }); + tasks.Add(task); + } - Thread.Sleep(700); + Thread.Sleep(700); - Assert.That(threadId.Count, Is.EqualTo(threadCount)); + Assert.That(threadId.Count, Is.EqualTo(threadCount)); - startSignal.Set(); - } + startSignal.Set(); } } \ No newline at end of file diff --git a/MyThreadPoolTask/MyThreadPoolTask/IMyTask.cs b/MyThreadPoolTask/MyThreadPoolTask/IMyTask.cs index 3055e8a..f212443 100644 --- a/MyThreadPoolTask/MyThreadPoolTask/IMyTask.cs +++ b/MyThreadPoolTask/MyThreadPoolTask/IMyTask.cs @@ -2,30 +2,29 @@ // Copyright (c) Kalinin Andrew. All rights reserved. // -namespace MyThreadPoolTask +namespace MyThreadPoolTask; + +/// +/// Represents a task that returns a result and supports continuations. +/// +/// Type of task completion result. +public interface IMyTask { /// - /// Represents a task that returns a result and supports continuations. + /// Gets a value indicating whether the task is completed. /// - /// Type of task completion result. - public interface IMyTask - { - /// - /// Gets a value indicating whether the task is completed. - /// - bool IsCompleted { get; } + bool IsCompleted { get; } - /// - /// Gets the result of the task completion. - /// - TResult Result { get; } + /// + /// Gets the result of the task completion. + /// + TResult Result { get; } - /// - /// Creates a continuation that runs after the current task is completed. - /// - /// Continuation result type. - /// A continuation function that accepts the result of the current task and returns a new value. - /// A new task that represents a continuation. - IMyTask ContinueWith(Func next); - } + /// + /// Creates a continuation that runs after the current task is completed. + /// + /// Continuation result type. + /// A continuation function that accepts the result of the current task and returns a new value. + /// A new task that represents a continuation. + IMyTask ContinueWith(Func next); } diff --git a/MyThreadPoolTask/MyThreadPoolTask/MyThreadPool.cs b/MyThreadPoolTask/MyThreadPoolTask/MyThreadPool.cs index 422ea44..eb0f6e8 100644 --- a/MyThreadPoolTask/MyThreadPoolTask/MyThreadPool.cs +++ b/MyThreadPoolTask/MyThreadPoolTask/MyThreadPool.cs @@ -2,262 +2,261 @@ // Copyright (c) Kalinin Andrew. All rights reserved. // -namespace MyThreadPoolTask +namespace MyThreadPoolTask; + +/// +/// A thread pool with a fixed number of threads to complete tasks. +/// +public class MyThreadPool { - /// - /// A thread pool with a fixed number of threads to complete tasks. - /// - public class MyThreadPool - { - private readonly Thread[] workers; + private readonly Thread[] workers; - private readonly Queue taskQueue = new(); + private readonly Queue taskQueue = new(); - private readonly object queueLock = new(); + private readonly object queueLock = new(); - private readonly CancellationTokenSource cancellationTokenSource = new(); + private readonly CancellationTokenSource cancellationTokenSource = new(); - private volatile bool isShutdown = false; + private volatile bool isShutdown = false; - /// - /// Initializes a new instance of the class. - /// - /// Number of threads in the pool. - /// It is thrown if threadCount is less than or equal to 0. - /// - public MyThreadPool(int threadCount) + /// + /// Initializes a new instance of the class. + /// + /// Number of threads in the pool. + /// It is thrown if threadCount is less than or equal to 0. + /// + public MyThreadPool(int threadCount) + { + if (threadCount <= 0) { - if (threadCount <= 0) - { - throw new ArgumentOutOfRangeException($"Количество потоков {threadCount} должно быть больше нуля"); - } + throw new ArgumentOutOfRangeException($"Количество потоков {threadCount} должно быть больше нуля"); + } - this.workers = new Thread[threadCount]; - for (int i = 0; i < threadCount; ++i) + this.workers = new Thread[threadCount]; + for (int i = 0; i < threadCount; ++i) + { + this.workers[i] = new Thread(this.WorkerLoop) { - this.workers[i] = new Thread(this.WorkerLoop) - { - IsBackground = true, - }; + IsBackground = true, + }; - this.workers[i].Start(); - } + this.workers[i].Start(); } + } - /// - /// Adds a task to the thread pool for execution. - /// - /// Task result type. - /// Function to execute. - /// Task object that allows tracking execution and getting result. - /// - /// Thrown when pool is shutdown. - /// - public IMyTask Submit(Func func) + /// + /// Adds a task to the thread pool for execution. + /// + /// Task result type. + /// Function to execute. + /// Task object that allows tracking execution and getting result. + /// + /// Thrown when pool is shutdown. + /// + public IMyTask Submit(Func func) + { + if (this.isShutdown) { - if (this.isShutdown) - { - throw new InvalidOperationException("Пул остановлен"); - } + throw new InvalidOperationException("Пул остановлен"); + } - ArgumentNullException.ThrowIfNull(func, nameof(func)); + ArgumentNullException.ThrowIfNull(func, nameof(func)); - var task = new MyTask(func, this); - this.EnqueueAction(task.Execute); - return task; - } + var task = new MyTask(func, this); + this.EnqueueAction(task.Execute); + return task; + } - /// - /// Stops all threads after completing current tasks. - /// - public void Shutdown() + /// + /// Stops all threads after completing current tasks. + /// + public void Shutdown() + { + if (this.cancellationTokenSource.IsCancellationRequested) { - if (this.cancellationTokenSource.IsCancellationRequested) - { - return; - } - - this.isShutdown = true; - this.cancellationTokenSource.Cancel(); - lock (this.queueLock) - { - Monitor.PulseAll(this.queueLock); - } + return; + } - foreach (var workerThread in this.workers) - { - workerThread.Join(); - } + this.isShutdown = true; + this.cancellationTokenSource.Cancel(); + lock (this.queueLock) + { + Monitor.PulseAll(this.queueLock); + } - this.cancellationTokenSource.Dispose(); + foreach (var workerThread in this.workers) + { + workerThread.Join(); } - /// - /// Method for adding action to execution queue. - /// - /// Action to execute. - /// Thrown when pool is shutdown. - /// - internal void EnqueueAction(Action action) + this.cancellationTokenSource.Dispose(); + } + + /// + /// Method for adding action to execution queue. + /// + /// Action to execute. + /// Thrown when pool is shutdown. + /// + internal void EnqueueAction(Action action) + { + lock (this.queueLock) { - lock (this.queueLock) + if (this.isShutdown) { - if (this.isShutdown) - { - throw new InvalidOperationException("Пул останолен"); - } + throw new InvalidOperationException("Пул останолен"); + } - this.taskQueue.Enqueue(action); + this.taskQueue.Enqueue(action); - Monitor.Pulse(this.queueLock); - } + Monitor.Pulse(this.queueLock); } + } - private void WorkerLoop() + private void WorkerLoop() + { + while (!this.cancellationTokenSource.IsCancellationRequested) { - while (!this.cancellationTokenSource.IsCancellationRequested) + Action? taskAction = null; + lock (this.queueLock) { - Action? taskAction = null; - lock (this.queueLock) + while (this.taskQueue.Count == 0) { - while (this.taskQueue.Count == 0) + if (this.cancellationTokenSource.IsCancellationRequested) { - if (this.cancellationTokenSource.IsCancellationRequested) - { - return; - } + return; + } - Monitor.Wait(this.queueLock); + Monitor.Wait(this.queueLock); - if (this.cancellationTokenSource.IsCancellationRequested && this.taskQueue.Count == 0) - { - return; - } + if (this.cancellationTokenSource.IsCancellationRequested && this.taskQueue.Count == 0) + { + return; } - - taskAction = this.taskQueue.Dequeue(); } - taskAction?.Invoke(); + taskAction = this.taskQueue.Dequeue(); } + + taskAction?.Invoke(); } + } - private class MyTask : IMyTask - { - private readonly MyThreadPool pool; - private readonly object taskLock = new(); - private readonly ManualResetEventSlim resultReady = new(false); - private readonly List followUpActions = new(); + private class MyTask : IMyTask + { + private readonly MyThreadPool pool; + private readonly object taskLock = new(); + private readonly ManualResetEventSlim resultReady = new(false); + private readonly List followUpActions = new(); - private Func? func; - private TResult? result; - private AggregateException? exception; - private volatile bool isCompleted; + private Func? func; + private TResult? result; + private AggregateException? exception; + private volatile bool isCompleted; - public MyTask(Func func, MyThreadPool pool) - { - ArgumentNullException.ThrowIfNull(func); - ArgumentNullException.ThrowIfNull(pool); - this.func = func; - this.pool = pool; - } + public MyTask(Func func, MyThreadPool pool) + { + ArgumentNullException.ThrowIfNull(func); + ArgumentNullException.ThrowIfNull(pool); + this.func = func; + this.pool = pool; + } - public bool IsCompleted => this.isCompleted; + public bool IsCompleted => this.isCompleted; - public TResult Result + public TResult Result + { + get { - get - { - this.resultReady.Wait(); - - if (this.exception is not null) - { - throw this.exception; - } + this.resultReady.Wait(); - return this.result!; + if (this.exception is not null) + { + throw this.exception; } + + return this.result!; } + } - public IMyTask ContinueWith(Func next) + public IMyTask ContinueWith(Func next) + { + ArgumentNullException.ThrowIfNull(next); + + if (this.pool.cancellationTokenSource.IsCancellationRequested) { - ArgumentNullException.ThrowIfNull(next); + throw new InvalidOperationException(); + } - if (this.pool.cancellationTokenSource.IsCancellationRequested) + lock (this.taskLock) + { + if (this.pool.cancellationTokenSource.IsCancellationRequested && this.pool.isShutdown) { throw new InvalidOperationException(); } - lock (this.taskLock) + var nextTask = new MyTask( + () => { - if (this.pool.cancellationTokenSource.IsCancellationRequested && this.pool.isShutdown) - { - throw new InvalidOperationException(); - } + var currentResult = this.Result; + return next(currentResult); + }, + this.pool); - var nextTask = new MyTask( - () => - { - var currentResult = this.Result; - return next(currentResult); - }, - this.pool); - - if (this.IsCompleted) + if (this.IsCompleted) + { + try { - try - { - this.pool.EnqueueAction(nextTask.Execute); - } - catch (InvalidOperationException) - { - throw new InvalidOperationException("Пул остановлен"); - } + this.pool.EnqueueAction(nextTask.Execute); } - else + catch (InvalidOperationException) { - this.followUpActions.Add(nextTask.Execute); + throw new InvalidOperationException("Пул остановлен"); } - - return nextTask; } + else + { + this.followUpActions.Add(nextTask.Execute); + } + + return nextTask; } + } - internal void Execute() + internal void Execute() + { + try { - try - { - if (this.func is not null) - { - this.result = this.func(); - } - } - catch (Exception exception) + if (this.func is not null) { - this.exception = new AggregateException(exception); + this.result = this.func(); } - finally + } + catch (Exception exception) + { + this.exception = new AggregateException(exception); + } + finally + { + lock (this.taskLock) { - lock (this.taskLock) - { - this.func = null; - this.isCompleted = true; + this.func = null; + this.isCompleted = true; - this.resultReady.Set(); + this.resultReady.Set(); - foreach (var action in this.followUpActions) + foreach (var action in this.followUpActions) + { + try + { + this.pool.EnqueueAction(action); + } + catch (InvalidOperationException) { - try - { - this.pool.EnqueueAction(action); - } - catch (InvalidOperationException) - { - } } - - this.followUpActions.Clear(); } + + this.followUpActions.Clear(); } } } From 929e7b68f77f98a3f527695e738850e4cebd4084 Mon Sep 17 00:00:00 2001 From: Andrew Kalinin Date: Sun, 28 Dec 2025 23:21:47 +0300 Subject: [PATCH 07/14] fix: eliminating double-checking CancellationToken in ContinueWith --- MyThreadPoolTask/MyThreadPoolTask/MyThreadPool.cs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/MyThreadPoolTask/MyThreadPoolTask/MyThreadPool.cs b/MyThreadPoolTask/MyThreadPoolTask/MyThreadPool.cs index eb0f6e8..f56e20f 100644 --- a/MyThreadPoolTask/MyThreadPoolTask/MyThreadPool.cs +++ b/MyThreadPoolTask/MyThreadPoolTask/MyThreadPool.cs @@ -104,7 +104,7 @@ internal void EnqueueAction(Action action) { if (this.isShutdown) { - throw new InvalidOperationException("Пул останолен"); + throw new InvalidOperationException("Пул остановлен"); } this.taskQueue.Enqueue(action); @@ -183,16 +183,11 @@ public IMyTask ContinueWith(Func ne { ArgumentNullException.ThrowIfNull(next); - if (this.pool.cancellationTokenSource.IsCancellationRequested) - { - throw new InvalidOperationException(); - } - lock (this.taskLock) { - if (this.pool.cancellationTokenSource.IsCancellationRequested && this.pool.isShutdown) + if (this.pool.isShutdown) { - throw new InvalidOperationException(); + throw new InvalidOperationException("Пул остановлен"); } var nextTask = new MyTask( From 831833c411621a14d5b3fb3987413c21404d3db2 Mon Sep 17 00:00:00 2001 From: Andrew Kalinin Date: Sun, 28 Dec 2025 23:41:46 +0300 Subject: [PATCH 08/14] fix: correct processing of ContinueWith continuations during Shutdown --- .../MyThreadPoolTask/MyThreadPool.cs | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/MyThreadPoolTask/MyThreadPoolTask/MyThreadPool.cs b/MyThreadPoolTask/MyThreadPoolTask/MyThreadPool.cs index f56e20f..28d5c03 100644 --- a/MyThreadPoolTask/MyThreadPoolTask/MyThreadPool.cs +++ b/MyThreadPoolTask/MyThreadPoolTask/MyThreadPool.cs @@ -147,7 +147,7 @@ private class MyTask : IMyTask private readonly MyThreadPool pool; private readonly object taskLock = new(); private readonly ManualResetEventSlim resultReady = new(false); - private readonly List followUpActions = new(); + private readonly List<(Action Execute, Action OnShutDown)> followUpActions = new(); private Func? func; private TResult? result; @@ -211,13 +211,28 @@ public IMyTask ContinueWith(Func ne } else { - this.followUpActions.Add(nextTask.Execute); + this.followUpActions.Add((nextTask.Execute, nextTask.SetShutdownException)); } return nextTask; } } + internal void SetShutdownException() + { + lock (this.taskLock) + { + if (this.isCompleted) + { + return; + } + + this.exception = new AggregateException(new InvalidOperationException("Пул остановлен")); + this.isCompleted = true; + this.resultReady.Set(); + } + } + internal void Execute() { try @@ -240,14 +255,15 @@ internal void Execute() this.resultReady.Set(); - foreach (var action in this.followUpActions) + foreach (var (execute, onShutdown) in this.followUpActions) { try { - this.pool.EnqueueAction(action); + this.pool.EnqueueAction(execute); } catch (InvalidOperationException) { + onShutdown(); } } From a303136cd3e4f26c75ced067f3ce8c52e6f96066 Mon Sep 17 00:00:00 2001 From: Andrew Kalinin Date: Sun, 28 Dec 2025 23:43:35 +0300 Subject: [PATCH 09/14] refactor: class library now --- MyThreadPoolTask/MyThreadPoolTask/MyThreadPoolTask.csproj | 2 +- MyThreadPoolTask/MyThreadPoolTask/Program.cs | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) delete mode 100644 MyThreadPoolTask/MyThreadPoolTask/Program.cs diff --git a/MyThreadPoolTask/MyThreadPoolTask/MyThreadPoolTask.csproj b/MyThreadPoolTask/MyThreadPoolTask/MyThreadPoolTask.csproj index d764014..085d85a 100644 --- a/MyThreadPoolTask/MyThreadPoolTask/MyThreadPoolTask.csproj +++ b/MyThreadPoolTask/MyThreadPoolTask/MyThreadPoolTask.csproj @@ -1,7 +1,7 @@  - Exe + Library net9.0 enable enable diff --git a/MyThreadPoolTask/MyThreadPoolTask/Program.cs b/MyThreadPoolTask/MyThreadPoolTask/Program.cs deleted file mode 100644 index ad0c575..0000000 --- a/MyThreadPoolTask/MyThreadPoolTask/Program.cs +++ /dev/null @@ -1,5 +0,0 @@ -// -// Copyright (c) Kalinin Andrew. All rights reserved. -// - -Console.WriteLine("Hello World"); \ No newline at end of file From 03ad62bb52bd6e086b55213c6c3286aa8adad8f8 Mon Sep 17 00:00:00 2001 From: Andrew Kalinin Date: Mon, 29 Dec 2025 01:37:47 +0300 Subject: [PATCH 10/14] test: add more tests --- .../MyThreadPoolTask.Tests/ThreadPoolTests.cs | 101 ++++++++++++++++++ MyThreadPoolTask/MyThreadPoolTask/IMyTask.cs | 2 +- 2 files changed, 102 insertions(+), 1 deletion(-) diff --git a/MyThreadPoolTask/MyThreadPoolTask.Tests/ThreadPoolTests.cs b/MyThreadPoolTask/MyThreadPoolTask.Tests/ThreadPoolTests.cs index 6d71f16..6199a72 100644 --- a/MyThreadPoolTask/MyThreadPoolTask.Tests/ThreadPoolTests.cs +++ b/MyThreadPoolTask/MyThreadPoolTask.Tests/ThreadPoolTests.cs @@ -101,4 +101,105 @@ public void ThreadPool_NumberOfThreads_ShouldUsetheSpecifiedAmount() startSignal.Set(); } + + [Test] + public void ContinueWith_MultipleContinuations_AllShouldExecute() + { + this.threadPool = new MyThreadPool(4); + var baseTask = this.threadPool.Submit(() => 10); + + var continuation1 = baseTask.ContinueWith(x => x + 1); + var continuation2 = baseTask.ContinueWith(x => x + 2); + var continuation3 = baseTask.ContinueWith(x => x * 2); + + Assert.That(continuation1.Result, Is.EqualTo(11)); + Assert.That(continuation2.Result, Is.EqualTo(12)); + Assert.That(continuation3.Result, Is.EqualTo(20)); + } + + [Test] + public void ContinueWith_OnCompletedTask_ShouldStillWork() + { + this.threadPool = new MyThreadPool(2); + var task = this.threadPool.Submit(() => 5); + var result = task.Result; + + Assert.That(task.IsCompleted, Is.True); + + var continuation = task.ContinueWith(x => x * 10); + Assert.That(continuation.Result, Is.EqualTo(50)); + } + + [Test] + public void Result_BeforeCompletion_ShouldBlockUntilComplete() + { + this.threadPool = new MyThreadPool(2); + var startSignal = new ManualResetEventSlim(false); + var task = this.threadPool.Submit(() => + { + startSignal.Wait(); + return 4; + }); + + Assert.That(task.IsCompleted, Is.False); + + var resultTask = Task.Run(() => task.Result); + + Thread.Sleep(100); + Assert.That(resultTask.IsCompleted, Is.False); + + startSignal.Set(); + Assert.That(resultTask.Result, Is.EqualTo(4)); + Assert.That(task.IsCompleted, Is.True); + } + + [Test] + public void ContinueWith_AfterShutdown_ShouldThrowException() + { + this.threadPool = new MyThreadPool(2); + var task = this.threadPool.Submit(() => 10); + var result = task.Result; + + this.threadPool.Shutdown(); + + Assert.Throws(() => task.ContinueWith(x => x * 2)); + this.threadPool = null; + } + + [TestCase(0)] + [TestCase(-1)] + public void Constructor_InvalidThreadCount_ShouldThrowException(int count) + { + Assert.Throws(() => new MyThreadPool(count)); + } + + [Test] + public void ContinueWith_WhenShutdownDuringExecution_ShouldThrowExceptionInResult() + { + this.threadPool = new MyThreadPool(1); + var blockSignal = new ManualResetEventSlim(false); + var taskStarted = new ManualResetEventSlim(false); + + var baseTask = this.threadPool.Submit(() => + { + taskStarted.Set(); + blockSignal.Wait(); + return 4; + }); + + taskStarted.Wait(); + + var continuation = baseTask.ContinueWith(x => x * 2); + var shutdownThread = new Thread(() => this.threadPool!.Shutdown()); + shutdownThread.Start(); + + Thread.Sleep(100); + blockSignal.Set(); + + shutdownThread.Join(TimeSpan.FromSeconds(2)); + var exception = Assert.Throws(() => _ = continuation.Result); + Assert.That(exception!.InnerException, Is.InstanceOf()); + + this.threadPool = null; + } } \ No newline at end of file diff --git a/MyThreadPoolTask/MyThreadPoolTask/IMyTask.cs b/MyThreadPoolTask/MyThreadPoolTask/IMyTask.cs index f212443..f67cd25 100644 --- a/MyThreadPoolTask/MyThreadPoolTask/IMyTask.cs +++ b/MyThreadPoolTask/MyThreadPoolTask/IMyTask.cs @@ -27,4 +27,4 @@ public interface IMyTask /// A continuation function that accepts the result of the current task and returns a new value. /// A new task that represents a continuation. IMyTask ContinueWith(Func next); -} +} \ No newline at end of file From 8b31073d975e05951ceedfcc77a50557a24c54a4 Mon Sep 17 00:00:00 2001 From: Andrew Kalinin Date: Thu, 8 Jan 2026 09:37:58 +0300 Subject: [PATCH 11/14] refactor: changes in connection with comments --- MyThreadPoolTask/MyThreadPoolTask/IMyTask.cs | 4 ++ .../MyThreadPoolTask/MyThreadPool.cs | 68 +++++++++++-------- 2 files changed, 43 insertions(+), 29 deletions(-) diff --git a/MyThreadPoolTask/MyThreadPoolTask/IMyTask.cs b/MyThreadPoolTask/MyThreadPoolTask/IMyTask.cs index f67cd25..dc8746e 100644 --- a/MyThreadPoolTask/MyThreadPoolTask/IMyTask.cs +++ b/MyThreadPoolTask/MyThreadPoolTask/IMyTask.cs @@ -17,7 +17,11 @@ public interface IMyTask /// /// Gets the result of the task completion. + /// Blocks the calling thread until the task completes. /// + /// + /// Thrown if the task completed with an exception. + /// TResult Result { get; } /// diff --git a/MyThreadPoolTask/MyThreadPoolTask/MyThreadPool.cs b/MyThreadPoolTask/MyThreadPoolTask/MyThreadPool.cs index 28d5c03..021f13f 100644 --- a/MyThreadPoolTask/MyThreadPoolTask/MyThreadPool.cs +++ b/MyThreadPoolTask/MyThreadPoolTask/MyThreadPool.cs @@ -17,7 +17,7 @@ public class MyThreadPool private readonly CancellationTokenSource cancellationTokenSource = new(); - private volatile bool isShutdown = false; + private int isShutdown = 0; /// /// Initializes a new instance of the class. @@ -27,10 +27,7 @@ public class MyThreadPool /// public MyThreadPool(int threadCount) { - if (threadCount <= 0) - { - throw new ArgumentOutOfRangeException($"Количество потоков {threadCount} должно быть больше нуля"); - } + ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(threadCount, 0); this.workers = new Thread[threadCount]; for (int i = 0; i < threadCount; ++i) @@ -55,12 +52,8 @@ public MyThreadPool(int threadCount) /// public IMyTask Submit(Func func) { - if (this.isShutdown) - { - throw new InvalidOperationException("Пул остановлен"); - } - ArgumentNullException.ThrowIfNull(func, nameof(func)); + ArgumentNullException.ThrowIfNull(func); var task = new MyTask(func, this); this.EnqueueAction(task.Execute); @@ -72,12 +65,11 @@ public IMyTask Submit(Func func) /// public void Shutdown() { - if (this.cancellationTokenSource.IsCancellationRequested) + if (Interlocked.CompareExchange(ref this.isShutdown, 1, 0) != 0) { return; } - this.isShutdown = true; this.cancellationTokenSource.Cancel(); lock (this.queueLock) { @@ -102,7 +94,7 @@ internal void EnqueueAction(Action action) { lock (this.queueLock) { - if (this.isShutdown) + if (this.isShutdown != 0) { throw new InvalidOperationException("Пул остановлен"); } @@ -145,9 +137,9 @@ private void WorkerLoop() private class MyTask : IMyTask { private readonly MyThreadPool pool; - private readonly object taskLock = new(); + private readonly Lock taskLock = new(); private readonly ManualResetEventSlim resultReady = new(false); - private readonly List<(Action Execute, Action OnShutDown)> followUpActions = new(); + private readonly List<(Action Execute, Action OnShutDown, Action OnParentException)> followUpActions = new(); private Func? func; private TResult? result; @@ -185,10 +177,6 @@ public IMyTask ContinueWith(Func ne lock (this.taskLock) { - if (this.pool.isShutdown) - { - throw new InvalidOperationException("Пул остановлен"); - } var nextTask = new MyTask( () => @@ -200,18 +188,18 @@ public IMyTask ContinueWith(Func ne if (this.IsCompleted) { - try + if (this.exception is not null) { - this.pool.EnqueueAction(nextTask.Execute); + nextTask.SetParentException(this.exception); } - catch (InvalidOperationException) + else { - throw new InvalidOperationException("Пул остановлен"); + this.pool.EnqueueAction(nextTask.Execute); } } else { - this.followUpActions.Add((nextTask.Execute, nextTask.SetShutdownException)); + this.followUpActions.Add((nextTask.Execute, nextTask.SetShutdownException, nextTask.SetParentException)); } return nextTask; @@ -233,6 +221,21 @@ internal void SetShutdownException() } } + internal void SetParentException(AggregateException parentException) + { + lock (this.taskLock) + { + if (this.isCompleted) + { + return; + } + + this.exception = parentException; + this.isCompleted = true; + this.resultReady.Set(); + } + } + internal void Execute() { try @@ -255,15 +258,22 @@ internal void Execute() this.resultReady.Set(); - foreach (var (execute, onShutdown) in this.followUpActions) + foreach (var (execute, onShutdown, onParentException) in this.followUpActions) { - try + if (this.exception is not null) { - this.pool.EnqueueAction(execute); + onParentException(this.exception); } - catch (InvalidOperationException) + else { - onShutdown(); + try + { + this.pool.EnqueueAction(execute); + } + catch (InvalidOperationException) + { + onShutdown(); + } } } From 6f0ec82d1e8c63bab616dbef4aaf262997c7fcce Mon Sep 17 00:00:00 2001 From: Andrew Kalinin Date: Thu, 8 Jan 2026 09:38:41 +0300 Subject: [PATCH 12/14] test: adding new tests in response to teacher comments --- .../MyThreadPoolTask.Tests/ThreadPoolTests.cs | 237 +++++++++++++++++- 1 file changed, 233 insertions(+), 4 deletions(-) diff --git a/MyThreadPoolTask/MyThreadPoolTask.Tests/ThreadPoolTests.cs b/MyThreadPoolTask/MyThreadPoolTask.Tests/ThreadPoolTests.cs index 6199a72..78a0be8 100644 --- a/MyThreadPoolTask/MyThreadPoolTask.Tests/ThreadPoolTests.cs +++ b/MyThreadPoolTask/MyThreadPoolTask.Tests/ThreadPoolTests.cs @@ -11,10 +11,7 @@ public class ThreadPoolTests private MyThreadPool? threadPool; [TearDown] - public void TearDown() - { - this.threadPool?.Shutdown(); - } + public void TearDown() => this.threadPool?.Shutdown(); [Test] public void Submit_SinglTask_ReturnCorrectResult() @@ -202,4 +199,236 @@ public void ContinueWith_WhenShutdownDuringExecution_ShouldThrowExceptionInResul this.threadPool = null; } + + [Test] + public void RaceCondition_SubmitAndShutdownConcurrently_ShouldHandleGracefully() + { + this.threadPool = new MyThreadPool(4); + var exceptions = new ConcurrentBag(); + var successfulTasks = new ConcurrentBag>(); + var shutdownStarted = new ManualResetEventSlim(false); + var submitThreadsReady = new CountdownEvent(10); + + var submitThreads = new List(); + for (int i = 0; i < 10; ++i) + { + int taskId = i; + var thread = new Thread(() => + { + submitThreadsReady.Signal(); + shutdownStarted.Wait(); + try + { + var task = this.threadPool!.Submit(() => taskId); + successfulTasks.Add(task); + } + catch (InvalidOperationException ex) + { + exceptions.Add(ex); + } + }); + submitThreads.Add(thread); + thread.Start(); + } + + submitThreadsReady.Wait(); + shutdownStarted.Set(); + + var shutdownThread = new Thread(() => + { + Thread.Sleep(5); + this.threadPool!.Shutdown(); + }); + shutdownThread.Start(); + + foreach (var thread in submitThreads) + { + thread.Join(); + } + + shutdownThread.Join(); + + Assert.That(successfulTasks.Count + exceptions.Count, Is.EqualTo(10)); + + this.threadPool = null; + } + + [Test] + public void RaceCondition_MultipleContinueWithDuringShutdown_ShouldHandleCorrectly() + { + this.threadPool = new MyThreadPool(2); + var blockSignal = new ManualResetEventSlim(false); + var baseTask = this.threadPool.Submit(() => + { + blockSignal.Wait(); + return 10; + }); + + var exceptions = new ConcurrentBag(); + var continuations = new ConcurrentBag>(); + var continueThreadsReady = new CountdownEvent(5); + var shutdownSignal = new ManualResetEventSlim(false); + + var continueThreads = new List(); + for (int i = 0; i < 5; ++i) + { + int multiplier = i + 1; + var thread = new Thread(() => + { + continueThreadsReady.Signal(); + shutdownSignal.Wait(); + try + { + var cont = baseTask.ContinueWith(x => x * multiplier); + continuations.Add(cont); + } + catch (InvalidOperationException ex) + { + exceptions.Add(ex); + } + }); + continueThreads.Add(thread); + thread.Start(); + } + + continueThreadsReady.Wait(); + shutdownSignal.Set(); + + var shutdownThread = new Thread(() => + { + Thread.Sleep(10); + this.threadPool!.Shutdown(); + }); + shutdownThread.Start(); + + foreach (var thread in continueThreads) + { + thread.Join(); + } + + blockSignal.Set(); + shutdownThread.Join(); + + Assert.That(continuations.Count + exceptions.Count, Is.EqualTo(5)); + + this.threadPool = null; + } + + [Test] + public void RaceCondition_MultipleShutdownCalls_ShouldNotCrash() + { + this.threadPool = new MyThreadPool(4); + var tasks = new List>(); + for (int i = 0; i < 5; ++i) + { + int local = i; + tasks.Add(this.threadPool.Submit(() => + { + Thread.Sleep(50); + return local; + })); + } + + var shutdownThreads = new List(); + for (int i = 0; i < 5; ++i) + { + var thread = new Thread(() => this.threadPool!.Shutdown()); + shutdownThreads.Add(thread); + thread.Start(); + } + + foreach (var thread in shutdownThreads) + { + thread.Join(); + } + + Assert.Pass(); + this.threadPool = null; + } + + [Test] + public void RaceCondition_ResultAccessDuringShutdown_ShouldNotDeadlock() + { + this.threadPool = new MyThreadPool(2); + var blockSignal = new ManualResetEventSlim(false); + var task = this.threadPool.Submit(() => + { + blockSignal.Wait(); + return 42; + }); + + var resultThread = new Thread(() => + { + Thread.Sleep(50); + try + { + var result = task.Result; + } + catch + { + } + }); + resultThread.Start(); + + var shutdownThread = new Thread(() => + { + Thread.Sleep(100); + this.threadPool!.Shutdown(); + }); + shutdownThread.Start(); + + Thread.Sleep(150); + blockSignal.Set(); + + bool resultFinished = resultThread.Join(TimeSpan.FromSeconds(2)); + bool shutdownFinished = shutdownThread.Join(TimeSpan.FromSeconds(2)); + + Assert.That(resultFinished, Is.True); + Assert.That(shutdownFinished, Is.True); + + this.threadPool = null; + } + + [Test] + public void RaceCondition_SubmitManyTasksConcurrently_AllShouldExecute() + { + this.threadPool = new MyThreadPool(4); + var tasks = new ConcurrentBag>(); + var submitThreads = new List(); + var startSignal = new ManualResetEventSlim(false); + + for (int i = 0; i < 20; ++i) + { + int taskId = i; + var thread = new Thread(() => + { + startSignal.Wait(); + var task = this.threadPool!.Submit(() => + { + Thread.Sleep(10); + return taskId * 2; + }); + tasks.Add(task); + }); + submitThreads.Add(thread); + thread.Start(); + } + + startSignal.Set(); + + foreach (var thread in submitThreads) + { + thread.Join(); + } + + Assert.That(tasks.Count, Is.EqualTo(20)); + + var results = new HashSet(); + foreach (var task in tasks) + { + results.Add(task.Result); + } + + Assert.That(results.Count, Is.EqualTo(20)); + } } \ No newline at end of file From b867c1430c24da87d3e0141b9ba828463efd1247 Mon Sep 17 00:00:00 2001 From: Andrew Kalinin Date: Thu, 8 Jan 2026 09:47:08 +0300 Subject: [PATCH 13/14] test: correction of the names of some tests --- MyThreadPoolTask/MyThreadPoolTask.Tests/ThreadPoolTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/MyThreadPoolTask/MyThreadPoolTask.Tests/ThreadPoolTests.cs b/MyThreadPoolTask/MyThreadPoolTask.Tests/ThreadPoolTests.cs index 78a0be8..264fca4 100644 --- a/MyThreadPoolTask/MyThreadPoolTask.Tests/ThreadPoolTests.cs +++ b/MyThreadPoolTask/MyThreadPoolTask.Tests/ThreadPoolTests.cs @@ -14,7 +14,7 @@ public class ThreadPoolTests public void TearDown() => this.threadPool?.Shutdown(); [Test] - public void Submit_SinglTask_ReturnCorrectResult() + public void Submit_SingleTask_ReturnCorrectResult() { this.threadPool = new MyThreadPool(4); var task = this.threadPool.Submit(() => 42 * 2); @@ -23,7 +23,7 @@ public void Submit_SinglTask_ReturnCorrectResult() } [Test] - public void Submit_MultiplyTask_ReturnCorrectResult() + public void Submit_MultipleTask_ReturnsCorrectResult() { this.threadPool = new MyThreadPool(5); var tasks = new List>(); @@ -73,7 +73,7 @@ public void ContinueWith_ThreeTasks_ShouldReturnCorrectResult() } [Test] - public void ThreadPool_NumberOfThreads_ShouldUsetheSpecifiedAmount() + public void ThreadPool_NumberOfThreads_ShouldUseTheSpecifiedAmount() { const int threadCount = 5; this.threadPool = new MyThreadPool(threadCount); From be397736be431ccd39b1c402869de4bbbf8d9009 Mon Sep 17 00:00:00 2001 From: Andrew Kalinin Date: Thu, 8 Jan 2026 09:47:36 +0300 Subject: [PATCH 14/14] style: changes due to stylecop --- MyThreadPoolTask/MyThreadPoolTask/MyThreadPool.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/MyThreadPoolTask/MyThreadPoolTask/MyThreadPool.cs b/MyThreadPoolTask/MyThreadPoolTask/MyThreadPool.cs index 021f13f..98531ae 100644 --- a/MyThreadPoolTask/MyThreadPoolTask/MyThreadPool.cs +++ b/MyThreadPoolTask/MyThreadPoolTask/MyThreadPool.cs @@ -52,7 +52,6 @@ public MyThreadPool(int threadCount) /// public IMyTask Submit(Func func) { - ArgumentNullException.ThrowIfNull(func); var task = new MyTask(func, this); @@ -177,7 +176,6 @@ public IMyTask ContinueWith(Func ne lock (this.taskLock) { - var nextTask = new MyTask( () => {