Эффективность таймера - PullRequest
       12

Эффективность таймера

5 голосов
/ 15 октября 2010

Я планирую разработать систему с десятками тысяч объектов, каждый из которых будет иметь до 42 (но более вероятно, около 4 или 5) отдельных действий, которые они потенциально будут выполнять через регулярные промежутки времени. Я также планирую написать код, который будет деактивировать таймеры до тех пор, пока объект не начнет использоваться. В режиме ожидания объектам потребуется только 1 таймер каждый, но когда они активны, все другие таймеры будут запускаться одновременно. Сначала количество объектов будет небольшим, возможно, несколько сотен, но я ожидаю, что оно будет расти в геометрической прогрессии, и через несколько месяцев оно достигнет десятков тысяч.

Итак, я очень обеспокоен эффективностью кода, который буду писать для таймеров и для этих объектов. Есть три уровня, на которых я мог бы написать это приложение, чтобы все они успешно выполняли необходимые задачи. Кроме того, я планирую запустить эту систему на сервере Quad Core, поэтому я хотел бы использовать многопоточность везде, где это возможно.

С этой целью я решил использовать класс System.Timers.Timer, который запускает новый поток для каждого события elapse.

Это 3 уровня, которые я рассматриваю:

  1. Один таймер управляет всем приложением, он перебирает каждый объект, проверяет, нужно ли запускать какие-либо другие действия, и, если это так, запускает их, затем переходит к следующему.

  2. Многоуровневый таймер, в котором каждый объект имеет главный таймер, который проверяет все функции, которые может понадобиться объекту, запускает все готовые, а затем устанавливает следующий интервал таймера на следующее требуемое время действия.

  3. Таймер рекурсивного уровня, в котором каждое действие в каждом объекте имеет свой собственный таймер, который будет запущен, а затем настроен на запуск при следующем доступе.

Проблема с вариантом 1 заключается в том, что при таком количестве объектов и действий один единственный таймер истечения времени таким образом может работать, возможно, более 20 секунд (в то время как он выполнил несколько миллионов строк зацикленного кода), где это, вероятно, должно идти каждую 1 секунду Если объекты не синхронизированы, система, вероятно, не будет работать хорошо.

Проблема с вариантом 2 заключается в том, что писать будет немного сложнее, чем в варианте 3, но ненамного, это также будет означать, что, возможно, более 10000 таймеров, работающих в системе (по одному для каждого объекта), создают и уничтожают потоки с каждым истечением, как его никому не занимаются (что я не уверен, если это проблема или нет). В этом случае каждый таймер должен срабатывать как минимум раз в секунду, при этом может быть запущено несколько сотен строк кода (в крайнем случае - до тысячи).

Проблема с вариантом 3 заключается в большом количестве таймеров, которые потенциально могут быть введены в систему. Я говорю о среднем 10000+ таймеров с возможностью одновременного запуска около 100 000+ таймеров. Каждое событие истечения может иметь только 50 или менее строк кода, что делает их очень короткими. События истекания имели бы задержки между сотой секунды в одном экстремуме и пятью минутами в другом, при этом среднее значение, вероятно, составило бы около 1 секунды.

Я опытный в Visual Basic .NET, и планировал написать его в этом, но я мог бы также вернуться к моим школьным дням и попытаться написать это на C ++ для эффективности, если это будет иметь большое значение (пожалуйста, дайте мне знать, если у вас есть источники по эффективности кода между языками). Также играю с понятием запуска этого на кластерном сервере Linux вместо моего сервера Quad Core Windows, но я не уверен, смогу ли я заставить какие-либо из моих приложений .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 мог перестать жаловаться на это;)

~ Edit 3 ~

Добавлены переменные для отслеживания времени и обеспечения цикловслучается когда нужно

Ответы [ 4 ]

5 голосов
/ 16 октября 2010

