Code Monkey home page Code Monkey logo

jcd.threading's Introduction

Jcd.Threading

A netstandard2.0 library that provides utility classes to help simplify some aspects of multi-threading and synchronization.

Read the API documentation carefully.

Features

  • TaskScheduler extension Run to mimic the Task.Run API, ensuring tasks are run with the desired scheduler.
  • IdleTaskScheduler, a custom task scheduler that schedules tasks in a round robin manner with idle threads.
  • ThreadWrapper class to simplify the process of making a pauseable thread.
  • ItemProcessor<TItem> class encapsulating a queue+worker thread.
  • Various Lock extension methods to simplify using synchronization primitives ( e.g. SemaphoreSlimExtensions)
  • Various synchronized value holder generics to automatically use and release a specific locking mechanism when getting or setting a value. (e.g. ReaderWriterLockSlimValue)

Examples

Execute TaskSchedulerExtensions.Run on a default instance of IdleTaskScheduler configured to provide 4 STA threads.

using Jcd.Threading;
using Jcd.Threading.Tasks;

var its = new IdleTaskScheduler(4,ApartmentState.STA,"My Scheduler");
its.Run(()=>{/* do some work.*/});

Use SemaphoreSlimExtensions.Lock to automatically call Wait and Release

using Jcd.Threading;

var sem = new SemaphoreSlim(1,1);
using (sem.Lock()) // This calls sem.Wait
{
   // This is your critical section. 
   // Do only what needs to be done
   // while the lock is held. Do everything 
   // else before or after.
}
// once .Dispose is called, sem.Release() is called.

Use ReaderWriterLockSlimValue to synchronize reads and writes to a single shared value. Useful for needing exclusive write access for multiple readers whose use case can tolerate slightly stale data, such as thread specific status updates.

using Jcd.Threading.SynchronizedValues;

const int initialValue=20;
var rwlsv = new ReaderWriterLockSlimValue<int>(initialValue);

// writer thread.
var wt = Task.Run(()=>{
   var sw = StopWatch.StartNew();
   int i=0;
   do
   {
      i++;
      rwlsv.Value = i;   // here we communicate thread-local information blocking all reads as its written.
      Thread.Sleep(100); // wait 0.1 seconds 
   }
   while(sw.ElapsedMilliseconds < 1000)
});

// reader thread 1.
var rt1 = Task.Run(()=>{
   var sw=StopWatch.StartNew();
   do
   {
      // here we read the data maintained by the writer thread, blocking writes as its read.
      Console.WriteLine($"[1] The count is: {rwlsv.Value}");
      Thread.Sleep(71); 
   }
   while(sw.ElapsedMilliseconds < 750)
});

// reader thread 2.
var rt1 = Task.Run(()=>{
   var sw=StopWatch.StartNew();
   do
   {
      // here we read the data maintained by the writer thread, blocking writes as its read.
      Console.WriteLine($"[2] The count is: {rwlsv.Value}");
      Thread.Sleep(50); 
   }
   while(sw.ElapsedMilliseconds < 1500)
});

// wait for all threads to finish.
await Task.WhenAll(new[]{wt,rt1,rt2});

These were just an overview of what's available. See EXAMPLES.md for more and detailed examples.

And as always, read the API documentation

Badges

GitHub Build status CodeFactor Grade Quality Gate Status

MyGet Nuget

API Docs

jcd.threading's People

Contributors

jason-c-daniels avatar

Watchers

 avatar

jcd.threading's Issues

CHORE: Analyze original potential use cases for SynchronizedValue: ChangeValue, ChangeValueAsync, Do, DoAsync for viability

A meaningful API surface is essential to the success of any library. To that end some methods are potentially unsuitable in SynchronizedValue. These need to have their original potential use cases examined to see if they're valid and significant enough to warrant their continued existence. If not they should be removed, along with unit tests, example code and documentation referencing them as part of a separate work item.

Add some simplifying methods to help synchronize access to resources via using standard primitives.

Request Summary

SemaphoreSlim, ReaderWriterLockSlim, and others have some well documented methods for implementing exclusive access to a resource. Identify and implement ways to simplify adherence to common mutual exclusion practices using these primitives.

In its simplest form synchronized access to a resource follows the pattern below:

SynchronizationPrimitive primitive= new();

// locking thread.
try {
     primitive.Wait();
     primitive.AcquireLock()
     // app code, do something.
}
finally 
{
   // this is sometimes multiple steps. Finally ensures this gets called even for exceptions.
   primitive.ReleaseLock();
}


// waiting thread.

