6

私は、数万のオブジェクトを含むシステムを開発することを計画しています。これらのオブジェクトには、それぞれ最大42(ただし、おそらく4または5程度)の個別のアクションがあり、定期的に実行される可能性があります。また、オブジェクトが使用されるまでタイマーを非アクティブ化するコードを作成する予定です。アイドル状態の場合、オブジェクトはそれぞれ1つのタイマーのみを必要としますが、アクティブな場合、他のタイマーはすべて一度に開始されます。最初はオブジェクトの数は少なく、おそらく数百になりますが、指数関数的に増加し、数か月以内に数万に達し始めると思います。

そのため、タイマーとこれらのオブジェクト用に作成するコードの効率について非常に心配しています。このアプリケーションを作成できるレベルは3つあり、すべてが必要なタスクを正常に実行します。また、このシステムをクアッドコアサーバーで実行する予定なので、可能な限りマルチスレッドを利用したいと思います。

この目的のために、経過イベントごとに新しいスレッドを起動するSystem.Timers.Timerクラスを使用することにしました。

これらは私が検討している3つのレベルです:

  1. 1つのタイマーがアプリケーション全体を操作し、各オブジェクトを反復処理し、他のアクションを実行する必要があるかどうかを確認し、実行する必要がある場合は、それらを実行してから次のアクションに進みます。

  2. 各オブジェクトにマスタータイマーがあり、オブジェクトが実行する必要のあるすべての機能をチェックし、準備ができている機能を実行して、次のタイマー間隔を次に必要なアクション時間に設定する多層タイマー。

  3. 各オブジェクトの各アクションに、トリガーされ、次に使用可能になるときに実行されるように設定された独自のタイマーがある再帰層タイマー。

オプション1の問題は、オブジェクトとアクションが非常に多い場合、この方法で1つの単一タイマーが経過すると(数百万行のループコードを実行している間)20秒以上実行される可能性があることです。 。オブジェクトの同期が保たれていないと、システムがうまく機能しない可能性があります。

オプション2の問題は、オプション3よりも書き込みが少し難しいことですが、それほどではありません。システム上で実行されているタイマー(オブジェクトごとに1つ)が10,000以上あり、それぞれでスレッドを作成および破棄することも意味します。誰のビジネスのようにも経過します(これが問題であるかどうかはわかりません)。この状況では、各タイマーは少なくとも1秒に1回起動する必要があり、おそらく数百行のコードが実行されます(極端な場合は最大で1,000行)。

オプション3の問題は、システムに導入される可能性のあるタイマーの量が非常に多いことです。私は平均10,000以上のタイマーについて話していて、100,000以上のタイマーが同時に実行される可能性があります。ただし、各経過イベントは50行以下のコードを実行するだけでよく、非常に短くなります。経過イベントには、一方の極端な場合は100分の1秒、もう一方の極端な場合は5分の遅延があり、平均は約1秒である可能性があります。

私はVisualBasic.NETに精通しており、その中でそれを書くことを計画していましたが、それが大きな違いを生むのであれば、高校時代に戻って効率を上げるためにこれをC ++で書くこともできます(言語間のコード効率に関する情報源があるかどうかを知っています)。また、クアッドコアWindowsサーバーではなくクラスター化されたLinuxサーバーでこれを実行するという概念をいじっていますが、.NETアプリをそのようなLinuxクラスターで実行できるかどうかはわかりません(情報があればいいのですが)その上でも)。

このトピックに答える主な質問は次のとおりです。

オプション1、2、または3を使用しますか?その理由は何ですか?

〜コメントを考慮して編集〜

したがって、スピンロック付きのタイマーホイールを含む4番目のオプション。これがジョブクラスです:

Public Class Job
Private dFireTime As DateTime
Private objF As CrossAppDomainDelegate
Private objParams() As Object

Public Sub New(ByVal Func As CrossAppDomainDelegate, ByVal Params() As Object, ByVal FireTime As DateTime)
    objF = Func
    dFireTime = FireTime
    objParams = Params
End Sub

Public ReadOnly Property FireTime()
    Get
        Return dFireTime
    End Get
End Property

Public ReadOnly Property Func() As CrossAppDomainDelegate
    Get
        Return objF
    End Get
End Property

Public ReadOnly Property Params() As Object()
    Get
        Return objParams
    End Get
End Property
End Class