РЕДАКТИРОВАТЬ: Я помню интересное интервью определенно стоит посмотреть: Арун Кишан: Внутри Windows 7 - Прощание с блокировкой диспетчера ядра Windows

Как сказал @Steven Sudit, я снова предупреждаю: используйте его только в качестве демонстрации того, как работает колесо таймера, и о некоторых задачах, которые вы должны учитывать при его реализации. Не в качестве эталонной реализации. В реальном мире вы должны написать гораздо более сложную логику, чтобы учесть доступные ресурсы, логику планирования и т. Д.


Вот хорошие моменты, изложенные Стивеном Судитом (подробности см. В комментариях к посту):

1) Выберите правильную структуру, чтобы сохранить список вакансий (как обычно, зависит):

  • SortedList <> (или SortedDictionary <>) хорош для потребления памяти и индексации, но должен реализовывать синхронизированный доступ

  • ConcurrentQueue <> поможет вам избежать блокировки, но вам придется реализовать упорядочение. Это также очень эффективно памяти

  • LinkedList <> хорош при вставке и извлечении (в любом случае нам нужен только заголовок), но требует синхронизированного доступа (через это легко реализовать через блокировку без блокировки) и не столь эффективно использует память, поскольку хранит две ссылки (prev / next ). Но это становится проблемой, когда у вас есть миллионы рабочих мест, поэтому все они занимают значительный объем памяти.

Но:

Я полностью согласен с @Steven:

Это не имеет значения: ни один из них не подходит. Правильный ответ будет состоять в том, чтобы использовать обычную очередь и поддерживать ее порядок самостоятельно, поскольку нам чаще всего необходимо получать к ней доступ только из головы или хвоста.

Как правило, я бы рекомендовал использовать наиболее полнофункциональную коллекцию из библиотеки, но здесь это неприменимо, поскольку это код системного уровня. Нам нужно было бы свернуть свою собственную, либо с нуля, либо поверх менее функциональной коллекции

2) Чтобы упростить логику обработки одновременных заданий, вы можете добавить список делегатов (например, через ConcurrentQueue, чтобы сделать его свободным от блокировки) в исходный класс заданий, поэтому, когда вам нужно другое задание в то же время, просто добавьте другого делегата для запуска.

@ Стивен:

Если две задачи фактически запланированы на одно и то же время (будь то на самом деле или эффективно), это нормальный случай, который не требует усложнения нашей структуры данных. Другими словами, нам не нужно группировать одновременные задания, чтобы нам пришлось пересекать две разные коллекции; мы можем просто сделать их смежными

3) Запуск / остановка диспетчера не так прямолинейны, как это может быть, и поэтому могут привести к ошибкам. Вместо этого вы можете ждать события, используя тайм-аут.

@ Стивен:

Таким образом, он будет либо просыпаться, когда следующее задание готово, либо когда новое задание вставляется перед головкой. В последнем случае может потребоваться запустить его сейчас или установить другое ожидание. Если представлено, скажем, 100 заданий, запланированных на одно и то же мгновение, лучшее, что мы можем сделать, - поставить их в очередь.

Если нам нужно было установить приоритеты, это задача для очереди диспетчеризации с приоритетами и нескольких пулов в отношениях производитель / потребитель, но она по-прежнему не оправдывает диспетчер запуска / остановки. Диспетчер всегда должен быть включен, работать в одном цикле, который иногда уступает ядру

4) Об использовании галочек:

@ Стивен:

Хорошо придерживаться одного типа тиков, но смешивание и сопоставление становятся уродливыми, особенно потому, что они зависят от аппаратного обеспечения. Я уверен, что тики будут немного быстрее, чем миллисекунды, потому что он хранит первое и должно делиться на константу, чтобы получить второе. Является ли эта операция дорогостоящей, это другой вопрос, но я хорошо использую галочки, чтобы избежать риска.

Мои мысли:

