From 27d1d5982dc69795f1e0a29ab1f8ddaf3a34bc63 Mon Sep 17 00:00:00 2001 From: MattMo Date: Mon, 24 Jul 2023 10:43:48 -0700 Subject: [PATCH] Added initial code and documentation along with the initial set of tests. --- .gitignore | 263 +++++++++++++++++++ Logo_Symbol_Black_Outline.png | Bin 0 -> 21011 bytes Parallel.Net.Tests/Parallel.Net.Tests.csproj | 32 +++ Parallel.Net.Tests/ParallelTests.cs | 136 ++++++++++ Parallel.Net.sln | 31 +++ Parallel.Net/Parallel.Net.csproj | 30 +++ Parallel.Net/ParallelTask.cs | 124 +++++++++ Parallel.Net/ParallelTaskExtensions.cs | 28 ++ Parallel.Net/ParallelTaskT.cs | 149 +++++++++++ 9 files changed, 793 insertions(+) create mode 100644 .gitignore create mode 100644 Logo_Symbol_Black_Outline.png create mode 100644 Parallel.Net.Tests/Parallel.Net.Tests.csproj create mode 100644 Parallel.Net.Tests/ParallelTests.cs create mode 100644 Parallel.Net.sln create mode 100644 Parallel.Net/Parallel.Net.csproj create mode 100644 Parallel.Net/ParallelTask.cs create mode 100644 Parallel.Net/ParallelTaskExtensions.cs create mode 100644 Parallel.Net/ParallelTaskT.cs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f5fbf7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,263 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +project.fragment.lock.json + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +#*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +#Mac Store files +.DS_Store diff --git a/Logo_Symbol_Black_Outline.png b/Logo_Symbol_Black_Outline.png new file mode 100644 index 0000000000000000000000000000000000000000..d8ecce0a7438153f312d4c3a5e491de2e253e931 GIT binary patch literal 21011 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 aW_}3U6hz#}4}8?Kcfsi-0atm#FZn;;*N5N$ literal 0 HcmV?d00001 diff --git a/Parallel.Net.Tests/Parallel.Net.Tests.csproj b/Parallel.Net.Tests/Parallel.Net.Tests.csproj new file mode 100644 index 0000000..303973d --- /dev/null +++ b/Parallel.Net.Tests/Parallel.Net.Tests.csproj @@ -0,0 +1,32 @@ + + + + net6.0 + enable + enable + + false + true + MontoyaTech.Parallel.Net.Tests + MontoyaTech.Parallel.Net.Tests + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/Parallel.Net.Tests/ParallelTests.cs b/Parallel.Net.Tests/ParallelTests.cs new file mode 100644 index 0000000..89e0f1a --- /dev/null +++ b/Parallel.Net.Tests/ParallelTests.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; +using FluentAssertions; +using MontoyaTech.Parallel.Net; + +namespace MontoyaTech.Parallel.Net.Tests +{ + public class ParallelTests + { + [Fact] + public void Parallel_NoMaxTasks_Should_Work() + { + var inputs = new List() { 0, 1, 2, 3, 4 }; + + var results = inputs.Parallel(input => input * 5); + + results.Count.Should().Be(inputs.Count); + + results.Should().Contain(0); + results.Should().Contain(5); + results.Should().Contain(10); + results.Should().Contain(15); + results.Should().Contain(20); + } + + [Fact] + public void Parallel_MaxTasks_Should_Work() + { + var inputs = new List() { 0, 1, 2, 3, 4 }; + + var results = inputs.Parallel(input => input * 5, maxTasks: 2); + + results.Count.Should().Be(inputs.Count); + + results.Should().Contain(0); + results.Should().Contain(5); + results.Should().Contain(10); + results.Should().Contain(15); + results.Should().Contain(20); + } + + [Fact] + public void Parallel_MaxTasks_Zero_Ignore_Should_Work() + { + var inputs = new List() { 0, 1, 2, 3, 4 }; + + var results = inputs.Parallel(input => input * 5, maxTasks: 0); + + results.Count.Should().Be(inputs.Count); + + results.Should().Contain(0); + results.Should().Contain(5); + results.Should().Contain(10); + results.Should().Contain(15); + results.Should().Contain(20); + } + + [Fact] + public void Parallel_Timeout_Should_Work() + { + var inputs = new List() { 0, 1, 2, 3, 4 }; + + var action = new Action(() => inputs.Parallel(input => input * 5, timeout: TimeSpan.FromSeconds(0))); + + action.Should().Throw(); + } + + [Fact] + public void Parallel_Timeout_NoException_Should_Work() + { + var inputs = new List() { 0, 1, 2, 3, 4 }; + + var action = new Action(() => inputs.Parallel(input => input * 5, timeout: TimeSpan.FromSeconds(0), throwOnTimeout: false)); + + action.Should().NotThrow(); + } + + [Fact] + public void Parallel_NoTimeout_Exception_Should_Work() + { + var inputs = new List() { 0, 1, 2, 3, 4 }; + + var action = new Action(() => inputs.Parallel(input => input * 5)); + + action.Should().NotThrow(); + } + + [Fact] + public void Parallel_Exception_Should_Bubble() + { + var inputs = new List() { 0, 1, 2, 3, 4 }; + + var results = new List(); + + var action = new Action(() => results = inputs.Parallel(input => { + if (input == 0) + throw new Exception("Testing"); + + return input * 5; + })); + + action.Should().Throw(); + + results.Count.Should().Be(0); + + inputs.RemoveAt(0); + + action.Should().NotThrow(); + + results.Count.Should().BeGreaterThan(0); + } + + [Fact] + public void Parallel_Exception_NoBubble_Should_Work() + { + var inputs = new List() { 0, 1, 2, 3, 4 }; + + var results = new List(); + + var action = new Action(() => results = inputs.Parallel(input => { + if (input == 0) + throw new Exception("Testing"); + + return input * 5; + }, bubbleExceptions: false)); + + action.Should().NotThrow(); + + results.Count.Should().BeGreaterThan(0); + } + } +} diff --git a/Parallel.Net.sln b/Parallel.Net.sln new file mode 100644 index 0000000..e2d19b0 --- /dev/null +++ b/Parallel.Net.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.6.33815.320 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Parallel.Net", "Parallel.Net\Parallel.Net.csproj", "{985B6E54-9C5F-4560-BF0A-8EAF63ACEF44}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Parallel.Net.Tests", "Parallel.Net.Tests\Parallel.Net.Tests.csproj", "{5F1D1944-9FBC-40F6-8861-083839CEC00A}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {985B6E54-9C5F-4560-BF0A-8EAF63ACEF44}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {985B6E54-9C5F-4560-BF0A-8EAF63ACEF44}.Debug|Any CPU.Build.0 = Debug|Any CPU + {985B6E54-9C5F-4560-BF0A-8EAF63ACEF44}.Release|Any CPU.ActiveCfg = Release|Any CPU + {985B6E54-9C5F-4560-BF0A-8EAF63ACEF44}.Release|Any CPU.Build.0 = Release|Any CPU + {5F1D1944-9FBC-40F6-8861-083839CEC00A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5F1D1944-9FBC-40F6-8861-083839CEC00A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5F1D1944-9FBC-40F6-8861-083839CEC00A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5F1D1944-9FBC-40F6-8861-083839CEC00A}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {A1921B0D-A0B8-4094-8B29-6B23CEAE18C1} + EndGlobalSection +EndGlobal diff --git a/Parallel.Net/Parallel.Net.csproj b/Parallel.Net/Parallel.Net.csproj new file mode 100644 index 0000000..d098137 --- /dev/null +++ b/Parallel.Net/Parallel.Net.csproj @@ -0,0 +1,30 @@ + + + + net6.0 + enable + enable + MontoyaTech.Parallel.Net + MontoyaTech.Parallel.Net + True + MontoyaTech.Parallel.Net + A simple library with a set of extensions for helping do work on a set of items in parallel. + MontoyaTech + https://code.montoyatech.com/MontoyaTech/Parallel.Net + Logo_Symbol_Black_Outline.png + https://code.montoyatech.com/MontoyaTech/Parallel.Net + True + + + + + True + \ + + + + + + + + diff --git a/Parallel.Net/ParallelTask.cs b/Parallel.Net/ParallelTask.cs new file mode 100644 index 0000000..df39276 --- /dev/null +++ b/Parallel.Net/ParallelTask.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MontoyaTech.Parallel.Net +{ + /// + /// A set of helper functions for working with ParallelTasks. + /// + public class ParallelTask + { + /// + /// Takes a set of inputes and creates a set of parallel tasks and runs them returning the results. The results will be out of order. + /// + /// The input type + /// The result type + /// The list of inputs to process + /// The function to run against each input and produce a result + /// The max number of tasks to run in parallel. If null the number of inputs becomes the number of tasks. Default is null. + /// If set this function will return early if the timeout is reached even if the tasks are not done. Default is null. + /// If true an exception will be thrown if a timeout is reached if one was set. Default is true. + /// If true, rethrows any exceptions from the running tasks. Default is true. + /// The list of results from the inputs processed. + public static List ForEach(IList inputs, Func func, int? maxTasks = null, TimeSpan? timeout = null, bool throwOnTimeout = true, bool bubbleExceptions = true) + { + if (inputs == null || inputs.Count == 0) + return new List(); + + var tasks = new List, List>>(); + + if (!maxTasks.HasValue) + maxTasks = inputs.Count; + else if (maxTasks <= 0) + maxTasks = 1; + + int itemsPerChunk = (inputs.Count / maxTasks.Value) + 1; + + int itemIndex = 0; + + for (int i = 0; i < maxTasks.Value; i++) + { + var chunk = new List(); + + for (int p = 0; p < itemsPerChunk; p++) + { + chunk.Add(inputs[itemIndex++]); + + if (itemIndex >= inputs.Count) + break; + } + + if (chunk.Count > 0) + { + tasks.Add(new ParallelTask, List>(inputs => + { + var results = new List(); + + if (inputs != null) + for (int i = 0; i < inputs.Count; i++) + results.Add(func(inputs[i])); + + return results; + }, chunk)); + } + + if (itemIndex >= inputs.Count) + break; + } + + if (tasks.Count == 0) + return new List(); + + var chunkResults = WhenAll(tasks, timeout, throwOnTimeout); + + if (bubbleExceptions) + for (int i = 0; i < tasks.Count; i++) + if (tasks[i].Failed && tasks[i].ThrownException != null) + throw tasks[i].ThrownException; + + var results = new List(); + + foreach (var chunk in chunkResults) + if (chunk != null) + results.AddRange(chunk); + + return results; + } + + /// + /// Runs a list of tasks and waits until they have been completed or failed and returns the results. + /// + /// The input type + /// The result type + /// The list of tasks to run + /// If set this function will leave early if the timeout is met. Default is null. + /// If set an exception will be thrown if a timeout is reached. Default is true. + /// The list of results from the tasks. + /// A timeout exception is thrown if throwOnTimeout is true and a timeout is reached. + public static List WhenAll(IList> tasks, TimeSpan? timeout = null, bool throwOnTimeout = true) + { + if (tasks == null || tasks.Count == 0) + return new List(); + + var countDown = new CountdownEvent(tasks.Count); + + foreach (var task in tasks) + task.Run(timeout, false, throwOnTimeout, countDown); + + if (!timeout.HasValue) + countDown.Wait(); + else if (timeout.HasValue && !countDown.Wait(timeout.Value) && throwOnTimeout) + throw new Exception($"ParallelTasks did not complete within {timeout.Value.TotalMilliseconds} miliseconds"); + + var results = new List(); + + foreach (var task in tasks) + results.Add(task.Result); + + return results; + } + } +} diff --git a/Parallel.Net/ParallelTaskExtensions.cs b/Parallel.Net/ParallelTaskExtensions.cs new file mode 100644 index 0000000..4b87967 --- /dev/null +++ b/Parallel.Net/ParallelTaskExtensions.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MontoyaTech.Parallel.Net +{ + public static class ParallelTaskExtensions + { + /// + /// Process a list of items in parallel and returns the results. The results will be out of order. + /// + /// Input type + /// Result type + /// The list of items to process in parallel + /// The function to take an input and return a result + /// The max tasks to run in parallel if specified. Default is null. If null the number of items will be the number of tasks + /// If set a timeout will be thrown if this doesnt complete in time. Default is null + /// Whether or not to throw an exception on timeout. Default is true + /// If true, rethrows any exceptions from the running tasks. Default is true. + /// A list of results from the items processed. + public static List Parallel(this IList list, Func func, int? maxTasks = null, TimeSpan? timeout = null, bool throwOnTimeout = true, bool bubbleExceptions = true) + { + return ParallelTask.ForEach(list, func, maxTasks, timeout, throwOnTimeout, bubbleExceptions); + } + } +} diff --git a/Parallel.Net/ParallelTaskT.cs b/Parallel.Net/ParallelTaskT.cs new file mode 100644 index 0000000..82b0fb4 --- /dev/null +++ b/Parallel.Net/ParallelTaskT.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MontoyaTech.Parallel.Net +{ + /// + /// The outline of a ParallelTask that takes in an input, and produces and output when ran. + /// + /// + /// + public class ParallelTask + { + /// + /// Whether or not this parallel task is currently running. + /// + public bool Running + { + get + { + return this.running && !this.completed && !this.failed; + } + } + + /// + /// Whether or not this parallel task completed successfully. + /// + public bool Completed + { + get + { + return this.completed; + } + } + + /// + /// Whether or not this parallel task failed due to an exception. + /// + public bool Failed + { + get + { + return this.failed; + } + } + + /// + /// The result of this parallel task if it completed successfully. + /// + public ResultT Result + { + get + { + return this.result; + } + } + + /// + /// The exception that was thrown while running this parallel task if it failed. + /// + public Exception ThrownException + { + get + { + return this.thrownException; + } + } + + protected bool running = false; + + protected bool completed = false; + + protected bool failed = false; + + protected Func work = null; + + protected InputT input = default(InputT); + + protected ResultT result = default(ResultT); + + protected bool exceptionStopped = false; + + protected ManualResetEvent handle; + + protected Exception thrownException = null; + + /// + /// Creates a new parallel task with the worker function and input. + /// + /// + /// + public ParallelTask(Func work, InputT input) + { + this.work = work; + + this.input = input; + } + + /// + /// Runs this parallel task and returns the result when it completes. + /// + /// Default is null, if set, this function will return if the timeout is reached regardless if the task is complete. + /// Default is true, whether or not to block the caller function and not return until this task is complete. + /// Whether or not to throw an exception if a timeout is reached, if one was specified. Default is true. + /// An optional count down that if specified is signaled when the task completes. + /// The result from the worker function ran in this task. + /// Throws a timeout exception if the task doesn't complete in time. + public ResultT Run(TimeSpan? timeout = null, bool blocking = true, bool throwOnTimeout = true, CountdownEvent countDown = null) + { + this.running = true; + + this.handle = new ManualResetEvent(false); + + ThreadPool.QueueUserWorkItem((object state) => + { + try + { + this.result = this.work((InputT)state); + + this.completed = true; + } + catch (Exception ex) + { + this.failed = true; + + this.thrownException = ex; + } + + this.handle.Set(); + + if (countDown != null) + countDown.Signal(); + + }, this.input); + + if (blocking) + { + if (!timeout.HasValue) + this.handle.WaitOne(); + else if (!this.handle.WaitOne(timeout.Value) && throwOnTimeout) + throw new Exception($"ParallelTask did not complete within {timeout.Value.TotalMilliseconds} miliseconds"); + } + + return this.result; + } + } +}