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 new file mode 100644 index 0000000..64e1ef8 --- /dev/null +++ b/MyThreadPoolTask/MyThreadPoolTask.Tests/MyThreadPoolTask.Tests.csproj @@ -0,0 +1,35 @@ + + + + net9.0 + latest + enable + enable + false + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + diff --git a/MyThreadPoolTask/MyThreadPoolTask.Tests/ThreadPoolTests.cs b/MyThreadPoolTask/MyThreadPoolTask.Tests/ThreadPoolTests.cs new file mode 100644 index 0000000..264fca4 --- /dev/null +++ b/MyThreadPoolTask/MyThreadPoolTask.Tests/ThreadPoolTests.cs @@ -0,0 +1,434 @@ +// +// Copyright (c) Kalinin Andrew. All rights reserved. +// + +namespace MyThreadPoolTask.Tests; + +using System.Collections.Concurrent; + +public class ThreadPoolTests +{ + private MyThreadPool? threadPool; + + [TearDown] + public void TearDown() => this.threadPool?.Shutdown(); + + [Test] + public void Submit_SingleTask_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_MultipleTask_ReturnsCorrectResult() + { + 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); + } + } + + [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(); + } + + [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; + } + + [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 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.sln b/MyThreadPoolTask/MyThreadPoolTask.sln new file mode 100644 index 0000000..cfab85d --- /dev/null +++ b/MyThreadPoolTask/MyThreadPoolTask.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +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}") = "MyThreadPoolTask.Tests", "MyThreadPoolTask.Tests\MyThreadPoolTask.Tests.csproj", "{2F44C96D-7958-4B1A-ADD6-AF479A3026AE}" +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 + {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 + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {CBA5D183-FDFD-4BAB-8782-23CE4B80DF14} + EndGlobalSection +EndGlobal 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 new file mode 100644 index 0000000..dc8746e --- /dev/null +++ b/MyThreadPoolTask/MyThreadPoolTask/IMyTask.cs @@ -0,0 +1,34 @@ +// +// 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. + /// Blocks the calling thread until the task completes. + /// + /// + /// Thrown if the task completed with an exception. + /// + 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); +} \ No newline at end of file diff --git a/MyThreadPoolTask/MyThreadPoolTask/MyThreadPool.cs b/MyThreadPoolTask/MyThreadPoolTask/MyThreadPool.cs new file mode 100644 index 0000000..98531ae --- /dev/null +++ b/MyThreadPoolTask/MyThreadPoolTask/MyThreadPool.cs @@ -0,0 +1,283 @@ +// +// 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; + + private readonly Queue taskQueue = new(); + + private readonly object queueLock = new(); + + private readonly CancellationTokenSource cancellationTokenSource = new(); + + private int isShutdown = 0; + + /// + /// 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) + { + ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(threadCount, 0); + + this.workers = new Thread[threadCount]; + for (int i = 0; i < threadCount; ++i) + { + this.workers[i] = new Thread(this.WorkerLoop) + { + IsBackground = true, + }; + + 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) + { + ArgumentNullException.ThrowIfNull(func); + + var task = new MyTask(func, this); + this.EnqueueAction(task.Execute); + return task; + } + + /// + /// Stops all threads after completing current tasks. + /// + public void Shutdown() + { + if (Interlocked.CompareExchange(ref this.isShutdown, 1, 0) != 0) + { + return; + } + + this.cancellationTokenSource.Cancel(); + lock (this.queueLock) + { + Monitor.PulseAll(this.queueLock); + } + + foreach (var workerThread in this.workers) + { + workerThread.Join(); + } + + 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 != 0) + { + 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 readonly Lock taskLock = new(); + private readonly ManualResetEventSlim resultReady = new(false); + private readonly List<(Action Execute, Action OnShutDown, Action OnParentException)> followUpActions = new(); + + 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 bool IsCompleted => this.isCompleted; + + public TResult Result + { + get + { + this.resultReady.Wait(); + + if (this.exception is not null) + { + throw this.exception; + } + + return this.result!; + } + } + + public IMyTask ContinueWith(Func next) + { + ArgumentNullException.ThrowIfNull(next); + + lock (this.taskLock) + { + var nextTask = new MyTask( + () => + { + var currentResult = this.Result; + return next(currentResult); + }, + this.pool); + + if (this.IsCompleted) + { + if (this.exception is not null) + { + nextTask.SetParentException(this.exception); + } + else + { + this.pool.EnqueueAction(nextTask.Execute); + } + } + else + { + this.followUpActions.Add((nextTask.Execute, nextTask.SetShutdownException, nextTask.SetParentException)); + } + + 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 SetParentException(AggregateException parentException) + { + lock (this.taskLock) + { + if (this.isCompleted) + { + return; + } + + this.exception = parentException; + this.isCompleted = true; + this.resultReady.Set(); + } + } + + internal void Execute() + { + try + { + if (this.func is not 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 (execute, onShutdown, onParentException) in this.followUpActions) + { + if (this.exception is not null) + { + onParentException(this.exception); + } + else + { + try + { + this.pool.EnqueueAction(execute); + } + catch (InvalidOperationException) + { + onShutdown(); + } + } + } + + 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..085d85a --- /dev/null +++ b/MyThreadPoolTask/MyThreadPoolTask/MyThreadPoolTask.csproj @@ -0,0 +1,21 @@ + + + + Library + net9.0 + enable + enable + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + 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