Еще один хороший момент, я согласен с вами.Но иногда деление на константу становится дорогостоящим, и это не так быстро, как может показаться.Но когда мы говорим о 100 000 DateTimes, это не имеет значения, вы правы, спасибо вам за указание.

5) «Управление ресурсами»:

@ Steven:

Проблема, которую я пытаюсь выделить, заключается в том, что вызов GetAvailableThreads дорог и наивен;ответ устарел, прежде чем вы сможете его использовать.Если бы мы действительно хотели отслеживать, мы могли бы получить начальные значения и сохранить счетчик выполнения, вызвав задание из оболочки, которая использует Interlocked.Increment / Decrement.Даже в этом случае предполагается, что остальная часть программы не использует пул потоков.Если нам действительно нужен точный контроль, то правильный ответ здесь - это накатить наш собственный пул потоков

. Я абсолютно согласен, что вызов GetAvailableThreads - наивный метод для мониторинга доступных ресурсов через CorGetAvailableThreads, не такой дорогой.Я хочу продемонстрировать, что есть необходимость управлять ресурсами и, похоже, выбрал плохой пример.

Любым способом, представленным в примере с исходным кодом, является , который не должен рассматриваться как правильный способ мониторинга доступных ресурсов.Я просто хочу продемонстрировать, что вы должны думать об этом.Через, может быть, не такой хороший код, как пример.

6) Использование Interlocked.CompareExchange:

@ Стивен:

Нет, это не обычный шаблон.Наиболее распространенная схема - это кратковременная блокировка.Менее распространенным является отметить переменную как volatile.Гораздо реже было бы использовать VolatileRead или MemoryBarrier.Использование Interlocked.CompareExchange таким образом неясно, даже если это делает Рихтер.использование его без пояснительного комментария абсолютно гарантирует путаницу, так как слово «Сравнить» подразумевает, что мы проводим сравнение, хотя на самом деле это не так.

Вы правы, я долженпункт о его использовании.


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();
    }

Надеюсь, это будет полезно.


@ Стивен Судит указывает мне на некоторые проблемы, поэтому здесь я попытаюсь дать свое видение.

1) Я бы не рекомендовал использовать SortedList здесь или где-либо еще, так какЭто устаревший класс .NET 1.1

SortedList <> ни в коем случае не является устаревшим.Он все еще существует в .NET 4.0 и появился в .NET 2.0 , когда дженерики были введены в язык.Я не вижу смысла удалять его из .NET.

Но реальный вопрос здесь, я пытаюсь ответить: Какая структура данных может хранить значения в отсортированном порядкеи будет эффективен при хранении и индексации .Есть две подходящие готовые структуры данных: SortedDictionary <> и SortedList <> . Здесь некоторая информация о том, как выбрать.Я просто не хочу тратить впустую реализацию со своим собственным кодом и скрывать основной алгоритм.Здесь я могу реализовать массив приоритетов или что-то другое, но для кода требуется больше строк.Я не вижу причин, чтобы не использовать SortedList <> здесь ...

Кстати, я не могу понять почему вы не рекомендуете это? Какие причины?

2) В общем, нет необходимости усложнять код специальными случаями для одновременных событий.

Когда @Jrud говорит, что у него, вероятно, будет много задач для планирования Iдумаю, у них может быть тяжелый параллелизм, поэтому я покажу, как его решить.Но моя точка зрения: даже если у вас низкий уровень параллелизма, у вас все равно есть шанс получить события одновременно.Также это легко возможно в многопоточной среде или когда есть много источников, которые хотят планировать задания.

Блокированные функции не так сложны, дешевы и, так как .NET 4.0 встроены, так что нет проблем с добавлением защиты в такой ситуации.

3) Метод IsWorking должен просто использовать барьер памяти и затем непосредственно читать значение.

Я не уверен, что вы правы.Я бы порекомендовал прочитать две хорошие статьи: Часть 4: Расширенные потоки Потоков в C # Джозефа Альбахари и Как блокировка блокировок? Джеффа Мозера.И, конечно, глава 28 (Примитивные конструкции синхронизации потоков) CLR через C # (3-е издание) Джеффри Рихтера.