try {
     primitive.Wait();
     primitive.AcquireLock()
     // app code, do something after lock is released.
}
finally 
{
   // this is sometimes multiple steps. Finally ensures this gets called even for exceptions.
   primitive.ReleaseLock();
}

However, it's remarkably easy to forgot the pattern, especially in light of the disparate API signatures among some of the dotnet synchronization primitives. For example ReaderWriterLockSlim has multiple types of locks, which can be upgraded to...etc, These need correct releasing based on what happened after the lock was acquired. While, SemaphoreSlim only has Wait and Release.

Resolution

Provide a set of extension methods that encapsulate and coordinate these calls, will help ensure these lock-release pairings are properly executed. Extension methods allow the point of use to write less code and improve readability.

Alternatives

Option 1: use an IDisposable to ensure release of the locks.

Choosing to implement this in an extension that returns a disposable would resemble:

public static class SynchronizationPrimitiveExtensions
{
   public IDisposable UseExclusively(this SynchronizationPrimitive primitive) => {
      primitive.Wait();

      return new SynchronizationPrimitiveLock(primitive);
   }
}

internal class SynchronizationPrimitiveLock : IDisposable
{
   private SynchronizationPrimitive primitive;

   internal SynchronizationPrimitiveLock(SynchronizationPrimitive primitive)
   {
      this.primitive = primitive;
      primitive.AcquireLock();
   }

   public void Dispose() { primitive.ReleaseLock(); }
}

Using the IDisposable instances changes the original code into:

SynchronizationPrimitive primitive= new();

// locking thread.
using(primitive.UseExclusively() 
{
     // app code, do something.
}

// waiting thread.
using(primitive.UseExclusively() 
{
     // app code, do something after lock is released.
}

Option 2: Use delegates in a wrapper method calling try..finally

public static class SynchronizationPrimitiveExtensions
{
   public void UseExclusively(this SynchronizationPrimitive primitive, Action action) {
      try
      {
         primitive.Wait();
         primitive.AcquireLock();
         action();
      }
      finally
      {
         primitive.ReleaseLock();
      }
   }
}

With the above pattern the point of use would look like:

SynchronizationPrimitive primitive= new();

// locking thread.
primitive.UseExclusively(()=> 
{
     // app code, do something.
});

// waiting thread.
primitive.UseExclusively(()=> 
{
     // app code, do something after lock is released.
});

delegate vs IDisposable Considerations

  • IDisposable concerns:

    • Moderate concern: Failure to call Dispose when done with a resource may cause deadlocks and will cause locking for longer than intended. The risk of this happening is mitigated some with using var val = style of using syntax.
    • Minor concern: Done in a time consuming method using var val = primitive.UseExclusively(); can cause a resource to be locked longer than intended, but will get released.
  • Delegate concerns really only exist for captured variables (i.e. a closure).

    • Moderate concern: Creating a closures takes a non-trivial amount of CPU time. Depending on the number of captured variables, this is slower than creating a trivial IDisposable which is important in performance-sensitive code executing in a tight loop[1].
    • Moderate concern: In a tight loop, executing closures also exert GC pressure proportional to the number of captured values and and number of scopes (more scopes = more generated capture classes; more captured variables at each scope = more fields in the generated capture classes). NOTE: ReSharper DPA routinely warns me about this when I'm analyzing code with closures. (According to this article they can be 50% slower than simple delegates)
    • Minor concern: Automated code formatters (especially those in a git hook) may indent the delegate's code in unexpected ways.

[1] - Performance sensitive code is code we want to ensure we're not imposing unnecessary CPU burdens on, but which is not performance-critical. Performance-critical code should always directly deal with the primitives.

Motivation

I've found myself trying this same pattern, over and over, when looking into selecting synchronization primitives. For non-performance critical code, simplifying improve code readability, and help ensure correct usage would be immensely useful.

Create Comprehensive Example Code and Documentation

User Story

As a library consumer, I want comprehensive example code, so I can understand how to appropriately use the library.

Acceptance Criteria

  • All public classes have example usage in code to demonstrate how to use the class.
  • All public and protected methods on classes have example usage code.
  • All public classes have example usage documentation in the API documentation discussing the reasons for or for not using a class in particular ways.
  • All public and protected methods on have example usage documentation in the API documentation discussing the reasons for or for not using a method in particular ways.

Work Ready Checklist

    • Acceptance criteria defined
    • Team understands acceptance criteria
    • Team has defined solution / steps to satisfy acceptance criteria
    • Acceptance criteria is verifiable / testable
    • External / 3rd Party dependencies identified (none)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.