そして、メインループの実装:

Private Tasks As LinkedList(Of Job)

Private Sub RunTasks()
    While True
        Dim CurrentTime as DateTime = Datetime.Now            

        If Not Tasks.Count = 0 AndAlso Tasks(0).FireTime > CurrentTime Then
            Dim T As Job = Tasks(0)
            Tasks.RemoveFirst()
            T.Func.Invoke()
        Else
            Dim MillisecondDif As Double

            MillisecondDif = Tasks(0).FireTime.Subtract(CurrentTime).Milliseconds
            If MillisecondDif > 30 Then
                Threading.Thread.Sleep(MillisecondDif)
            End If
        End If

    End While
End Sub

私はそれを正しく持っていますか?

EpicClanWars.com

〜編集2〜

「タスク」という単語を「ジョブ」に切り替えて、pplがそれについて文句を言うのをやめることができるようにしました;)

〜編集3〜

時間を追跡し、必要なときにスピンループが発生するようにするための変数を追加しました

4

4 に答える 4

5

編集:私は間違いなく見る価値のある興味深いインタビューを覚えています: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)利用可能なスレッドを追跡する試みは効果的ではないようです

私はあなたがそれを気にしなければならないことを示したいだけです。現実の世界では、リソースのスケジューリングと監視という私のまっすぐなロジックからはほど遠い実装を行う必要があります。

タイマーホイールアルゴリズムをデモンストレーションし、それを実装するときに作成者が考えなければならないいくつかの問題を指摘したいと思います。

あなたは絶対に正しいです私はそれについて警告しなければなりません。「早くプトトタイプ」で十分だと思いました。私のソリューションは、いかなる意味でも本番環境では使用できません。

于 2010-10-16T15:05:42.070 に答える
3

上記のどれでもない。標準的な解決策は、イベントのリストを保持して、各イベントが次に発生するイベントを指すようにすることです。次に、単一のタイマーを使用して、次のイベントに間に合うようにのみタイマーを起動させます。

編集

これはタイマーホイールと呼ばれているように見えます。

編集

Sentinelが指摘したように、イベントはスレッドプールにディスパッチする必要があります。これらのイベントのハンドラーは、ブロックすることなく、可能な限り迅速に小さな作業を実行する必要があります。I / Oを実行する必要がある場合は、非同期タスクを起動して終了する必要があります。そうしないと、スレッドプールがオーバーフローします。

ここでは、 .NET 4.0Taskクラスが、特にその継続メソッドに役立つ場合があります。

于 2010-10-15T18:44:20.537 に答える
0

3つのオプションのトレードオフは、メモリとCPUの間です。より多くのタイマーはより多くのタイマーノード(メモリ)を意味し、実行時にサービスが必要なイベントをチェックするときに、これらのタイマーをより少ないタイマーに集約することはより多くのCPUを意味します。あまりにも多くのタイマーを開始する(そしてそれらを期限切れにする)ことによるCPUオーバーヘッドは、適切なタイマーの実装ではそれほど大きな問題ではありません。

したがって、私の意見では、適切なタイマーの実装がある場合は、必要な数のタイマーを開始することを選択します(できるだけ細かくしてください)。ただし、オブジェクトごとにこれらのタイマーのいずれかが相互に排他的である場合は、タイマーノードの再利用を検討してください。

于 2010-10-15T19:16:46.437 に答える
0

これは、あなたが列を作っていた古い航空券システムを思い出させます。発券リクエストは、必要な注意の種類に応じて異なるキューに入れられました。

したがって、頻繁に注意を払う必要のあるオブジェクトのキューと、あまり注意を払う必要のないオブジェクトのキューが存在する可能性があります。必要に応じて、それらを一方から他方に移動します。

頻繁なキュー用のタイマーと、まれなキュー用のタイマーを設定できます。頻繁なキューの場合、スレッドごとに1つずつ、複数のキューに分割できます。

頻繁なキューを処理するために、コアよりも多くのスレッドを使用しないでください。2つのコアがある場合、実行したいのは、両方をクランキングさせることです。それより多くのスレッドは物事を速くしません。実際、オブジェクトの処理にディスクI / Oが必要な場合、または他の共有ハードウェアに対応する必要がある場合は、両方のコアを実行するのに役立たない場合もあります。

于 2010-10-15T23:20:54.623 に答える