commit 818f2a2a7adcf2cf5e726676c9153f415a9eadbd Author: Harald Wolff-Thobaben Date: Wed Nov 18 00:23:20 2020 +0100 Initial Commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cd51a9f --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +# Autosave files +*~ + +# build +[Oo]bj/ +[Bb]in/ +packages/ +TestResults/ + +# globs +Makefile.in +*.DS_Store +*.sln.cache +*.suo +*.cache +*.pidb +*.userprefs +*.usertasks +config.log +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.user +*.tar.gz +tarballs/ +test-results/ +Thumbs.db +.vs/ + +# Mac bundle stuff +*.dmg +*.app + +# resharper +*_Resharper.* +*.Resharper + +# dotCover +*.dotCover diff --git a/CCALock.cs b/CCALock.cs new file mode 100644 index 0000000..3d013b1 --- /dev/null +++ b/CCALock.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Threading; + +namespace ln.threading +{ + public enum CCAState { + READ, // Read access may be acquired + WAIT, // Write access is to be acquired after current reads are released + WRITE // Write access is acquired + } + public class CCALock + { + public CCAState State { get; private set; } + + public IEnumerable Waiting => waiting; + public IEnumerable Locks => locks; + + HashSet locks = new HashSet(); + Lock writeWait; + HashSet waiting = new HashSet(); + + public CCALock() + {} + + public Lock AcquireRead() => AcquireLock(false, -1); + public Lock AcquireWrite() => AcquireLock(true, -1); + public Lock AcquireRead(int timeout) => AcquireLock(false, timeout); + public Lock AcquireWrite(int timeout) => AcquireLock(true, timeout); + + public Lock AcquireLock(bool writeaccess,int timeout) + { + Lock lck = new Lock(this); + lock (this) + { + WaitForRead(lck, timeout); + + if (writeaccess) + { + writeWait = lck; + + if (locks.Count > 0) + { + State = CCAState.WAIT; + + lock (writeWait) + { + Monitor.Exit(this); + if (!Monitor.Wait(writeWait, timeout)) + { + Monitor.Enter(this); + State = CCAState.READ; + throw new TimeoutException(); + } + Monitor.Enter(this); + } + } + State = CCAState.WRITE; + } + + locks.Add(lck); + lck.Acquired = true; + } + + return lck; + } + + private void WaitForRead(Lock lck,int timeout) + { + while (State != CCAState.READ) + { + waiting.Add(lck); + if (!Monitor.Wait(this, timeout)) + throw new TimeoutException(); + waiting.Remove(lck); + } + } + + public void ReleaseLock(Lock lck) + { + lock (this) + { + locks.Remove(lck); + if ((State == CCAState.WAIT) && (locks.Count == 0)) + { + lock (writeWait) + { + Monitor.Pulse(writeWait); + } + } + else if (State == CCAState.WRITE) + { + if (writeWait != lck) + throw new LockingException(); + + writeWait = null; + State = CCAState.READ; + Monitor.PulseAll(this); + } + } + } + + public class Lock : IDisposable + { + public CCALock CCALock { get; } + + public bool Acquired { get; internal set; } + + public Lock(CCALock cca) + { + CCALock = cca; + } + + public void Dispose() + { + if (Acquired) + CCALock.ReleaseLock(this); + } + } + + } +} diff --git a/DisposableLock.cs b/DisposableLock.cs new file mode 100644 index 0000000..a6a931a --- /dev/null +++ b/DisposableLock.cs @@ -0,0 +1,20 @@ +using System; +using System.Threading; +namespace ln.threading +{ + public class DisposableLock : IDisposable + { + public object LockedObject { get; } + + public DisposableLock(object lockedObject) + { + LockedObject = lockedObject; + Monitor.Enter(lockedObject); + } + + public void Dispose() + { + Monitor.Exit(LockedObject); + } + } +} diff --git a/DynamicPool.cs b/DynamicPool.cs new file mode 100644 index 0000000..26c72c9 --- /dev/null +++ b/DynamicPool.cs @@ -0,0 +1,61 @@ +using System; +using System.Threading; +namespace ln.threading +{ + public class DynamicPool : Pool + { + public int Timeout { get; set; } + + public DynamicPool() :this(Environment.ProcessorCount){} + public DynamicPool(int maxPoolSize) + :base(maxPoolSize) + { + Timeout = 15000; + } + + public override void Start() + { + if ((State == PoolState.RUN) || (State == PoolState.SHUTDOWN)) + throw new NotSupportedException("Pool can only be started if not running"); + + State = PoolState.RUN; + } + + protected override PoolJob WaitForJob(PoolThread poolThread) + { + if (State == PoolState.RUN) + { + lock (waitingThreads) + { + PoolJob poolJob = Dequeue(poolThread); + if (poolJob != null) + return poolJob; + + waitingThreads.Add(poolThread); + Monitor.Wait(waitingThreads, Timeout); + waitingThreads.Remove(poolThread); + + poolJob = Dequeue(poolThread); + return poolJob; + } + } + return null; + } + + protected override void PulseWaitingThread() + { + lock (waitingThreads) + { + if (waitingThreads.Count > 0) + { + Monitor.Pulse(waitingThreads); + } + else if (CurrentPoolSize < PoolSize) + { + CreatePoolThread(); + } + } + } + + } +} diff --git a/LockingException.cs b/LockingException.cs new file mode 100644 index 0000000..ef77d64 --- /dev/null +++ b/LockingException.cs @@ -0,0 +1,10 @@ +using System; +namespace ln.threading +{ + public class LockingException : Exception + { + public LockingException() + { + } + } +} diff --git a/Pool.cs b/Pool.cs new file mode 100644 index 0000000..e0b8f1c --- /dev/null +++ b/Pool.cs @@ -0,0 +1,278 @@ +using System; +using System.Threading; +using System.Collections.Generic; +using System.Linq; +using ln.logging; + +namespace ln.threading +{ + public enum PoolThreadState { READY, WORKING, EXITED } + public delegate void PoolJobFinished(Pool pool, PoolJob job); + + public enum PoolState + { + INITIALIZE, // Pool Instanz erstellt + RUN, // Pool ist in Betrieb + SHUTDOWN, // Pool wird heruntergefahren (es warten noch laufende Jobs) + STOPPED // Pool wurde beendet, alle Resourcen freigegeben + } + + public partial class Pool : IDisposable + { + public static bool DEBUG = false; + + public PoolState State { get; protected set; } = PoolState.INITIALIZE; + + public event PoolJobFinished PoolJobFinished; + + public PoolThread[] PoolThreads => ThreadHelpers.GetLockedValue(poolThreads, () => poolThreads.ToArray()); + + public int PoolSize { get; } + public int CurrentPoolSize => ThreadHelpers.GetLockedValue(poolThreads, () => poolThreads.Count); + public PoolJob[] CurrentPoolJobs => ThreadHelpers.GetLockedValue(poolThreads, () => poolThreads.Select((t) => t.CurrentJob).Where((j) => j != null).ToArray()); + + public PoolJob[] QueuedJobs => ThreadHelpers.GetLockedValue(waitingThreads, () => queuedJobs.ToArray()); + public int NumQueuedJobs => ThreadHelpers.GetLockedValue(waitingThreads, () => queuedJobs.Count); + + private List poolThreads = new List(); + + protected HashSet workingThreads = new HashSet(); + protected HashSet waitingThreads = new HashSet(); + + private Queue queuedJobs = new Queue(); + + public Pool() : this(Environment.ProcessorCount){} + public Pool(int numThreads) + { + PoolSize = numThreads; + } + + public virtual void Start() + { + if ((State == PoolState.RUN) || (State == PoolState.SHUTDOWN)) + throw new NotSupportedException("Pool can only be started if not running"); + + State = PoolState.RUN; + + for (int n = 0; n < PoolSize; n++) + { + CreatePoolThread(); + } + } + + public virtual void Stop() => Stop(false); + public virtual void Stop(bool abort) + { + if (State != PoolState.RUN) + throw new NotSupportedException("Pool must be running to be able to stop"); + + State = PoolState.SHUTDOWN; + + lock (waitingThreads) + { + queuedJobs.Clear(); + } + + if (abort) + Abort(); + + while (CurrentPoolSize > ((PoolThread.CurrentPoolThread.Value?.Pool == this) ? 1 : 0)) + { + try + { + lock (waitingThreads) + Monitor.PulseAll(waitingThreads); + Thread.Sleep(100); + } + catch (ThreadInterruptedException) + { } + } + + State = PoolState.STOPPED; + } + + public void Enqueue(JobDelegate job) => Enqueue((poolJob) => job()); + public PoolJob Enqueue(ExtendedJobDelegate extendedJobDelegate) => Enqueue(extendedJobDelegate, "N/A"); + public PoolJob Enqueue(ExtendedJobDelegate extendedJobDelegate, string jobName) => EnqueuePoolJob(new PoolJob(jobName, extendedJobDelegate)); + public bool Enqueue(PoolJob poolJob) => EnqueuePoolJob(poolJob) != null; + + protected virtual PoolJob EnqueuePoolJob(PoolJob poolJob) + { + if (State != PoolState.RUN) + throw new NotSupportedException("Pool not running"); + + lock (waitingThreads) + { + if (queuedJobs.Contains(poolJob) || CurrentPoolJobs.Contains(poolJob)) + return null; + + poolJob.Prepare(); + queuedJobs.Enqueue(poolJob); + + PulseWaitingThread(); + } + + return poolJob; + } + + protected virtual void PulseWaitingThread() + { + lock (waitingThreads) + Monitor.Pulse(waitingThreads); + } + + public virtual int Enqueue(IEnumerable poolJobs) + { + if (State != PoolState.RUN) + throw new NotSupportedException("Pool not running"); + + int count = 0; + + lock (waitingThreads) + { + HashSet currentPoolJobs = new HashSet(CurrentPoolJobs); + foreach (PoolJob j in queuedJobs) + currentPoolJobs.Add(j); + + foreach (PoolJob poolJob in poolJobs) + { + if (!currentPoolJobs.Contains(poolJob)) + { + poolJob.Prepare(); + queuedJobs.Enqueue(poolJob); + count++; + } + } + Monitor.PulseAll(waitingThreads); + } + + return count; + } + + protected virtual PoolJob Dequeue() => Dequeue(PoolThread.CurrentPoolThread?.Value); + protected virtual PoolJob Dequeue(PoolThread poolThread) + { + if (poolThread == null) + throw new NullReferenceException(); + + if (State == PoolState.RUN) + { + lock (waitingThreads) + { + if (queuedJobs.Count > 0) + return queuedJobs.Dequeue(); + } + } + return null; + } + + protected virtual PoolJob WaitForJob(PoolThread poolThread) + { + while (State == PoolState.RUN) + { + lock (waitingThreads) + { + PoolJob poolJob = Dequeue(poolThread); + if (poolJob != null) + return poolJob; + + waitingThreads.Add(poolThread); + Monitor.Wait(waitingThreads); + waitingThreads.Remove(poolThread); + } + } + return null; + } + + + protected virtual void WorkerThreadLoop() + { + PoolThread poolThread = PoolThread.CurrentPoolThread.Value; + if (poolThread == null) + throw new NotSupportedException("Pool.WorkerThreadLoop(): Must be called from PoolThread"); + + lock (poolThreads) + poolThreads.Add(poolThread); + + try + { + while (State == PoolState.RUN) + { + PoolJob poolJob = WaitForJob(poolThread); + if (poolJob == null) + { + break; + } + else + { + lock (workingThreads) + workingThreads.Add(poolThread); + + try + { + poolJob.Run(this); + if (PoolJobFinished != null) + PoolJobFinished(this,poolJob); + } + finally + { + lock (workingThreads) + workingThreads.Remove(poolThread); + } + } + } + } + catch (PoolThreadMustExitException) + {} + catch (Exception e) + { + Logging.Log(e); + } + finally + { + lock (poolThreads) + poolThreads.Remove(poolThread); + } + } + + + + public void Abort() + { + lock (poolThreads) + { + foreach (PoolThread poolThread in poolThreads) + poolThread?.CurrentJob?.RequestAbort(); + } + } + + private bool AmIPoolThread() => PoolThread.CurrentPoolThread.Value != null; + + public PoolThread FindPoolThread(PoolJob job) + { + lock (poolThreads) + { + foreach (PoolThread poolThread in poolThreads) + { + if (poolThread.CurrentJob == job) + return poolThread; + } + } + return null; + } + + + + protected virtual PoolThread CreatePoolThread() + { + PoolThread poolThread = new PoolThread(WorkerThreadLoop); + return poolThread; + } + + public void Dispose() + { + if (State == PoolState.RUN) + Stop(); + } + } +} diff --git a/PoolJob.cs b/PoolJob.cs new file mode 100644 index 0000000..6d26079 --- /dev/null +++ b/PoolJob.cs @@ -0,0 +1,172 @@ +using System; +using ln.logging; +using System.Threading; +namespace ln.threading +{ + public delegate void JobDelegate(); + public delegate void ExtendedJobDelegate(PoolJob poolJob); + + public enum PoolJobState { PREPARED, RUNNING, DONE, FAILED }; + + public class PoolJob + { + public string Name { get; set; } + + public JobDelegate Job { get; } + public ExtendedJobDelegate ExtendedJob { get; } + + public double Progress { get; set; } + public string State { get; set; } + + public PoolJobState JobState { get; private set; } + + public bool JobAbortRequested { get; private set; } + + private Pool Pool { get; set; } + + protected PoolJob() + { + JobState = PoolJobState.PREPARED; + } + public PoolJob(JobDelegate job) + : this() + { + Job = job; + ExtendedJob = null; + Name = "N/A"; + } + public PoolJob(ExtendedJobDelegate job) + : this() + { + Job = null; + ExtendedJob = job; + Name = "N/A"; + } + public PoolJob(String name, ExtendedJobDelegate job) + : this() + { + Job = null; + ExtendedJob = job; + Name = name; + } + + public void RequestAbort() + { + JobAbortRequested = true; + if (Pool != null) + { + PoolThread poolThread = Pool.FindPoolThread(this); + if (poolThread != null) + { + if (poolThread.Thread.ThreadState == ThreadState.WaitSleepJoin) + poolThread.Thread.Interrupt(); + } + } + } + + public void ForceFail() + { + JobState = PoolJobState.FAILED; + } + public void setState(string state,params object[] p) + { + State = String.Format(state, p); + } + + + public virtual void RunJob() + { + if (Job != null) + { + Job(); + } + else if (ExtendedJob != null) + { + ExtendedJob(this); + } + else + { + Logging.Log(LogLevel.ERROR, "PoolJob without JobDelegate tried to run"); + JobState = PoolJobState.FAILED; + } + } + + public void Run(Pool pool) + { + Pool = pool; + try + { + JobState = PoolJobState.RUNNING; + + RunJob(); + + JobState = PoolJobState.DONE; + + if (Pool.DEBUG && (State != null) && !String.Empty.Equals(State)) + Logging.Log(LogLevel.DEBUG, "PoolJob exited normally with State={0}", State); + } + catch (Exception e) + { + JobState = PoolJobState.FAILED; + Logging.Log(LogLevel.ERROR, "PoolJob: {0} caught exception {1}", this, e); + Logging.Log(e); + } + Pool = null; + } + + public void SplitJob(PoolJob[] jobs) + { + if (Pool == null) + throw new ArgumentNullException(); + + foreach (PoolJob job in jobs) + { + if (!Pool.Enqueue(job)) + { + Logging.Log(LogLevel.ERROR, "PoolJob.SplitJob(): Failed to enqueue splitted Job"); + foreach (PoolJob _job in jobs) + { + _job.RequestAbort(); + } + throw new Exception("Failed to enqueue splitted Job"); + } + } + + while (true) + { + int nFinished = 0, nError = 0; + double progress = 0.0; + + foreach (PoolJob job in jobs) + { + progress += job.Progress; + + switch (job.JobState) + { + case PoolJobState.DONE: + nFinished++; + break; + case PoolJobState.FAILED: + nError++; + break; + } + + if (JobAbortRequested) + job.RequestAbort(); + } + + Progress = progress / jobs.Length; + + if ((nError + nFinished) == jobs.Length) + break; + + Thread.Sleep(1000); + } + } + + public virtual void Prepare() + { + } + + } +} diff --git a/PoolThread.cs b/PoolThread.cs new file mode 100644 index 0000000..ed46767 --- /dev/null +++ b/PoolThread.cs @@ -0,0 +1,79 @@ +using System; +using System.Threading; + +namespace ln.threading +{ + public class PoolThreadMustExitException : Exception + {} + + public class PoolThread + { + public static ThreadLocal CurrentPoolThread { get; } = new ThreadLocal(); + + public Pool Pool { get; private set; } + public Thread Thread { get; } + + public PoolJob CurrentJob { get; private set; } + + private Action ThreadLoop; + + public PoolThread(Action threadLoop) + { + ThreadLoop = threadLoop; + + Thread = new Thread(thread); + Thread.Start(); + } + + private void thread() + { + lock (this) + { + CurrentPoolThread.Value = this; + } + + ThreadLoop(); + + lock (this) + { + CurrentPoolThread.Value = null; + } + } + //while (Pool.State != PoolState.SHUTDOWN) + //{ + // PoolJob poolJob = null; + // try + // { + // poolJob = Pool.Dequeue(); + // } + // catch (TimeoutException) + // { + // break; + // } + + // if (poolJob != null) + // { + // State = PoolThreadState.WORKING; + // CurrentJob = poolJob; + // CurrentJob.Run(Pool); + + // if (Pool.PoolJobFinished != null) + // Pool.PoolJobFinished.Invoke(Pool, CurrentJob); + + // CurrentJob = null; + // State = PoolThreadState.READY; + // } + //} + + //CurrentPoolThread.Value = null; + + //State = PoolThreadState.EXITED; + + //lock (Pool.poolThreads) + //{ + // Pool.poolThreads.Remove(this); + // Monitor.Pulse(Pool.poolThreads); + //} + + } +} diff --git a/SchedulingPool.cs b/SchedulingPool.cs new file mode 100644 index 0000000..3a2dce0 --- /dev/null +++ b/SchedulingPool.cs @@ -0,0 +1,162 @@ +// /** +// * File: SchedulingPool.cs +// * Author: haraldwolff +// * +// * This file and it's content is copyrighted by the Author and / or copyright holder. +// * Any use wihtout proper permission is illegal and may lead to legal actions. +// * +// * +// **/ +using System; +using System.Collections.Generic; +using System.Threading; +using ln.collections; +using ln.type; + +namespace ln.threading +{ + + public class SchedulingPool + { + static SchedulingPool defaultPool = null; + static public SchedulingPool Default + { + get + { + if (defaultPool == null) + defaultPool = new SchedulingPool(); + + return defaultPool; + } + set => defaultPool = value; + } + + public Pool ThreadPool { get; private set; } + public int ThreadCount => ThreadPool.PoolSize; + + BTreeValueList schedule = new BTreeValueList(); + Thread schedulerThread; + + Dictionary scheduledJobs = new Dictionary(); + + public SchedulingPool() + :this(Environment.ProcessorCount) + { + } + public SchedulingPool(int nThreads) + { + ThreadPool = new Pool(nThreads); + ThreadPool.Start(); + + lock (this) + { + schedulerThread = new Thread(scheduler); + schedulerThread.Start(); + + Monitor.Wait(this); + } + } + + void scheduler() + { + lock (this) + { + Monitor.PulseAll(this); + + while (true) + { + double now = DateTime.Now.ToUnixTimeMilliseconds(); + if (schedule.Empty) + { + Monitor.Wait(this); + } + else if (schedule.First <= now) + { + while (!schedule.Empty && (schedule.First <= now)) + { + ScheduledJob scheduledJob = schedule.Shift(); + ThreadPool.Enqueue(scheduledJob); + scheduledJob.Reschedule(); + enqueue(scheduledJob); + } + } + else + { + Monitor.Wait(this, (int)(schedule.First - now)); + } + } + } + } + + void enqueue(ScheduledJob job) + { + lock (this) + { + if (schedule.TryAdd(job.NextScheduledExecution, job)) + Monitor.Pulse(this); + } + } + void dequeue(ScheduledJob job) + { + lock (this) + { + if (schedule.TryRemove(job.NextScheduledExecution, job)) + Monitor.Pulse(this); + } + } + + public void Schedule(Action action, double scheduledIntervall) + { + if (!scheduledJobs.TryGetValue(action, out ScheduledJob scheduledJob)) + { + scheduledJob = new ScheduledJob(action, scheduledIntervall); + scheduledJobs.Add(action, scheduledJob); + } + else + { + scheduledJob.ScheduledInterval = scheduledIntervall; + } + enqueue(scheduledJob); + } + public void Unschedule(Action action) + { + if (scheduledJobs.TryGetValue(action, out ScheduledJob scheduledJob)) + { + dequeue(scheduledJob); + } + } + + public class ScheduledJob : PoolJob + { + public SchedulingPool Pool { get; private set; } + public Action Action { get; } + + public double NextScheduledExecution { get; set; } + public double ScheduledInterval { get; set; } + + public ScheduledJob(Action action) + : this(action, 0, DateTime.Now.ToUnixTimeMilliseconds()) + { } + public ScheduledJob(Action action, double scheduledInterval) + : this(action, scheduledInterval, DateTime.Now.ToUnixTimeMilliseconds() + scheduledInterval) + { + } + public ScheduledJob(Action action, double scheduledInterval, double nextScheduledExecution) + { + Action = action; + ScheduledInterval = scheduledInterval; + NextScheduledExecution = nextScheduledExecution; + } + + public override void RunJob() => Action.Invoke(); + + public void Reschedule() => Reschedule(ScheduledInterval); + public void Reschedule(double interval) + { + double currentTime = DateTime.Now.ToUnixTimeMilliseconds(); + while (NextScheduledExecution < currentTime) + NextScheduledExecution += ScheduledInterval; + } + } + } +} diff --git a/TaskQueue.cs b/TaskQueue.cs new file mode 100644 index 0000000..ae5be72 --- /dev/null +++ b/TaskQueue.cs @@ -0,0 +1,44 @@ +// /** +// * File: TaskQueue.cs +// * Author: haraldwolff +// * +// * This file and it's content is copyrighted by the Author and / or copyright holder. +// * Any use wihtout proper permission is illegal and may lead to legal actions. +// * +// * +// **/ +using System; +using ln.collections; +using System.Linq; +namespace ln.threading +{ + public class TaskQueue + { + BTreeValueList queue = new BTreeValueList(); + + public TaskQueue() + { + } + + public bool Empty => queue.Keys.Count() == 0; + + public void Enqueue(Action action) => Enqueue(0, action); + public void Enqueue(int priority,Action action) + { + lock (queue) + { + queue.TryRemove(priority, action); + queue.Add(priority, action); + } + } + + public Action Dequeue() + { + lock (queue) + { + return queue.Shift(); + } + } + + } +} diff --git a/ThreadHelpers.cs b/ThreadHelpers.cs new file mode 100644 index 0000000..0c872a4 --- /dev/null +++ b/ThreadHelpers.cs @@ -0,0 +1,16 @@ +using System; +namespace ln.threading +{ + public static class ThreadHelpers + { + + public static T GetLockedValue(object lockObject,Func getter) + { + lock (lockObject) + { + return getter(); + } + } + + } +} diff --git a/Timing.cs b/Timing.cs new file mode 100644 index 0000000..3b0b47e --- /dev/null +++ b/Timing.cs @@ -0,0 +1,47 @@ +// /** +// * File: Timing.cs +// * Author: haraldwolff +// * +// * This file and it's content is copyrighted by the Author and / or copyright holder. +// * Any use wihtout proper permission is illegal and may lead to legal actions. +// * +// * +// **/ +using System; +using System.Diagnostics; +using ln.logging; +namespace ln.threading +{ + public static class Timing + { + public delegate void VoidDelegate(); + public delegate T TypedDelegate(); + + public static void Meassure(VoidDelegate f) => Meassure("", f); + public static void Meassure(String prefix,VoidDelegate f) + { + Stopwatch stopwatch = new Stopwatch(); + stopwatch.Start(); + f(); + stopwatch.Stop(); + Logging.Log(LogLevel.DEBUG, "Timing({1}): {0}ms", stopwatch.ElapsedMilliseconds,prefix); + } + public static T Meassure(TypedDelegate f) + { + return Meassure("", f); + } + public static T Meassure(String prefix,TypedDelegate f) + { + Stopwatch stopwatch = new Stopwatch(); + stopwatch.Start(); + T r = f(); + stopwatch.Stop(); + Logging.Log(LogLevel.DEBUG, "Timing({1}): {0}ms", stopwatch.ElapsedMilliseconds,prefix); + + return r; + } + + + + } +} diff --git a/ln.threading.csproj b/ln.threading.csproj new file mode 100644 index 0000000..95f7079 --- /dev/null +++ b/ln.threading.csproj @@ -0,0 +1,13 @@ + + + + netcoreapp2.1 + + + + + + + + +