I have implemented a basic binary heap. I wanted to see just how well it performed so I wrote a quick manual 'profiler':
public class MProfile : IDisposable
{
private static Dictionary<string, ProfilerEntry> _funcs = new Dictionary<string, ProfilerEntry>();
private Stopwatch _stopwatch;
private ProfilerEntry _entry;
public MProfile(string funcName)
{
if (!_funcs.ContainsKey(funcName))
{
_funcs.Add(funcName, new ProfilerEntry(funcName));
}
_entry = _funcs[funcName];
_stopwatch = Stopwatch.StartNew();
}
public void Dispose()
{
_stopwatch.Stop();
_entry.Record(_stopwatch.Elapsed.TotalMilliseconds);
}
}
The idea was to wrap it around functions calls with using and it would record the time taken. The ProfileEntry class is just a number of calls and total time taken. By storing them against the name, I can add them up.
Now, if I wrap it around my entire test:
Random random = new Random();
int count = 20000;
using (MProfile profile = new MProfile("HeapTest"))
{
PriorityHeap<PretendPathNode> heap = new PriorityHeap<PretendPathNode>(count);
for (int i = 0; i < count; i++)
{
int next = random.Next(-1000, 1000);
heap.Insert(new PretendPathNode(next));
}
while (heap.Count() > 0)
{
heap.Pop();
}
}
It will tell me that this took: 40.6682ms
However, if I add more profiler around the Insert and Pop calls, i.e:
using (MProfile profile2 = new MProfile("Insert"))
{
heap.Insert(new PretendPathNode(next));
}
using (MProfile profile3 = new MProfile("Pop"))
{
heap.Pop();
}
The total time taken is now: 452ms, with 107ms being from Insert and 131ms being from Pop (note: I've run these tests in huge loops and taken an average). I gather that my extra 'profiling' code will obviously have an impact, but how is it bloating the Insert and Pop times to above the original execution time? I thought they way I'd done the disposable meant that -only- the inner execution time would get recorded, which would still be exactly the same. The extra creating disposable, looking up the func in the dictionary and disposing happens -outside- of the Insert/Pop calls.
Is it to do with things like JIT and compile/run time optimizations? Has throwing in that disposable effectively ruined it? I thought maybe it was GC related but I tried a different profiler (static manual calls to start/stop) that had 0 garbage and it's the same...
Edit: I get the same times using this slightly more confusing code, which caches the MProfile objects and Stopwatch objects, so there is less creation/GC.
public class MProfile : IDisposable
{
private static Dictionary<string, ProfilerEntry> _funcs = new Dictionary<string, ProfilerEntry>();
private static Dictionary<string, Stopwatch> _stopwatches = new Dictionary<string, Stopwatch>();
private static Dictionary<string, MProfile> _profiles = new Dictionary<string, MProfile>();
private ProfilerEntry _entry;
private string _name;
public MProfile(string funcName)
{
_name = funcName;
_entry = new ProfilerEntry(funcName);
_funcs.Add(funcName, _entry);
}
public static MProfile GetProfiler(string funcName)
{
if (!_profiles.ContainsKey(funcName))
{
_profiles.Add(funcName, new MProfile(funcName));
_stopwatches.Add(funcName, new Stopwatch());
}
_stopwatches[funcName].Restart();
return _profiles[funcName];
}
public void Dispose()
{
_stopwatches[_name].Stop();
_entry.Record(_stopwatches[_name].Elapsed.TotalMilliseconds);
}
}
And calling it via:
using (profile2 = MProfile.GetProfiler("Insert"))
Related
In the below application there are two parties that are calling ChannelReservationCache to fetch or add information.
I want to use the "signal and wait" thing in my ChannelReservationCache class so that in case ChannelReservationCache.AddChannelState() is adding the cache, parallelly if the WebApi call hits the ChannelReservationCache.GetChannel() then the GetChannel() should wait for the execution of AddChannelState() and vice-versa.
How this can be done in ChannelReservationCache class?
Will there be any deadlock?
public class ChannelReservationCache
{
private readonly IDictionary<int, string> channelStates = new Dictionary<int, string>>();
private readonly object lockObject = new object();
private static readonly object lock = new object();
private static ChannelReservationCache instance = null;
private ChannelReservationCache() {}
public static ChannelReservationCache Instance
{
get
{
lock(lock) {
if (instance == null) {
instance = new ChannelReservationCache();
}
return instance;
}
}
}
public void AddChannelState(int level, string channel)
{
lock (this.lockObject)
{
//other code that makes the function take long time.
this.AddChannel(level, channel);
}
}
public Channel GetChannel(int level)
{
//other code that makes the function take long time.
Channel c = new Channel()
channelStates.TryGetValue(destinationId, out var c);
return c;
}
private void AddChannel(int level, string channel)
{
Channel c = new Channel();
c.ChannelName = channel;
c.IsActive = true;
channelStates.Add(level, resourceState)
}
}
public class Channel
{
public string ChannelName {get; set;}
public bool IsActive {get; set;}
}
public class RMQRequestHandler
{
public Task HandleChannelRequest(int level, Channel messages)
{
ChannelReservationCache.Instance.AddChannelState(level, messages)
}
}
[Route("api/v1")]
public class ChannnelController: ControllerBase
{
[HttpGet]
[Route("ChannelResource")]
public IActionResult GetChannelResource([FromQuery] int id)
{
ChannelReservationCache crc = ChannelReservationCache.Instance.GetChannel(id);
return this.Ok(crc);
}
}
First off there is a far simpler solution for you:
Simply change
private readonly IDictionary<int, string> channelStates = new Dictionary<int, string>();
To:
private readonly IDictionary<int, string> channelStates = new System.Collections.Concurrent.ConcurrentDictionary<int, string>();
//using ConcurrentDictionary instead of Dictionary
And forget about the thread concurrency locking etc...
In reality it is pretty hard to beat the performance of ConcurrentDictionary by writing our own locking structures to wrap a normal Dictionary. It is possible using ReaderWriterLockSlim to lock the dictionary and Interlocked to maintain a custom implementation for its count property. But this is a micro optimization that would only pay out over millions of itterations.
Now to answer your question:
One issue here:
AddChannelState is threadsafe but GetChannel is not
Think of it this way. Your writer is using a threadsafe lock but your reader is not. GetChannel also needs a lock in it.
Suggestion
If you are Not using Lazy < T > in your singleton then it is probably best to make the following change
Code Example below where the cost of thread synchronization is avoided after it has been initialized. Bearing in mind that Lock (or Monitor.Enter / Exit) is one of the most expensive operations to perform. Sure there will be minimum locking contentions once it is initialized however the memory barrier is enforced each and every time plus the Monitor is being checked.
Following reference link is discussing Interlocked but the context of the memory barrier cost is the same:
https://learn.microsoft.com/en-us/archive/msdn-magazine/2005/october/understanding-low-lock-techniques-in-multithreaded-apps
private static ChannelReservationCache instance;
private static readonly object lockInstance = new object();
//lock (lockInstance) enforces a memory barrier plus the monitor which is a cost you do not need to bear once the instance has been initialized
public static ChannelReservationCache Instance
{
get
{
if (instance == null)
{
lock (lockInstance)
{
if (instance == null)
{
instance = new ChannelReservationCache();
}
}
}
return instance;
}
}
I've been attempting to see how long functions take to execute in my code as practice to see where I can optimize. Right now I use a helper class that is essentially a stopwatch with a message to check these. The goal of this is that I should be able to wrap whatever method call I want in the helper and I'll get it's duration.
public class StopwatcherData
{
public long Time { get; set; }
public string Message { get; set; }
public StopwatcherData(long time, string message)
{
Time = time;
Message = message;
}
}
public class Stopwatcher
{
public delegate void CompletedCallBack(string result);
public static List<StopwatcherData> Data { get; set; }
private static Stopwatch stopwatch { get; set;}
public Stopwatcher()
{
Data = new List<StopwatcherData>();
stopwatch = new Stopwatch();
stopwatch.Start();
}
public static void Click(string message)
{
Data.Add(new StopwatcherData(stopwatch.ElapsedMilliseconds, message));
}
public static void Reset()
{
stopwatch.Reset();
stopwatch.Start();
}
}
Right now to use this, I have to call the Reset before the function I want so that the timer is restarted, and then call the click after it.
Stopwatcher.Reset()
MyFunction();
Stopwatcher.Click("MyFunction");
I've read a bit about delegates and actions, but I'm unsure of how to apply them to this situation. Ideally, I would pass the function as part of the Stopwatcher call.
//End Goal:
Stopwatcher.Track(MyFunction(), "MyFunction Time");
Any help is welcome.
It's not really a good idea to profile your application like that, but if you insist, you can at least make some improvements.
First, don't reuse Stopwatch, just create new every time you need.
Second, you need to handle two cases - one when delegate you pass returns value and one when it does not.
Since your Track method is static - it's a common practice to make it thread safe. Non-thread-safe static methods are quite bad idea. For that you can store your messages in a thread-safe collection like ConcurrentBag, or just use lock every time you add item to your list.
In the end you can have something like this:
public class Stopwatcher {
private static readonly ConcurrentBag<StopwatcherData> _data = new ConcurrentBag<StopwatcherData>();
public static void Track(Action action, string message) {
var w = Stopwatch.StartNew();
try {
action();
}
finally {
w.Stop();
_data.Add(new StopwatcherData(w.ElapsedMilliseconds, message));
}
}
public static T Track<T>(Func<T> func, string message) {
var w = Stopwatch.StartNew();
try {
return func();
}
finally {
w.Stop();
_data.Add(new StopwatcherData(w.ElapsedMilliseconds, message));
}
}
}
And use it like this:
Stopwatcher.Track(() => SomeAction(param1), "test");
bool result = Stopwatcher.Track(() => SomeFunc(param2), "test");
If you are going to use that with async delegates (which return Task or Task<T>) - you need to add two more overloads for that case.
Yes, you can create a timer function that accepts any action as a delegate. Try this block:
public static long TimeAction(Action action)
{
var timer = new Stopwatch();
timer.Start();
action();
timer.Stop();
return timer.ElapsedMilliseconds;
}
This can be used like this:
var elapsedMilliseconds = TimeAction(() => MyFunc(param1, param2));
This is a bit more awkward if your wrapped function returns a value, but you can deal with this by assigning a variable from within the closure, like this:
bool isSuccess ;
var elapsedMilliseconds = TimeToAction(() => {
isSuccess = MyFunc(param1, param2);
});
I've had this problem a while ago as well and was always afraid of the case that I'll leave errors when I change Stopwatcher.Track(() => SomeFunc(), "test")(See Evk's answer) back to SomeFunc(). So I tought about something that wraps it without changing it!
I came up with a using, which is for sure not the intended purpose.
public class OneTimeStopwatch : IDisposable
{
private string _logPath = "C:\\Temp\\OneTimeStopwatch.log";
private readonly string _itemname;
private System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
public OneTimeStopwatch(string itemname)
{
_itemname = itemname;
sw.Start();
}
public void Dispose()
{
sw.Stop();
System.IO.File.AppendAllText(_logPath, string.Format($"{_itemname}: {sw.ElapsedMilliseconds}ms{Environment.NewLine}"));
}
}
This can be used a easy way
using (new OneTimeStopwatch("test"))
{
//some sensible code not to touch
System.Threading.Thread.Sleep(1000);
}
//logfile with line "test: 1000ms"
I only need to remove 2 lines (and auto format) to make it normal again.
Plus I can easily wrap multiple lines here which isn't possible without defining new functions in the other approach.
Again, this is not recommended for terms of few miliseconds.
I'm trying to complete a programming exercise but one test fails. What have i done wrong?
The task is:
Write a program that manages robot factory settings.
When robots come off the factory floor, they have no name.
The first time you boot them up, a random name is generated, such as
RX837 or BC811.
Every once in a while we need to reset a robot to its factory
settings, which means that their name gets wiped. The next time you
ask, it gets a new name.
The tests for the program looks like this and the one that fails is the Different_robots_have_different_names:
public class RobotNameTest
{
private Robot robot;
[SetUp]
public void Setup()
{
robot = new Robot();
}
[Test]
public void Robot_has_a_name()
{
StringAssert.IsMatch(#"[A-Z]{2}\d{3}", robot.Name);
}
[Test]
public void Name_is_the_same_each_time()
{
Assert.That(robot.Name, Is.EqualTo(robot.Name));
}
[Test]
public void Different_robots_have_different_names()
{
var robot2 = new Robot();
Assert.That(robot.Name, Is.Not.EqualTo(robot2.Name));
}
[Test]
public void Can_reset_the_name()
{
var originalName = robot.Name;
robot.Reset();
Assert.That(robot.Name, Is.Not.EqualTo(originalName));
}
}
My code looks like this:
public class Robot
{
private List<string> Names = new List<string>();
private string name { get; set; }
public string Name { get { return this.name; } }
public Robot()
{
CreateName();
}
private void CreateName()
{
var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
var random = new Random();
StringBuilder sb = new StringBuilder();
sb.Append(
new string(
Enumerable.Repeat(chars, 3)
.Select(x => x[random.Next(x.Length)]).ToArray()
)
);
sb.Append(random.Next(100, 999));
if(Names.Any(word => word.Equals(sb.ToString())))
{
CreateName();
}
else
{
name = sb.ToString();
Names.Add(sb.ToString());
}
}
public void Reset()
{
this.name = "";
}
}
Making Names static passes all the tests:
private static List<string> Names = new List<string>();
Static keyword means that the member is being created on the class level, not the object.
In your case all Robots would share a link to a single list of Names. Removing the static keyword would result in Names list being create per Robot object. If you want to track how many times Robots constructor has been called, you should probably have a shared list of names, which is shared across all Robot object.
Your tests fails because when second object is created, it has a new empty Names list, which first Robot class knows nothing about. So in your case Names.Any(...) would always return false.
To read in more details consider msdn link above.
The mistake is not obvious. The Random class gets initialized with a random seed created from the current system time. The watch yielding the system time ticks slowly compared to the CPU clock frequency. Therefore it is well possible that random gets initialized twice with the same seed if new Random() is called to fast twice.
Solution: Declare random as static and initialize it only once.
public class Robot
{
private static Random random = new Random();
...
}
Now random is created only once no matter how many robots you create.
Say I have several List properties. Something Like this:
List<CustomerTypes> CustomerTypes {get; set;}
List<FormatTypes> FormatTypes {get; set;}
List<WidgetTypes> WidgetTypes {get; set}
List<PriceList> PriceList {get; set;}
Because these values update very rarely, I am caching them in my WCF Service at startup. I then have a service operation that can be called to refresh them.
The service operation will query them all from the database something like this:
// Get the data from the database.
var customerTypes = dbContext.GetCustomerTypes();
var formatTypes = dbContext.GetFormatTypes();
var widgetTypes = dbContext.GetWidgetTypes ();
var priceList = dbContext.GetPriceList ();
// Update the references
CustomerTypes = customerTypes;
FormatTypes = formatTypes;
WidgetTypes = widgetTypes;
PriceList = priceList;
This results in very little time that these are not all in sync. However, they are not fully thread safe. (A call could access a new CustomerType and an old PriceList.)
How can I make it so that while I am updating the references, any use of these lists has to wait until all references have been updated?
First put all of those lists in to a single container class.
Class TypeLists
{
List<CustomerTypes> CustomerTypes {get; set;}
List<FormatTypes> FormatTypes {get; set;}
List<WidgetTypes> WidgetTypes {get; set}
List<PriceList> PriceList {get; set;}
}
Then replace the old property accesses with a function call.
private readonly object _typeListsLookupLock = new object();
private volatile TypeLists _typeLists;
private volatile DateTime _typeListAge;
public TypeLists GetTypeList()
{
if(_typeLists == null || DateTime.UtcNow - _typeListAge > MaxCacheAge)
{
//The assignment of _typeLists is thread safe, this lock is only to
//prevent multiple concurrent database lookups. If you don't care that
//two threads could call GetNewTypeList() at the same time you can remove
//the lock and inner if check.
lock(_typeListsLookupLock)
{
//Check to see if while we where waiting to enter the lock someone else
//updated the lists and making the call to the database unnecessary.
if(_typeLists == null || DateTime.UtcNow - _typeListAge > MaxCacheAge)
{
_typeLists = GetNewTypeList();
_typeListAge = DateTime.UtcNow;
}
}
}
return _typeLists;
}
private TypeLists GetNewTypeList()
{
var container = new TypeLists()
using(var dbContext = GetContext())
{
container.CustomerTypes = dbContext.GetCustomerTypes();
container.FormatTypes = dbContext.GetFormatTypes();
container.WidgetTypes = dbContext.GetFormatTypes();
container.PriceList = dbContext.GetPriceList ();
}
return container;
}
The reason we change from a property to a function is you did
SomeFunction(myClass.TypeLists.PriceList, myClass.TypeLists.FormatTypes);
You could have TypeLists changed out from under you in a multi-threaded environment, however if you do
var typeLists = myClass.GetTypeLists();
SomeFunction(typeLists.PriceList, typeLists.FormatTypes);
that typeLists object is not mutated between threads so you do not need to worry about it's value changing out from under you, you could do var typeLists = myClass.TypeLists but making it a function makes it is more clear that you could potentially get different results between calls.
If you want to be fancy you can change GetTypeList() so it uses a MemoryCache to detect when it should expire the object and get a new one.
I thought it would be fun to put something together as an example. This answer is based on guidance from Marc Gravell's answer here.
The following class accepts a milliseconds value and provides an
event to notify the caller that the refresh interval has been hit.
It uses Environment.TickCount which is orders of magnitude faster
than using DateTime objects.
The double-checked lock prevents multiple threads from refreshing
concurrently and benefits from the reduced overhead of avoiding the
lock on every call.
Refreshing the data on the ThreadPool using Task.Run() allows the
caller to continue uninterrupted with the existing cached data.
using System;
using System.Threading.Tasks;
namespace RefreshTest {
public delegate void RefreshCallback();
public class RefreshInterval {
private readonly object _syncRoot = new Object();
private readonly long _interval;
private long _lastRefresh;
private bool _updating;
public event RefreshCallback RefreshData = () => { };
public RefreshInterval(long interval) {
_interval = interval;
}
public void Refresh() {
if (Environment.TickCount - _lastRefresh < _interval || _updating) {
return;
}
lock (_syncRoot) {
if (Environment.TickCount - _lastRefresh < _interval || _updating) {
return;
}
_updating = true;
Task.Run(() => LoadData());
}
}
private void LoadData() {
try {
RefreshData();
_lastRefresh = Environment.TickCount;
}
catch (Exception e) {
//handle appropriately
}
finally {
_updating = false;
}
}
}
}
Interlocked provides a fast, atomic replacement of the cached data.
using System.Collections.Generic;
namespace RefreshTest {
internal static class ContextCache {
private static readonly RefreshInterval _refresher = new RefreshInterval(60000);
private static List<int> _customerTypes = new List<int>();
static ContextCache() {
_refresher.RefreshData += RefreshData;
}
internal static List<int> CustomerTypes {
get {
_refresher.Refresh();
return _customerTypes;
}
}
private static void RefreshData() {
List<int> customerTypes = new List<int>(); //dbContext.GetCustomerTypes();
Interlocked.Exchange(ref _customerTypes, customerTypes);
}
}
}
Several million concurrent calls runs ~ 100ms (run your own tests though!):
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
namespace RefreshTest {
internal class Program {
private static void Main(string[] args) {
Stopwatch watch = new Stopwatch();
watch.Start();
List<Task> tasks = new List<Task>();
for (int i = 0; i < Environment.ProcessorCount; i++) {
Task task = Task.Run(() => Test());
tasks.Add(task);
}
tasks.ForEach(x => x.Wait());
Console.WriteLine("Elapsed Milliseconds: {0}", watch.ElapsedMilliseconds);
Console.ReadKey();
}
private static void Test() {
for (int i = 0; i < 1000000; i++) {
var a = ContextCache.CustomerTypes;
}
}
}
}
Hope that helps.
If you have a simple scenario maybe you can use a HACK.
Programatically edit your web.config (not important what you edit, you can invent a counter or go from 0 to 1 or back from 1 to 0 on some invented appSetting).
Look here for example.
This will allow all existing requests to finish and then it will restart your app domain inside IIS.
At start of new app domain data from db will be reloaded into your lists.
Be WARNED that your'll also experience a delay on starting of new app domain (on 1st request, jitting IL again) and you will also loose your data in Session, Application, etc.
Advantage is that when running you don't have any performance hit because of locking.
I have a method which needs to run exclusivley run a block of code, but I want to add this restriction only if it is really required. Depending on an Id value (an Int32) I would be loading/modifying distinct objects, so it doesn't make sense to lock access for all threads. Here's a first attempt of doing this -
private static readonly ConcurrentDictionary<int, Object> LockObjects = new ConcurrentDictionary<int, Object>();
void Method(int Id)
{
lock(LockObjects.GetOrAdd(Id,new Object())
{
//Do the long running task here - db fetches, changes etc
Object Ref;
LockObjects.TryRemove(Id,out Ref);
}
}
I have my doubts if this would work - the TryRemove can fail (which will cause the ConcurrentDictionary to keep getting bigger).
A more obvious bug is that the TryRemove successfully removes the Object but if there are other threads (for the same Id) which are waiting (locked out) on this object, and then a new thread with the same Id comes in and adds a new Object and starts processing, since there is no one else waiting for the Object it just added.
Should I be using TPL or some sort of ConcurrentQueue to queue up my tasks instead ? What's the simplest solution ?
I use a similar approach to lock resources for related items rather than a blanket resource lock... It works perfectly.
Your almost there but you really don't need to remove the object from the dictionary; just let the next object with that id get the lock on the object.
Surely there is a limit to the number of unique ids in your application? What is that limit?
The main semantic issue I see is that an object can be locked without being listed in the collection because the the last line in the lock removes it and a waiting thread can pick it up and lock it.
Change the collection to be a collection of objects that should guard a lock. Do not name it LockedObjects and do not remove the objects from the collection unless you no longer expect the object to be needed.
I always think of this type of objects as a key instead of a lock or blocked object; the object is not locked, it is a key to locked sequences of code.
I used the following approach. Do not check the original ID, but get small hash-code of int type to get the existing object for lock. The count of lockers depends on your situation - the more locker counter, the less the probability of collision.
class ThreadLocker
{
const int DEFAULT_LOCKERS_COUNTER = 997;
int lockersCount;
object[] lockers;
public ThreadLocker(int MaxLockersCount)
{
if (MaxLockersCount < 1) throw new ArgumentOutOfRangeException("MaxLockersCount", MaxLockersCount, "Counter cannot be less, that 1");
lockersCount = MaxLockersCount;
lockers = Enumerable.Range(0, lockersCount).Select(_ => new object()).ToArray();
}
public ThreadLocker() : this(DEFAULT_LOCKERS_COUNTER) { }
public object GetLocker(int ObjectID)
{
var idx = (ObjectID % lockersCount + lockersCount) % lockersCount;
return lockers[idx];
}
public object GetLocker(string ObjectID)
{
var hash = ObjectID.GetHashCode();
return GetLocker(hash);
}
public object GetLocker(Guid ObjectID)
{
var hash = ObjectID.GetHashCode();
return GetLocker(hash);
}
}
Usage:
partial class Program
{
static ThreadLocker locker = new ThreadLocker();
static void Main(string[] args)
{
var id = 10;
lock(locker.GetLocker(id))
{
}
}
}
Of cource, you can use any hash-code functions to get the corresponded array index.
If you want to use the ID itself and do not allow collisions, caused by hash-code, you can you the next approach. Maintain the Dictionary of objects and store info about the number of the threads, that want to use ID:
class ThreadLockerByID<T>
{
Dictionary<T, lockerObject<T>> lockers = new Dictionary<T, lockerObject<T>>();
public IDisposable AcquireLock(T ID)
{
lockerObject<T> locker;
lock (lockers)
{
if (lockers.ContainsKey(ID))
{
locker = lockers[ID];
}
else
{
locker = new lockerObject<T>(this, ID);
lockers.Add(ID, locker);
}
locker.counter++;
}
Monitor.Enter(locker);
return locker;
}
protected void ReleaseLock(T ID)
{
lock (lockers)
{
if (!lockers.ContainsKey(ID))
return;
var locker = lockers[ID];
locker.counter--;
if (Monitor.IsEntered(locker))
Monitor.Exit(locker);
if (locker.counter == 0)
lockers.Remove(locker.id);
}
}
class lockerObject<T> : IDisposable
{
readonly ThreadLockerByID<T> parent;
internal readonly T id;
internal int counter = 0;
public lockerObject(ThreadLockerByID<T> Parent, T ID)
{
parent = Parent;
id = ID;
}
public void Dispose()
{
parent.ReleaseLock(id);
}
}
}
Usage:
partial class Program
{
static ThreadLockerByID<int> locker = new ThreadLockerByID<int>();
static void Main(string[] args)
{
var id = 10;
using(locker.AcquireLock(id))
{
}
}
}
There are mini-libraries that do this for you, such as AsyncKeyedLock. I've used it and it saved me a lot of headaches.