編集:私は間違いなく見る価値のある興味深いインタビューを覚えています:Arun Kishan:Windows7の内部-Windowsカーネルディスパッチャーロックへの別れ
@Steven Suditが述べたように、私は再び警告します。タイマーホイールがどのように機能するか、および実装中に注意しなければならないいくつかのタスクのデモとしてのみ使用してください。リファレンス実装としてではありません。現実の世界では、利用可能なリソース、スケジューリングロジックなどを考慮に入れるために、はるかに複雑なロジックを作成する必要があります。
ここにスティーブン・スディットが述べた良い点があります(詳細については投稿コメントを読んでください):
1)ジョブリストを保持するための適切な構造を選択します(通常どおりに異なります)。
SortedList <>(またはSortedDictionary <>)はメモリ消費とインデックス作成に適していますが、同期アクセスを実装する必要があります
ConcurrentQueue <>はロックを回避するのに役立ちますが、順序付けを実装する必要があります。また、メモリ効率も非常に高い
LinkedList <>は挿入と取得に適していますが(とにかくヘッドのみが必要です)、同期アクセスが必要であり(ロックフリーで簡単に実装できます)、2つの参照(前/次)を格納するためメモリ効率が低くなります。しかし、何百万ものジョブがある場合は問題になるため、すべてのジョブが大量のメモリを消費します。
だが:
私は@Stevenに完全に同意します:
それは問題ではありません。これらのどちらも適切ではありません。正しい答えは、通常のキューを使用してその順序を自分で維持することです。これは、ほとんどの場合、頭または尾からのみアクセスする必要があるためです。
一般に、ライブラリから最も機能が豊富なコレクションを使用することをお勧めしますが、これはシステムレベルのコードであるため、ここでは適用されません。ゼロから、または機能があまり豊富でないコレクションの上に、独自のロールを作成する必要があります
2)同時ジョブの処理ロジックを簡素化するために、デリゲートリストを元のJobクラスに追加して(たとえば、ConcurrentQueueを介してロックフリーにする)、同時に別のジョブが必要な場合は、別のデリゲートを追加して開始できます。
@スティーブン:
2つのタスクが実際に(実際にまたは効果的に)同時にスケジュールされている場合、これは通常のケースであり、データ構造を複雑にする必要はありません。つまり、2つの異なるコレクションをトラバースする必要があるため、同時ジョブをグループ化する必要はありません。それらを隣接させることができます
3)ディスパッチャの開始/停止は、可能な限りまっすぐではないため、エラーが発生する可能性があります。代わりに、タイムアウトを使用しながらイベントを待機できます。
@スティーブン:
このように、次のジョブの準備ができたとき、または新しいジョブがヘッドの前に挿入されたときにウェイクアップします。後者の場合、今すぐ実行するか、別の待機を設定する必要があります。たとえば、100個のジョブがすべて同じ瞬間にスケジュールされている場合、私たちができる最善のことは、それらすべてをキューに入れることです。
優先順位を付ける必要がある場合、それは優先ディスパッチキューとプロデューサー/コンシューマー関係の複数のプールの仕事ですが、それでもディスパッチャーの開始/停止を正当化するものではありません。ディスパッチャは常にオンで、コアを譲る単一のループで実行されている必要があります
4)ダニの使用について:
@スティーブン:
1つのタイプのティックに固執することは問題ありませんが、特にハードウェアに依存するため、ミキシングとマッチングは見苦しくなります。ティックはミリ秒よりもわずかに速いと確信しています。これは、前者を格納し、後者を取得するには定数で除算する必要があるためです。この操作にコストがかかるかどうかは別の問題ですが、リスクを回避するためにティックを使用しても問題ありません。
私の考え:
もう一つの良い点は、私はあなたに同意します。ただし、定数による除算はコストがかかる場合があり、思ったほど速くはありません。しかし、私たちが100 000のDateTimesについて話すとき、それは問題ではありません、あなたは正しいです、指摘してくれてありがとう。
5)「リソースの管理」:
@スティーブン:
私が強調しようとしている問題は、GetAvailableThreadsの呼び出しが高価で単純なことです。あなたがそれを使うことができる前に、答えは時代遅れです。本当に追跡したい場合は、Interlocked.Increment / Decrementを使用するラッパーからジョブを呼び出すことで、初期値を取得し、実行カウントを維持できます。それでも、プログラムの残りの部分がスレッドプールを使用していないことを前提としています。本当に細かい制御が必要な場合、ここでの正しい答えは、独自のスレッドプールをロールすることです。
GetAvailableThreadsの呼び出しは、CorGetAvailableThreadsを介して利用可能なリソースを監視するための単純な方法であり、それほど高価ではないことに完全に同意します。リソースを管理する必要があり、悪い例を選択しているように思われることを示したいと思います。
ソースコードの例で提供されている手段は、利用可能なリソースを監視する正しい方法として扱われてはなりません。私はあなたがそれについて考えなければならないことを示したいだけです。スルーは、例としてそれほど良いコードではないかもしれません。
6)Interlocked.CompareExchangeの使用:
@スティーブン:
いいえ、それは一般的なパターンではありません。最も一般的なパターンは、短時間ロックすることです。あまり一般的ではありませんが、変数に揮発性のフラグを立てます。あまり一般的ではないのは、VolatileReadまたはMemoryBarrierを使用することです。この方法でInterlocked.CompareExchangeを使用することは、Richterが使用したとしてもわかりにくいです。説明コメントなしで使用すると、混乱することが絶対に保証されます。「比較」という言葉は、実際には比較を行っていないのに、比較を行っていることを意味します。
あなたは正しいです私はその使用法について指摘しなければなりません。
using System;
using System.Threading;
// Job.cs
// WARNING! Your jobs (tasks) have to be ASYNCHRONOUS or at least really short-living
// else it will ruin whole design and ThreadPool usage due to potentially run out of available worker threads in heavy concurrency
// BTW, amount of worker threads != amount of jobs scheduled via ThreadPool
// job may waits for any IO (via async call to Begin/End) at some point
// and so free its worker thread to another waiting runner
// If you can't achieve this requirements then just use usual Thread class
// but you will lose all ThreadPool's advantages and will get noticeable overhead
// Read http://msdn.microsoft.com/en-us/magazine/cc164139.aspx for some details
// I named class "Job" instead of "Task" to avoid confusion with .NET 4 Task
public class Job
{
public DateTime FireTime { get; private set; }
public WaitCallback DoAction { get; private set; }
public object Param { get; private set; }
// Please use UTC datetimes to avoid different timezones problem
// Also consider to _never_ use DateTime.Now in repeat tasks because it significantly slower
// than DateTime.UtcNow (due to using TimeZone and converting time according to it)
// Here we always work with with UTC
// It will save you a lot of time when your project will get jobs (tasks) posted from different timezones
public static Job At(DateTime fireTime, WaitCallback doAction, object param = null)
{
return new Job {FireTime = fireTime.ToUniversalTime(), DoAction = doAction, Param = param};
}
public override string ToString()
{
return string.Format("{0}({1}) at {2}", DoAction != null ? DoAction.Method.Name : string.Empty, Param,
FireTime.ToLocalTime().ToString("o"));
}
}
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
// Dispatcher.cs
// Take a look at System.Runtime IOThreadTimer.cs and IOThreadScheduler.cs
// in Microsoft Reference Source, its interesting reading
public class Dispatcher
{
// You need sorted tasks by fire time. I use Ticks as a key to gain some speed improvements during checks
// There are maybe more than one task in same time
private readonly SortedList<long, List<Job>> _jobs;
// Synchronization object to access _jobs (and _timer) and make it thread-safe
// See comment in ScheduleJob about locking
private readonly object _syncRoot;
// Queue (RunJobs method) is running flag
private int _queueRun;
// Flag to prevent pollute ThreadPool with many times scheduled JobsRun
private int _jobsRunQueuedInThreadPool;
// I'll use Stopwatch to measure elapsed interval. It is wrapper around QueryPerformanceCounter
// It does not consume any additional resources from OS to count
// Used to check how many OS ticks (not DateTime.Ticks!) elapsed already
private readonly Stopwatch _curTime;
// Scheduler start time. It used to build time delta for job
private readonly long _startTime;
// System.Threading.Timer to schedule next active time
// You have to implement syncronized access as it not thread-safe
// http://msdn.microsoft.com/en-us/magazine/cc164015.aspx
private readonly Timer _timer;
// Minimum timer increment to schedule next call via timer instead ThreadPool
// Read http://www.microsoft.com/whdc/system/pnppwr/powermgmt/Timer-Resolution.mspx
// By default it around 15 ms
// If you want to know it exactly use GetSystemTimeAdjustment via Interop ( http://msdn.microsoft.com/en-us/library/ms724394(VS.85).aspx )
// You want TimeIncrement parameter from there
private const long MinIncrement = 15 * TimeSpan.TicksPerMillisecond;
// Maximum scheduled jobs allowed per queue run (specify your own suitable value!)
// Scheduler will add to ThreadPool queue (and hence count them as processed) no more than this constant
// This is balance between how quick job will be scheduled after it time elapsed in one side, and
// how long JobsList will be blocked and RunJobs owns its thread from ThreadPool
private const int MaxJobsToSchedulePerCheck = 10;
// Queue length
public int Length
{
get
{
lock (_syncRoot)
{
return _jobs.Count;
}
}
}
public Dispatcher()
{
_syncRoot = new object();
_timer = new Timer(RunJobs);
_startTime = DateTime.UtcNow.Ticks;
_curTime = Stopwatch.StartNew();
_jobs = new SortedList<long, List<Job>>();
}
// Is dispatcher still working
// Warning! Queue ends its work when no more jobs to schedule but started jobs can be still working
public bool IsWorking()
{
return Interlocked.CompareExchange(ref _queueRun, 0, 0) == 1;
}
// Just handy method to get current jobs list
public IEnumerable<Job> GetJobs()
{
lock (_syncRoot)
{
// We copy original values and return as read-only collection (thread-safety reasons)
return _jobs.Values.SelectMany(list => list).ToList().AsReadOnly();
}
}
// Add job to scheduler queue (schedule it)
public void ScheduleJob(Job job)
{
// WARNING! This will introduce bottleneck if you have heavy concurrency.
// You have to implement lock-free solution to avoid botleneck but this is another complex topic.
// Also you can avoid lock by using Jeffrey Richter's ReaderWriterGateLock (http://msdn.microsoft.com/en-us/magazine/cc163532.aspx)
// But it can introduce significant delay under heavy load (due to nature of ThreadPool)
// I recommend to implement or reuse suitable lock-free algorithm.
// It will be best solution in heavy concurrency (if you have to schedule large enough job count per second)
// otherwise lock or maybe ReaderWriterLockSlim is cheap enough
lock (_syncRoot)
{
// We'll shift start time to quick check when it pasts our _curTime
var shiftedTime = job.FireTime.Ticks - _startTime;
List<Job> jobs;
if (!_jobs.TryGetValue(shiftedTime, out jobs))
{
jobs = new List<Job> {job};
_jobs.Add(shiftedTime, jobs);
}
else jobs.Add(job);
if (Interlocked.CompareExchange(ref _queueRun, 1, 0) == 0)
{
// Queue not run, schedule start
Interlocked.CompareExchange(ref _jobsRunQueuedInThreadPool, 1, 0);
ThreadPool.QueueUserWorkItem(RunJobs);
}
else
{
// else queue already up and running but maybe we need to ajust start time
// See detailed comment in RunJobs
long firetime = _jobs.Keys[0];
long delta = firetime - _curTime.Elapsed.Ticks;
if (delta < MinIncrement)
{
if (Interlocked.CompareExchange(ref _jobsRunQueuedInThreadPool, 1, 0) == 0)
{
_timer.Change(Timeout.Infinite, Timeout.Infinite);
ThreadPool.QueueUserWorkItem(RunJobs);
}
}
else
{
Console.WriteLine("DEBUG: Wake up time changed. Next event in {0}", TimeSpan.FromTicks(delta));
_timer.Change(delta/TimeSpan.TicksPerMillisecond, Timeout.Infinite);
}
}
}
}
// Job runner
private void RunJobs(object state)
{
// Warning! Here I block list until entire process done,
// maybe better will use ReadWriterLockSlim or somewhat (e.g. lock-free)
// as usually "it depends..."
// Here processing is really fast (a few operation only) so until you have to schedule many jobs per seconds it does not matter
lock (_syncRoot)
{
// We ready to rerun RunJobs if needed
Interlocked.CompareExchange(ref _jobsRunQueuedInThreadPool, 0, 1);
int availWorkerThreads;
int availCompletionPortThreads;
// Current thread stats
ThreadPool.GetAvailableThreads(out availWorkerThreads, out availCompletionPortThreads);
// You can check max thread limits by
// ThreadPool.GetMaxThreads(out maxWorkerThreads, out maxCompletionPortThreads);
int jobsAdded = 0;
while (jobsAdded < MaxJobsToSchedulePerCheck && availWorkerThreads > MaxJobsToSchedulePerCheck + 1 && _jobs.Count > 0)
{
// SortedList<> implemented as two arrays for keys and values so indexing on key/value will be fast
// First element
List<Job> curJobs = _jobs.Values[0];
long firetime = _jobs.Keys[0];
// WARNING! Stopwatch ticks are different from DateTime.Ticks
// so we use _curTime.Elapsed.Ticks instead of _curTime.ElapsedTicks
// Each tick in the DateTime.Ticks value represents one 100-nanosecond interval.
// Each tick in the ElapsedTicks value represents the time interval equal to 1 second divided by the Frequency.
if (_curTime.Elapsed.Ticks <= firetime) break;
while (curJobs.Count > 0 && jobsAdded < MaxJobsToSchedulePerCheck && availWorkerThreads > MaxJobsToSchedulePerCheck + 1)
{
var job = curJobs[0];
// Time elapsed and we ready to start job
if (job.DoAction != null)
{
// Schedule new run
// I strongly recommend to look at new .NET 4 Task class because it give superior solution for managing Tasks
// e.g. cancel run, exception handling, continuation, etc
ThreadPool.QueueUserWorkItem(job.DoAction, job);
++jobsAdded;
// It may seems that we can just decrease availWorkerThreads by 1
// but don't forget about started jobs they can also consume ThreadPool's threads
ThreadPool.GetAvailableThreads(out availWorkerThreads, out availCompletionPortThreads);
}
// Remove job from list of simultaneous jobs
curJobs.Remove(job);
}
// Remove whole list if its empty
if (curJobs.Count < 1) _jobs.RemoveAt(0);
}
if (_jobs.Count > 0)
{
long firetime = _jobs.Keys[0];
// Time to next event
long delta = firetime - _curTime.Elapsed.Ticks;
if (delta < MinIncrement)
{
// Schedule next queue check via ThreadPool (immediately)
// It may seems we start to consume all resouces when we run out of available threads (due to "infinite" reschdule)
// because we pass thru our while loop and just reschedule RunJobs
// but this is not right because before RunJobs will be started again
// all other thread will advance a bit and maybe even complete its task
// so it safe just reschedule RunJobs and hence wait when we get some resources
if (Interlocked.CompareExchange(ref _jobsRunQueuedInThreadPool, 1, 0) == 0)
{
_timer.Change(Timeout.Infinite, Timeout.Infinite);
ThreadPool.QueueUserWorkItem(RunJobs);
}
}
else // Schedule next check via timer callback
{
Console.WriteLine("DEBUG: Next event in {0}", TimeSpan.FromTicks(delta)); // just some debug output
_timer.Change(delta / TimeSpan.TicksPerMillisecond, Timeout.Infinite);
}
}
else // Shutdown the queue, no more jobs
{
Console.WriteLine("DEBUG: Queue ends");
Interlocked.CompareExchange(ref _queueRun, 0, 1);
}
}
}
}
使用法の簡単な例:
// Test job worker
static void SomeJob(object param)
{
var job = param as Job;
if (job == null) return;
Console.WriteLine("Job started: {0}, [scheduled to: {1}, param: {2}]", DateTime.Now.ToString("o"),
job.FireTime.ToLocalTime().ToString("o"), job.Param);
}
static void Main(string[] args)
{
var curTime = DateTime.UtcNow;
Console.WriteLine("Current time: {0}", curTime.ToLocalTime().ToString("o"));
Console.WriteLine();
var dispatcher = new Dispatcher();
// Schedule +10 seconds to future
dispatcher.ScheduleJob(Job.At(curTime + TimeSpan.FromSeconds(10), SomeJob, "+10 sec:1"));
dispatcher.ScheduleJob(Job.At(curTime + TimeSpan.FromSeconds(10), SomeJob, "+10 sec:2"));
// Starts almost immediately
dispatcher.ScheduleJob(Job.At(curTime - TimeSpan.FromMinutes(1), SomeJob, "past"));
// And last job to test
dispatcher.ScheduleJob(Job.At(curTime + TimeSpan.FromSeconds(25), SomeJob, "+25 sec"));
Console.WriteLine("Queue length: {0}, {1}", dispatcher.Length, dispatcher.IsWorking()? "working": "done");
Console.WriteLine();
foreach (var job in dispatcher.GetJobs()) Console.WriteLine(job);
Console.WriteLine();
Console.ReadLine();
Console.WriteLine(dispatcher.IsWorking()?"Dispatcher still working": "No more jobs in queue");
Console.WriteLine();
foreach (var job in dispatcher.GetJobs()) Console.WriteLine(job);
Console.ReadLine();
}
お役に立てば幸いです。
@Steven Suditが私にいくつかの問題を指摘しているので、ここで私は自分のビジョンを示しようとしています。
1)廃止された.NET 1.1クラスであるため、ここやその他の場所でSortedListを使用することはお勧めしません。
SortedList<>は決して時代遅れではありません。.NET 4.0にはまだ存在し、ジェネリックが言語に導入されたときに.NET2.0で導入されました。.NETから削除するポイントがわかりません。
しかし、ここで私が答えようとしている本当の質問は、どのデータ構造がソートされた順序で値を格納でき、格納とインデックス作成に効率的であるかということです。すぐに使用できる2つの適切なデータ構造があります。SortedDictionary< >とSortedList<>です。ここに選択方法に関するいくつかの情報があります。自分のコードで実装を無駄にしたり、メインアルゴリズムを隠したりしたくないだけです。ここではpriority-arrayなどを実装できますが、コーディングにはさらに多くの行が必要です。ここでSortedList<>を使用しない理由はわかりません...
ところで、なぜあなたがそれをお勧めしないのか理解できませんか?理由は何ですか?
2)一般に、同時イベントの特殊なケースでコードを複雑にする必要はありません。
@Jrudがスケジュールするタスクはおそらくたくさんあると言ったとき、それらは同時実行性が高いと思うので、それを解決する方法を示します。しかし、私のポイント:同時実行性が低くても、同時にイベントを取得する可能性があります。また、これはマルチスレッド環境で、またはジョブをスケジュールしたいソースが多数ある場合に簡単に実行できます。
インターロックされた機能はそれほど複雑ではなく、安価であり、.NET 4.0がインライン化されているため、このような状況でガードを追加するのに問題はありません。
3)IsWorkingメソッドは、メモリバリアを使用してから、値を直接読み取る必要があります。
ここであなたが正しいかどうかはわかりません。2つの素晴らしい記事を読むことをお勧めします:パート4: Joseph AlbahariによるC#でのスレッドの高度なスレッドとロックのロック方法 ジェフ・モーザー著。そして、原因は、Jeffrey RichterによるC#(第3版)を介したCLRの第28章(プリミティブスレッド同期コンストラクト)です。
ここにいくつかのqoute:
MemoryBarrierメソッドはメモリにアクセスしませんが、MemoryBarrierを呼び出す前に、以前のプログラムオーダーのロードとストアを強制的に完了させます。また、MemoryBarrierの呼び出し後に、後のプログラムオーダーのロードとストアを強制的に完了させます。MemoryBarrierは、他の2つの方法よりもはるかに有用性が低くなります
重要これは非常に混乱する可能性があることを知っているので、簡単なルールとして要約します。スレッドが共有メモリを介して相互に通信している場合、VolatileWriteを呼び出して最後の値を書き込み、VolatileReadを呼び出して最初の値を読み取ります。
また、真剣に考えている場合は、インテル®64およびIA-32アーキテクチャーソフトウェア開発者マニュアルをお勧めします。
したがって、コードでVolatileRead / VolatileWriteを使用せず、volatileキーワードも使用しません。ここでは、Thread.MemoryBarrierの方が優れているとは思いません。多分あなたは私が恋しいものを私に指摘することができますか?いくつかの記事または詳細な議論?
4)GetJobsメソッドは、長期間ロックされる可能性があるようです。必要ですか?
まず第一に、その便利な方法ですが、少なくともデバッグのために、すべてのタスクをキューに入れる必要がある場合があります。
しかし、あなたは正しくありません。コードコメントで述べたように、2つの配列として実装されたSortedList <>は、参照ソースまたはReflectorで表示するだけで確認できます。ここに参照ソースからのいくつかのコメントがあります:
// A sorted list internally maintains two arrays that store the keys and
// values of the entries.
.NET 4.0から入手しましたが、2-3.5以降あまり変更されていません。
だから私のコード:
_jobs.Values.SelectMany(list => list).ToList().AsReadOnly();
以下を含みます:
- Listへの参照の配列で値を繰り返し処理します。インデックス配列は非常に高速です。
- 各リストを反復処理します(これも配列として内部的に実装されます)。それも非常に速いです。
- (ToList()を介して)新しい参照リストを作成します。これも非常に高速です(動的配列のみ)(.NETは非常に堅固で高速な実装です)
- 読み取り専用ラッパーをビルドします(コピーなし、イテレーターラッパーのみ)
その結果、Jobのオブジェクトへの参照の読み取り専用リストをフラット化しただけです。何百万ものタスクがある場合でも、非常に高速です。自分で測定してみてください。
いずれにせよ、(デバッグ目的で)実行サイクル中に何が起こるかを示すために追加しましたが、役立つと思います。
5).NET4.0ではロックフリーキューを使用できます。
StephenToubによる並列プログラミングのパターンと.NETFramework4のスレッドセーフコレクションとそのパフォーマンス特性、およびここにも多くの興味深い記事を読むことをお勧めします。
だから私は引用します:
ConcurrentQueue(T)は、.NET Framework 4のデータ構造であり、FIFO(先入れ先出し)順序付けされた要素へのスレッドセーフなアクセスを提供します。内部的には、ConcurrentQueue(T)は、小さな配列のリストと、ヘッド配列とテール配列のロックフリー操作を使用して実装されます。したがって、配列に支えられ、外部使用に依存するQueue(T)とはまったく異なります。同期を提供するモニターの数。ConcurrentQueue(T)は、Queue(T)を手動でロックするよりも確かに安全で便利ですが、2つのスキームの相対的なパフォーマンスを判断するには、ある程度の実験が必要です。このセクションの残りの部分では、手動でロックされたQueue(T)を、SynchronizedQueue(T)と呼ばれる自己完結型のタイプと呼びます。
順序付けられたキューを維持するためのメソッドはありません。新しいスレッドセーフコレクションのいずれも、それらはすべて順序付けられていないコレクションを維持します。しかし、元の@Jrudの説明を読むと、タスクを実行する必要がある時間の順序付きリストを維持する必要があると思います。私が間違っている?
6)ディスパッチャの起動と停止を気にしません。次の仕事まで寝かせて
ThreadPoolのスレッドをスリープ状態にする良い方法を知っていますか?どのように実装しますか?
ディスパッチャは、タスクを処理せず、ジョブのウェイクアップをスケジュールすると、「スリープ」状態になると思います。とにかく、それをスリープまたはウェイクアップするための特別な処理はないので、私の考えでは、このプロセスは「スリープ」に相当します。
間違っているときに利用できるジョブがないときに、ThreadPoolを介してRunJobsを再スケジュールする必要があると言った場合、リソースを大量に消費し、開始されたジョブに影響を与える可能性があります。自分で試してみてください。簡単に回避できるのに、なぜ不要な仕事をするのか。
7)さまざまな種類のティックについて心配するのではなく、ミリ秒に固執することができます。
あなたは正しくありません。あなたはダニに固執するか、それを完全に気にしないかのどちらかです。DateTimeの実装を確認してください。ミリ秒プロパティへの各アクセスには、内部表現(ティック単位)を除算を含むミリ秒に変換することが含まれます。これは、古い(Pentiumクラス)コンパルターのパフォーマンスを損なう可能性があります(私はそれを自分で測定し、あなたもそうすることができます)。
一般的に私はあなたに同意します。ここでは、パフォーマンスが大幅に向上しないため、表現については気にしません。
それは私の習慣です。私は最近のプロジェクトで何十億ものDateTimeを処理しているので、それに応じてコーディングしています。私のプロジェクトでは、ティックによる処理とDateTimeの他のコンポーネントによる処理の間に顕著な違いがあります。
8)利用可能なスレッドを追跡する試みは効果的ではないようです
私はあなたがそれを気にしなければならないことを示したいだけです。現実の世界では、リソースのスケジューリングと監視という私のまっすぐなロジックからはほど遠い実装を行う必要があります。
タイマーホイールアルゴリズムをデモンストレーションし、それを実装するときに作成者が考えなければならないいくつかの問題を指摘したいと思います。
あなたは絶対に正しいです私はそれについて警告しなければなりません。「早くプトトタイプ」で十分だと思いました。私のソリューションは、いかなる意味でも本番環境では使用できません。