-
Notifications
You must be signed in to change notification settings - Fork 0
HW3 MyThreadPool #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
c0bce62
aaa0310
1191c38
adf377a
4531fe6
45c0a77
cfd553a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| | ||
| Microsoft Visual Studio Solution File, Format Version 12.00 | ||
| Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyThreadPool", "MyThreadPool\MyThreadPool.csproj", "{FA31B85A-23F0-4566-8D57-08FA33575C42}" | ||
| EndProject | ||
| Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyThreadPool.Test", "MyThreadPool.Test\MyThreadPool.Test.csproj", "{0655F283-5458-481F-83B3-F5091360DA57}" | ||
| EndProject | ||
| Global | ||
| GlobalSection(SolutionConfigurationPlatforms) = preSolution | ||
| Debug|Any CPU = Debug|Any CPU | ||
| Release|Any CPU = Release|Any CPU | ||
| EndGlobalSection | ||
| GlobalSection(ProjectConfigurationPlatforms) = postSolution | ||
| {FA31B85A-23F0-4566-8D57-08FA33575C42}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | ||
| {FA31B85A-23F0-4566-8D57-08FA33575C42}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||
| {FA31B85A-23F0-4566-8D57-08FA33575C42}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||
| {FA31B85A-23F0-4566-8D57-08FA33575C42}.Release|Any CPU.Build.0 = Release|Any CPU | ||
| {0655F283-5458-481F-83B3-F5091360DA57}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | ||
| {0655F283-5458-481F-83B3-F5091360DA57}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||
| {0655F283-5458-481F-83B3-F5091360DA57}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||
| {0655F283-5458-481F-83B3-F5091360DA57}.Release|Any CPU.Build.0 = Release|Any CPU | ||
| EndGlobalSection | ||
| EndGlobal |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
|
|
||
| <PropertyGroup> | ||
| <TargetFramework>net9.0</TargetFramework> | ||
| <ImplicitUsings>enable</ImplicitUsings> | ||
| <Nullable>enable</Nullable> | ||
| <GenerateDocumentationFile>true</GenerateDocumentationFile> | ||
| <IsPackable>false</IsPackable> | ||
| <IsTestProject>true</IsTestProject> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <PackageReference Include="coverlet.collector" Version="6.0.0"/> | ||
| <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0"/> | ||
| <PackageReference Include="NUnit" Version="3.14.0"/> | ||
| <PackageReference Include="NUnit.Analyzers" Version="3.9.0"/> | ||
| <PackageReference Include="NUnit3TestAdapter" Version="4.5.0"/> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <Using Include="NUnit.Framework"/> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556"> | ||
| <PrivateAssets>all</PrivateAssets> | ||
| <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||
| </PackageReference> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <AdditionalFiles Include="stylecop.json" /> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <ProjectReference Include="..\MyThreadPool\MyThreadPool.csproj" /> | ||
| </ItemGroup> | ||
|
|
||
| </Project> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,131 @@ | ||
| // <copyright file="MyThreadPoolTest.cs" company="khusainovilas"> | ||
| // Copyright (c) khusainovilas. All rights reserved. | ||
| // </copyright> | ||
|
|
||
| namespace MyThreadPool.Test; | ||
|
|
||
| using System.Diagnostics; | ||
|
|
||
| /// <summary> | ||
| /// Unit tests for MyThreadPool. | ||
| /// </summary> | ||
| public class MyThreadPoolTest | ||
| { | ||
| /// <summary> | ||
| /// Tests that submitting a simple task returns the correct result and completes. | ||
| /// </summary> | ||
| [Test] | ||
| public void MyThreadPool_Submit_ReturnsCorrectResult() | ||
| { | ||
| var pool = new MyThreadPool(1); | ||
| var task = pool.Submit(() => 42); | ||
| Assert.Multiple(() => | ||
| { | ||
| Assert.That(task.Result, Is.EqualTo(42)); | ||
| Assert.That(task.IsCompleted, Is.True); | ||
| }); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Tests that a continuation task executes correctly with the transformed result. | ||
| /// </summary> | ||
| [Test] | ||
| public void IMyTask_ContinueWith_ExecutesCorrectly() | ||
| { | ||
| var pool = new MyThreadPool(1); | ||
| var task = pool.Submit(() => 2 * 2); | ||
| var continuation = task.ContinueWith(x => x.ToString()); | ||
| Assert.Multiple(() => | ||
| { | ||
| Assert.That(continuation.Result, Is.EqualTo("4")); | ||
| Assert.That(continuation.IsCompleted, Is.True); | ||
| }); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Tests that an exception in a task is wrapped in AggregateException and the task completes. | ||
| /// </summary> | ||
| [Test] | ||
| public void IMyTask_Result_ThrowsAggregateExceptionOnError() | ||
| { | ||
| var pool = new MyThreadPool(1); | ||
| var task = pool.Submit<int>(() => throw new InvalidOperationException("Test error")); | ||
| var exception = Assert.Throws<AggregateException>(() => _ = task.Result); | ||
| Assert.That(exception.InnerException, Is.TypeOf<InvalidOperationException>()); | ||
| Assert.Multiple(() => | ||
| { | ||
| Assert.That(exception.InnerException.Message, Is.EqualTo("Test error")); | ||
| Assert.That(task.IsCompleted, Is.True); | ||
| }); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Tests that multiple continuations for a single task execute correctly with different transformations. | ||
| /// </summary> | ||
| [Test] | ||
| public void IMyTask_ContinueWith_SupportsMultipleContinuations() | ||
| { | ||
| var pool = new MyThreadPool(1); | ||
| var task = pool.Submit(() => 5); | ||
| var cont1 = task.ContinueWith(x => x + 1); | ||
| var cont2 = task.ContinueWith(x => x * 2); | ||
| Assert.Multiple(() => | ||
| { | ||
| Assert.That(cont1.Result, Is.EqualTo(6)); | ||
| Assert.That(cont2.Result, Is.EqualTo(10)); | ||
| Assert.That(cont1.IsCompleted, Is.True); | ||
| Assert.That(cont2.IsCompleted, Is.True); | ||
| }); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Tests that shutdown prevents new tasks and allows existing tasks to complete. | ||
| /// </summary> | ||
| [Test] | ||
| public void MyThreadPool_Shutdown_PreventsNewTasksAndCompletesExisting() | ||
| { | ||
| var pool = new MyThreadPool(1); | ||
| var task = pool.Submit(() => | ||
| { | ||
| Thread.Sleep(100); | ||
| return 1; | ||
| }); | ||
| pool.Shutdown(); | ||
| Assert.Throws<InvalidOperationException>(() => pool.Submit(() => 2)); | ||
| Assert.Multiple(() => | ||
| { | ||
| Assert.That(task.Result, Is.EqualTo(1)); | ||
| Assert.That(task.IsCompleted, Is.True); | ||
| }); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Tests that the thread pool uses at least n threads by checking parallel execution time. | ||
| /// </summary> | ||
| [Test] | ||
| public void MyThreadPool_ThreadCount_UsesAtLeastNThreads() | ||
| { | ||
| const int n = 4; | ||
| var pool = new MyThreadPool(n); | ||
| var tasks = new List<IMyTask<int>>(); | ||
| var stopwatch = Stopwatch.StartNew(); | ||
| for (var i = 0; i < n; i++) | ||
| { | ||
| tasks.Add(pool.Submit(() => | ||
| { | ||
| Thread.Sleep(1000); | ||
| return 1; | ||
| })); | ||
| } | ||
|
|
||
| foreach (var task in tasks) | ||
| { | ||
| Assert.That(task.Result, Is.EqualTo(1)); | ||
| } | ||
|
|
||
| stopwatch.Stop(); | ||
|
|
||
| // The work of 4 threads will take approximately 1 second. | ||
| Assert.That(stopwatch.Elapsed.TotalSeconds, Is.LessThan(1.5)); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| { | ||
| "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", | ||
| "settings": { | ||
| "documentationRules": { | ||
| "companyName": "khusainovilas", | ||
| "copyrightText": "Copyright (c) {companyName}. All rights reserved.", | ||
| "xmlHeader": true | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| // <copyright file="IMyTask.cs" company="khusainovilas"> | ||
| // Copyright (c) khusainovilas. All rights reserved. | ||
| // </copyright> | ||
|
|
||
| namespace MyThreadPool; | ||
|
|
||
| /// <summary> | ||
| /// Defines a contract for a task in the custom thread pool. | ||
| /// </summary> | ||
| /// <typeparam name="TResult">The type of the result produced by the task.</typeparam> | ||
| public interface IMyTask<out TResult> | ||
| { | ||
| /// <summary> | ||
| /// Gets a value indicating whether returns true if the task is completed (successfully or with an error). | ||
| /// </summary> | ||
| bool IsCompleted { get; } | ||
|
|
||
| /// <summary> | ||
| /// Gets the result of the task. | ||
| /// If the task is not completed, it blocks the calling thread until it is completed. | ||
| /// If the task completes with an exception, it throws an AggregateException with an internal exception. | ||
| /// </summary> | ||
| TResult Result { get; } | ||
|
|
||
| /// <summary> | ||
| /// Creates a new task that runs after the current one is completed, | ||
| /// using its result as an argument for continuation. | ||
| /// A new task is placed in the pool and does not block the calling thread. | ||
| /// </summary>\ | ||
| /// <typeparam name="TNewResult">The type of the result produced by the continuation task.</typeparam> | ||
| /// <param name="continuation">A function that takes the result of the current task and produces a new result.</param> | ||
| /// <returns>A new task representing the continuation.</returns> | ||
| IMyTask<TNewResult> ContinueWith<TNewResult>(Func<TResult, TNewResult> continuation); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,123 @@ | ||
| // <copyright file="MyTask.cs" company="khusainovilas"> | ||
| // Copyright (c) khusainovilas. All rights reserved. | ||
| // </copyright> | ||
|
|
||
| namespace MyThreadPool; | ||
|
|
||
| /// <summary> | ||
| /// Represents a task that executes a function and produces a result>. | ||
| /// </summary> | ||
| /// <typeparam name="TResult">The type of the result produced by the task.</typeparam> | ||
| internal class MyTask<TResult> : IMyTask<TResult> | ||
| { | ||
| private readonly object @lock = new(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. В современном .NET лучше использовать не object, а Lock. Про него знает компилятор и генерит более эффективный код. Ну и называть поле "lock" кажется сомнительной идеей, '@' выглядит страшно. |
||
| private readonly ManualResetEvent waitHandle = new(false); | ||
| private readonly MyThreadPool pool; | ||
| private readonly Func<TResult> func; | ||
| private TResult result = default!; | ||
| private Exception exception = null!; | ||
|
Comment on lines
+17
to
+18
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Если они по смыслу могут быть null хоть в какое-то время своей жизни, сделайте их nullable. Я вообще против оператора |
||
| private volatile bool isCompleted; | ||
| private List<Action>? continuations; | ||
|
|
||
| /// <summary> | ||
| /// Initializes a new instance of the <see cref="MyTask{TResult}"/> class. | ||
| /// </summary> | ||
| /// <param name="pool">The thread pool to queue continuations.</param> | ||
| /// <param name="func">The function to execute to produce the result.</param> | ||
| public MyTask(MyThreadPool pool, Func<TResult> func) | ||
| { | ||
| this.pool = pool ?? throw new ArgumentNullException(nameof(pool)); | ||
| this.func = func ?? throw new ArgumentNullException(nameof(func)); | ||
| this.continuations = []; | ||
| } | ||
|
|
||
| /// <inheritdoc/> | ||
| public bool IsCompleted => this.isCompleted; | ||
|
|
||
| /// <inheritdoc/> | ||
| public TResult Result | ||
| { | ||
| get | ||
| { | ||
| if (!this.isCompleted) | ||
| { | ||
| this.waitHandle.WaitOne(); | ||
| } | ||
|
|
||
| lock (this.@lock) | ||
| { | ||
| if (this.exception != null) | ||
| { | ||
| throw new AggregateException("Task failed with an exception.", this.exception); | ||
| } | ||
|
|
||
| return this.result; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// <inheritdoc/> | ||
| public IMyTask<TNewResult> ContinueWith<TNewResult>(Func<TResult, TNewResult> continuation) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(continuation); | ||
|
|
||
| var newTask = new MyTask<TNewResult>(this.pool, () => continuation(this.Result)); | ||
| lock (this.@lock) | ||
| { | ||
| if (this.isCompleted) | ||
| { | ||
| this.pool.QueueTask(newTask.GetExecuteAction()); | ||
| } | ||
| else | ||
| { | ||
| if (this.continuations == null) | ||
| { | ||
| throw new InvalidOperationException("Cannot add continuation to a completed task with cleared continuations."); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Она completed, но у неё isCompleted false, что? |
||
| } | ||
|
|
||
| this.continuations.Add(newTask.GetExecuteAction()); | ||
| } | ||
| } | ||
|
|
||
| return newTask; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Returns an Action that executes the task, stores the result or exception, and queues continuations. | ||
| /// </summary> | ||
| /// <returns>An Action to be executed by the thread pool.</returns> | ||
| public Action GetExecuteAction() | ||
| { | ||
| return () => | ||
| { | ||
| try | ||
| { | ||
| this.result = this.func(); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| lock (this.@lock) | ||
| { | ||
| this.exception = ex; | ||
| } | ||
| } | ||
| finally | ||
| { | ||
| lock (this.@lock) | ||
| { | ||
| this.isCompleted = true; | ||
| this.waitHandle.Set(); | ||
| if (this.continuations != null) | ||
| { | ||
| foreach (var continuation in this.continuations) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Если текущая задача завершилась исключением, continuation-ы по идее не должны запускаться, результата исходной задачи ведь нет. Наверное, надо им выставить исключение из "родительской" задачи и объявить их сделанными. |
||
| { | ||
| this.pool.QueueTask(continuation); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Если пул остановлен, то QueueTask бросит исключение, сюда (т.е. в поток из пула), так что "хозяева" задач-продолжений про него ничего не узнают и задедлочатся, вызвав Result. |
||
| } | ||
| } | ||
|
|
||
| this.continuations = null; | ||
| } | ||
| } | ||
| }; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Надо бы ещё тесты на разные другие случаи потенциальных гонок в пуле потоков, типа Submit одновременно с Shutdown, ContinueWith с Shutdown и т.п. И неплохо бы ещё тест на количество потоков в пуле.