Вот несколько цитат:

Метод MemoryBarrier не обращается к памяти, но он принудительно завершает любые более ранние загрузки и сохранения программного заказа перед вызовом MemoryBarrier.И это также вынуждает завершать любые последующие загрузки и сохранения программного заказа после вызова MemoryBarrier.MemoryBarrier гораздо менее полезен, чем два других метода

Важно Я знаю, что это может быть очень запутанным, поэтому позвольте мне обобщить это как простое правило: когда потоки взаимодействуют друг с другом через разделяемую память, напишите последнийвведите значение VolatileWrite и прочитайте первое значение, вызвав VolatileRead.

Я бы также порекомендовал: Руководства разработчика программного обеспечения Intel® 64 и IA-32 Architectures , если вы серьезно к этому относитесь.

Так что я не использую VolatileRead / VolatileWrite в своем коде и ключевое слово volatile, я не думаю, что Thread.MemoryBarrier будет лучше здесь.Может быть, вы можете указать мне, что я скучаю?Некоторые статьи или подробное обсуждение?

4) Похоже, что метод GetJobs может блокироваться на длительный период.Нужно ли это?

Прежде всего, это просто удобный метод, иногда необходимо поставить все задачи в очередь хотя бы для отладки.

Но вы не правы.Как я уже упоминал в комментариях к коду, 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 имеет очень надежную и быструю реализацию)
  • сборка чтения-only-обертка (без копии, только обертка итератора)

, поэтому мы просто сгладили список ссылок только для чтения на объекты Job.Это очень быстро, даже если у вас есть миллионы задач.Попробуйте измерить себя.

В любом случае я добавил его, чтобы показать, что происходит во время цикла выполнения (в целях отладки), но я думаю, что это может быть полезно.

5) Блокировкасвободная очередь доступна в .NET 4.0.

Я бы порекомендовал прочитать Шаблоны параллельного программирования Стивена Тауба и Потокобезопасные коллекции в .NET Framework 4 иИх характеристики производительности , также здесь много интересных статей.

Итак, я цитата :

ConcurrentQueue (T) являетсяструктура данных в .NET Framework 4, обеспечивающая поточно-ориентированный доступ к упорядоченным элементам FIFO (первым пришел - первым обслужен).Под капотом ConcurrentQueue (T) реализован с использованием списка небольших массивов и операций без блокировки над головным и хвостовым массивами, следовательно, он довольно сильно отличается от Queue (T), который поддерживается массивом и зависит от внешнего использования.мониторов для обеспечения синхронизации.ConcurrentQueue (T), безусловно, более безопасен и удобен, чем ручная блокировка очереди (T), но для определения относительной производительности двух схем требуются некоторые эксперименты.В оставшейся части этого раздела мы будем ссылаться на заблокированную вручную очередь (T) как на отдельный тип, называемый SynchronizedQueue (T).

У него нет методов для поддержки упорядоченной очереди. Ни одна из новой многопоточной коллекции, все они поддерживают неупорядоченную коллекцию. Но, читая оригинальное описание @Jrud, я думаю, что мы должны вести упорядоченный список времени, когда нужно выполнить задание. Я не прав?

6) Я бы не стал запускать и останавливать диспетчер; просто дайте ему спать до следующей работы

Знаете ли вы хороший способ сделать нить сна ThreadPool? Как вы это будете реализовывать?

Я думаю, что диспетчер уходит в "сон", когда он не обрабатывает какую-либо задачу и не ставит ее на время. Во всяком случае, нет никакой специальной обработки, чтобы усыпить или проснуться, поэтому в моих мыслях этот процесс равняется «сну».

Если вы сказали, что я должен просто перепланировать RunJobs через ThreadPool, когда нет доступных рабочих мест, если вы ошибаетесь, это израсходует слишком много ресурсов и может повлиять на запущенные задания. Попробуй себя. Зачем делать ненужную работу, когда мы можем легко ее избежать.

7) Вместо того, чтобы беспокоиться о разных видах тиков, вы можете просто придерживаться миллисекунд.

Вы не правы. Либо вы придерживаетесь клещей, либо вам все равно. Проверьте реализацию DateTime, каждый доступ к свойству в миллисекундах включает преобразование внутреннего представления (в тиках) в мс, включая деление. Это может снизить производительность на старых (класс Pentium) компиляторах (я измеряю это сам, и вы тоже).

В общем, я с тобой соглашусь. Мы не заботимся о представлении здесь, потому что это не дает нам заметного повышения производительности.

Это просто мой привычка. Я обрабатываю миллиарды DateTime в недавнем проекте, так закодированном в соответствии с ним. В моем проекте есть заметная разница между обработкой тиками и другими компонентами DateTime.

8) Попытка отследить доступные потоки, по-видимому, неэффективна

Я просто хочу продемонстрировать, что вы должны заботиться об этом. В реальном мире вы должны реализовать далеко от моей прямой логики планирования и мониторинга ресурсов.

Я хочу продемонстрировать алгоритм колеса таймера и указать на некоторую проблему, о которой автор должен подумать при его реализации.

Вы абсолютно правы, я должен предупредить об этом. Я думал, что "быстро ptototype" будет достаточно. Мое решение ни в коем случае нельзя использовать в производстве.

3 голосов
/ 15 октября 2010

Ничего из вышеперечисленного.Стандартное решение состоит в том, чтобы вести список событий, чтобы каждое из них указывало на следующее событие.Затем вы используете один таймер, и он включается только во время следующего события.

edit

Похоже, это называется колесо таймера .

edit

Как указал Sentinel, события должны отправляться в пул потоков.Обработчик этих событий должен выполнить небольшую работу как можно быстрее и без блокировки.Если ему нужно выполнить ввод / вывод, он должен запустить асинхронную задачу и завершиться.В противном случае пул потоков будет переполнен.

. Здесь может быть полезен класс .NET 4.0 Task, особенно для его методов продолжения.

0 голосов
/ 16 октября 2010

Это напоминает мне о старых системах продажи авиабилетов, где у вас были очереди.Запросы на получение билетов помещались в разные очереди в зависимости от того, какое внимание они нуждались.

Так что, возможно, вы могли бы иметь очередь объектов, требующих частого внимания, и очередь объектов, требующих нечастого внимания.При необходимости вы перемещаете их из одного в другое.

У вас может быть таймер для частой очереди и таймер для нечастой очереди.Для частой очереди вы можете разбить ее на несколько очередей, по одной для каждого потока.

Для сокращения частых очередей у ​​вас не должно быть большего количества потоков, чем у вас ядер.Если у вас есть два ядра, то вы хотите, чтобы они оба проворачивались.Больше потоков, чем это, не сделает вещи быстрее.На самом деле, если для обработки объектов требуется дисковый ввод-вывод или подключение к другому совместному оборудованию, это может даже не помочь запустить оба ядра.

0 голосов
/ 15 октября 2010

Компромисс между вашими тремя вариантами - между памятью и процессором.Чем больше таймеров, тем больше узлов таймера (памяти), а объединение этих таймеров в меньшее количество таймеров означает больше ЦП, поскольку вы проверяете события, которые требуют обслуживания во время выполнения.Излишняя загрузка ЦП при запуске слишком большого количества таймеров (и их истечении) - не слишком большая проблема с приличной реализацией таймера.

ТАК, на мой взгляд, если у вас есть хорошая реализация таймера, выберите запуск как можно большего количества таймеров.таймеры, как вам нужно (будьте как можно более детализированы).Но если любой из этих таймеров для каждого объекта является взаимоисключающим, рассмотрите возможность повторного использования узла таймера.

...