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

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

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

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

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

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

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

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

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

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

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

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

Заменил слово «Задание» на «Работа», чтобы люди могли перестать жаловаться на это;)

~Редактировать 3~

Добавлены переменные для отслеживания времени и обеспечения того, чтобы спин-петли происходили, когда это необходимо.


person Jrud    schedule 15.10.2010    source источник
comment
Я чувствую себя обязанным спросить, что эта штука собирается делать?   -  person Christopher Edwards    schedule 15.10.2010
comment
Использование проекта мне так же интересно, как и проблема, которую он представляет, я хотел бы сначала решить проблему, а затем я расскажу вам об использовании. Для некоторых это может быть менее важно, если я прямо укажу на использование, или могут быть предложены другие ответы для использования существующих проектов других людей, где на самом деле другие проекты не подходят. Чтобы предотвратить это, я не хочу указывать, что делает использование этого кода, пока не выясню, как это сделать.   -  person Jrud    schedule 15.10.2010
comment
Не уверен, что улавливаю суть без подробностей, но это похоже на Quartz.net quartznet.sourceforge. сеть/faq.html   -  person kenny    schedule 15.10.2010
comment
@Kenny: Это интересно, но это больше похоже на порт cron, то есть он предназначен для небольшого количества событий, которые обычно проходят с интервалом в несколько минут.   -  person Steven Sudit    schedule 16.10.2010
comment
Пожалуйста, не называйте это Задачей. В .NET 4.0 уже есть класс Task, так что это вызовет только путаницу.   -  person Steven Sudit    schedule 16.10.2010
comment
Это правильно в общих чертах, но немного неполно в частностях. Во-первых, когда он вызывает обработчик, он должен делать это через пул потоков. Текущий код потенциально может пересекать AppDomains, но по-прежнему будет находиться в том же потоке. Во-вторых, вам нужно проверить, будет ли следующая продолжительность сна менее 15 мс (или, чтобы быть в безопасности, 30 мс), и если это так, то вращение. В-третьих, я бы рекомендовал зафиксировать значение DateTime.Now в верхней части цикла while в локальной переменной, чтобы вы могли однозначно его использовать. В-четвертых, вам может понадобиться поддержка повторяющихся событий.   -  person Steven Sudit    schedule 16.10.2010
comment
System.Timers.Timer, который запускает новый поток, это не так, он использует пул потоков.   -  person Richard    schedule 16.10.2010
comment
Все еще не совсем там. 1) Свойство называется Millsecond, а не Milliseconds, и это int, а не double. Пожалуйста, не используйте double для хранения int. 2) Вызов делегата происходит в том же потоке. Вместо этого вам нужно поставить делегата в очередь в пуле потоков.   -  person Steven Sudit    schedule 17.10.2010


Ответы (4)


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

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


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

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

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

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

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

Но:

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

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

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

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

@Стивен:

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

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

@Стивен:

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

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

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

@Стивен:

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

Мои мысли:

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

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

@Стивен:

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

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

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

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

@Стивен:

Нет, это не обычная схема. Самый распространенный шаблон — кратковременная блокировка. Менее распространено помечать переменную как изменчивую. Гораздо менее распространенным было бы использование 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();
    }

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


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

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

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

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

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

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

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

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

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

Я не настолько уверен здесь, что вы правы. Я бы порекомендовал прочитать две замечательные статьи: Part 4: Advanced Threading of Threading in C# by Джозеф Альбахари и Джефф Мозер Как блокируются замки? . И, конечно же, Глава 28 (Конструкции синхронизации примитивных потоков) CLR через C# (3-е издание) Джеффри Рихтера.

Вот цитата:

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

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

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

Поэтому я не использую ни 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();

включать следующее:

  • перебирать значения в массиве ссылок на список. Индексация массива выполняется очень быстро.
  • перебирать каждый список (который также реализован внутри как массив). Это тоже очень быстро.
  • создать новый список ссылок (через ToList()), который тоже очень быстрый (просто динамический массив) (.NET имеет очень надежную и быструю реализацию)
  • построить оболочку только для чтения (без копирования, только оболочку итератора)

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

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

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

Я бы рекомендовал прочитать шаблоны параллельное программирование Стивена Туба и Потокобезопасные коллекции в .NET Framework 4 и их характеристики производительности, а также .com/en-us/concurrency/ee851578.aspx" rel="nofollow">здесь много интересных статей.

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

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

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

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

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

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

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

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

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

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

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

8) Попытка отслеживать доступные потоки вряд ли будет эффективной.

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

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

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

person Community    schedule 16.10.2010
comment
Вау, тебя ДЕЙСТВИТЕЛЬНО волнует мой вопрос, лол +1, и я понимаю код C#... Я выучил его через несколько семестров после VB в колледже. Он использует тот же API, что и VB, поэтому все практически одинаково, просто другой синтаксис. +1 за серьезный интерес. - person Jrud; 17.10.2010
comment
Здесь есть несколько хороших идей, но также и некоторые проблемы, в том числе множество чрезмерных сложностей. 1) Я бы не рекомендовал использовать SortedList здесь или где-либо еще, так как это устаревший класс .NET 1.1. 2) В общем случае не нужно усложнять код частными случаями одновременных событий. 3) Метод IsWorking должен просто использовать барьер памяти, а затем напрямую считывать значение. 4) Метод GetJobs выглядит так, как будто он может быть заблокирован на длительный период. Это необходимо? 5) Свободная от блокировки очередь доступна в .NET 4.0. - person Steven Sudit; 17.10.2010
comment
5) Я бы не стал заморачиваться запуском и остановкой диспетчера; просто дайте ему поспать до следующей работы. 6) Вместо того, чтобы беспокоиться о различных видах тиков, вы можете просто придерживаться миллисекунд. 7) Попытка отслеживать доступные потоки вряд ли будет эффективной. - person Steven Sudit; 17.10.2010
comment
@Стивен Судит, спасибо за комментарии. Я стараюсь отвечать в своем посте, а не здесь. - person Nick Martyshchenko; 17.10.2010
comment
Позвольте мне заранее сказать, что я очень хорошо знаком с источниками, на которые вы ссылаетесь, поэтому я постараюсь ограничить сферу своих ответов. Как я уже говорил, в вашем решении есть много хорошего, но я думаю, мы согласны с тем, что многое нужно изменить, прежде чем мы будем готовы к работе. Мои комментарии - это предложения о том, как это можно сделать. В конечном счете, это нужно будет сравнить с реальными данными, но сначала нам нужно что-то простое и правильное. В противном случае, когда мы настроим его на скорость, мы, вероятно, либо обнаружим скрытые разрывы, либо вставим новые. - person Steven Sudit; 17.10.2010
comment
1) Вы правы в том, что вы использовали тип SortedList 2.0, а не 1.1; Я ошибался. Причина, по которой я бы не рекомендовал неуниверсальную коллекцию для такой работы, заключается в том, что необходимость упаковывать и распаковывать вносит сложность и влияет на производительность. Насколько я понимаю, для этой задачи лучше подойдет SortedDictionary<>, чем SortedList, но ни один из них не совсем подходит. В частности, нам нужен список, который не требует перераспределения (связанный список, а не массив), который сортируется по ключу, но извлекается только последовательно и допускает несколько значений для каждого ключа. - person Steven Sudit; 17.10.2010
comment
Как правило, я бы рекомендовал использовать наиболее полнофункциональную коллекцию из библиотеки, но здесь это неприменимо, поскольку это код системного уровня. Нам нужно было бы создать свою собственную, либо с нуля, либо поверх менее многофункциональной коллекции. - person Steven Sudit; 17.10.2010
comment
2) На практике вы вряд ли получите одновременные события, если только они не запланированы на круглые числа (например, с точностью до секунды). Вот почему я предлагаю очередь, которая не пытается объединить несколько событий, а оставляет их рядом. Это упрощает логику диспетчеризации, так как устраняет особый случай. 3) Как написано, метод IsWorking вызывает Interlocked.Exchange без какого-либо обмена. Единственное, что вы могли бы получить из этого, это волатильность, которая лучше обеспечивается любым из упомянутых вами методов. Это в большей степени вопрос читабельности кода, чем производительности. - person Steven Sudit; 17.10.2010
comment
4) Позвольте мне выразить это более конкретно: независимо от того, как вы его нарезаете, это O (n). Хотя я бы рекомендовал сохранять очередь небольшой, вставляя только один экземпляр повторяющегося события за раз, она все же может вырасти до нетривиального размера, что означает, что операция копирования может быть дорогостоящей. Если мы повторно используем экземпляры задания, когда они повторяются, то копия должна быть глубокой, что увеличивает стоимость. В конечном счете, похоже, что для этого метода нет реального варианта использования, но его вызов может привести к задержке выполнения заданий. Исходя из этого, я бы рекомендовал его удалить. - person Steven Sudit; 17.10.2010
comment
5) Вы правы, что ConcurrentQueue не обеспечивает сортировку, но нам это на самом деле не нужно. Наиболее распространенной операцией является удаление головы очереди, что можно сделать без блокировок. Вероятно, следующая наиболее распространенная операция — добавление задания в хвост. Остается случай, когда нам нужно пройтись по очереди, чтобы найти точку вставки, но даже это можно сделать без блокировок; дешевле повторить попытку при проигрыше гонки, чем заблокировать. Вероятно, было бы полезно проверить, ближе ли новая работа к голове или к хвосту, чтобы решить, в каком направлении искать. - person Steven Sudit; 17.10.2010
comment
6) Я делаю это предложение, потому что логика запуска/остановки диспетчера очень сложна и поэтому подвержена ошибкам. Чтобы заставить диспетчера спать, я бы рекомендовал ждать события, используя тайм-аут. Таким образом, он либо проснется, когда будет готово следующее задание, либо когда новое задание будет вставлено перед головкой. В последнем случае может потребоваться запустить его сейчас или установить другое время ожидания. Если представить, скажем, 100 заданий, запланированных на одно и то же мгновение, лучшее, что мы можем сделать, — это поставить их все в очередь. - person Steven Sudit; 17.10.2010
comment
Если нам нужно обеспечить приоритетность, это задание для приоритетной очереди диспетчеризации и нескольких пулов в отношениях производитель/потребитель, но это все еще не оправдывает диспетчер запуска/остановки. Диспетчер всегда должен быть включен, работая в одном цикле, который иногда уступает ядро. - person Steven Sudit; 17.10.2010
comment
7) Придерживаться одного типа тиков — это хорошо, но смешивание и сопоставление становится уродливым, особенно потому, что это зависит от аппаратного обеспечения. Я уверен, что тики будут немного быстрее, чем миллисекунды, потому что он хранит первое и должен делить на константу, чтобы получить второе. Другой вопрос, окажется ли эта операция дорогостоящей, но я не против использовать тики, чтобы избежать риска. - person Steven Sudit; 17.10.2010
comment
8) Проблема, которую я пытаюсь подчеркнуть, заключается в том, что вызов GetAvailableThreads затратен и наивен; ответ устарел еще до того, как вы сможете его использовать. Если бы мы действительно хотели отслеживать, мы могли бы получить начальные значения и вести текущий подсчет, вызвав задание из оболочки, использующей Interlocked.Increment/Decrement. Даже в этом случае предполагается, что остальная часть программы не использует пул потоков. Если нам действительно нужен точный контроль, то правильным ответом здесь будет создание собственного пула потоков. - person Steven Sudit; 17.10.2010
comment
@Steven Sudit, давайте продолжим в комментариях :) 1) я объявляю свою переменную как приватную только для чтения SortedList‹long, List‹Job›› _jobs; так что его общий вариант SortedList, почему вы говорите о его неуниверсальном аналоге? SortedList эффективен при потреблении памяти и повторении, SortedDictionary лучше, когда нам нужна более быстрая вставка. Я измеряю их сам и использую в другом проекте, и обнаружил, что SortedList‹› часто эффективнее, чем SortedDictionary‹›, но, как обычно, это зависит. - person Nick Martyshchenko; 17.10.2010
comment
@2) Я неправильно понимаю. Вы правы, если мы действительно не можем получить одновременное событие. Но что, если мы можем добавить похожие задачи одновременно, как это делаю я в демо, и не хотим объединять их в одну? - person Nick Martyshchenko; 17.10.2010
comment
@3 Это распространенный шаблон для получения значения потокобезопасным способом с использованием методов Interlocked, например, проверьте Джеффри Рихтера. Он опирается на факт Interlocked.CompareExchange(не Interlocked.Exchange, как вы упомянули) не только изменяет значение, но и всегда возвращает старое значение - person Nick Martyshchenko; 17.10.2010
comment
@4 Вы правы. Я беру в качестве старта оригинальный класс @Jrud. Логика, которую вы предлагаете, должна быть реализована, но я думаю, что она далека от цели моего исходного кода. - person Nick Martyshchenko; 17.10.2010
comment
@5 Я не понимаю, как это помогает. Вам нужно поддерживать порядок, поэтому вам нужно найти правильную позицию для вставки элемента (SortedList делает это с помощью обычного BinarySearch). Кроме того, ConcurrentQueue(T) реализуется с использованием списка небольших массивов и операций без блокировок в головном и хвостовом массивах, поэтому он сильно отличается от Queue(T), который поддерживается массивом и полагается на внешнее использование мониторов для обеспечить синхронизацию. Взгляните на реализацию TryDequeue и потокобезопасные коллекции в .NET Framework 4 и их характеристики производительности, и это не всегда лучший выбор. - person Nick Martyshchenko; 17.10.2010
comment
@ 5 Но это помогает избежать блокировки, когда вы получаете голову, вы правы, но стоимость должна быть проверена. - person Nick Martyshchenko; 17.10.2010
comment
@7 Еще один хороший момент, я согласен с вами. Но иногда деление на константу становится затратным и не таким быстрым, как может показаться. Но когда мы говорим о 100 000 DateTimes, это не имеет значения, вы правы, спасибо за точку. - person Nick Martyshchenko; 17.10.2010
comment
@8 Я абсолютно согласен с тем, что обращение к GetAvailableThreads — это наивный метод мониторинга доступных ресурсов через CorGetAvailableThreads, не такой дорогой. Я хочу продемонстрировать, что есть необходимость в управлении ресурсами и, кажется, выбрал плохой пример. - person Nick Martyshchenko; 17.10.2010
comment
@Steven Sudit большое спасибо за комментарии и указания на проблемы - person Nick Martyshchenko; 17.10.2010
comment
Похоже, мы ближе к тому, чтобы быть на одной волне, поэтому я буду отвечать только на те пункты, о которых, кажется, нужно сказать больше. 1) Неважно: ни один из них не подходит. 2) Если две задачи фактически запланированы на одно и то же время (фактически или эффективно), это нормальный случай, не требующий усложнения нашей структуры данных. Другими словами, нам не нужно группировать одновременные задания, чтобы нам приходилось просматривать две разные коллекции; мы можем просто сделать их смежными. - person Steven Sudit; 17.10.2010
comment
3) Нет, это не обычная схема. Самый распространенный шаблон — кратковременная блокировка. Менее распространено помечать переменную как изменчивую. Гораздо реже можно было бы использовать VolatileRead или MemoryBarrier. Использование Interlocked.CompareExchange таким образом неясно, даже если это делает Рихтер. использование его без поясняющего комментария абсолютно гарантированно приведет к путанице, поскольку слово «Сравнить» подразумевает, что мы проводим сравнение, хотя на самом деле это не так. - person Steven Sudit; 17.10.2010
comment
4) Действительно, иногда быстрый замок дешевле сложных незапорных механизмов. Даже если бы здесь это было так, в чем я не уверен, правильным ответом было бы использовать обычную очередь и самостоятельно поддерживать ее порядок, так как нам чаще всего нужно обращаться к ней только из головы или хвоста. Вопреки тому, что вы сказали, SortedList - это O (n) для вставок, что означает, что он не использует двоичный поиск. - person Steven Sudit; 17.10.2010
comment
Да, это интересная проблема, и она вызвала интересную дискуссию. - person Steven Sudit; 17.10.2010
comment
@Steven (4): Я согласен, я обычно реализовывал структуру данных в подобной ситуации, которую вы описываете, но я думаю, что это выходит за рамки примера. Но SortedList‹› использует BinarySearch. Проверьте себя в исходниках или через Reflector в Add it используйте int i = Array.BinarySearch‹TKey›(keys, 0, _size, key, comparer); чтобы определить позицию, в которую нужно вставить. - person Nick Martyshchenko; 17.10.2010
comment
Я перепроверяю: msdn.microsoft.com/en-us/library/ms132330.aspx Это операция O(log n), если новый элемент добавляется в конец списка. Если вставка вызывает изменение размера, операция выполняется за O(n). Так что ты не прав - person Nick Martyshchenko; 17.10.2010
comment
В нем говорится, что этот метод является операцией O (n) для несортированных данных, где n — это количество. Это операция O(log n), если новый элемент добавляется в конец списка. Если вставка вызывает изменение размера, операция выполняется за O(n). Теперь мы можем игнорировать последний случай, потому что любое перераспределение блока фиксированного размера должно быть O(n). Обычно связанный список имеет хвостовой указатель, поэтому вставка в конец стоит O(1), а вставка в отсортированном порядке — O(n), поскольку она должна пройти по списку. (продолжение) - person Steven Sudit; 17.10.2010
comment
Как я понимаю, поскольку он использует массив вместо связанного списка, он может найти точку за O (log n) с помощью двоичного поиска, но все же должен заплатить O (n), чтобы переместить текущие элементы, чтобы сделать место для нового. Таким образом, вставка становится O(n), поскольку преобладает более медленная часть. Это объясняет, почему вставка в конец такая медленная: это бинарный поиск до последнего элемента, за которым не нужно освобождать место. Таким образом, вы правы в том, что он использует двоичный поиск, но было бы неправильно предположить, что его производительность сродни связанному двоичному дереву (то есть O (log n) для большинства операций). - person Steven Sudit; 17.10.2010
comment
Откровенно говоря, это не очень хороший набор характеристик, если только задействованные константы не являются крошечными по сравнению с альтернативой со связанными списками. - person Steven Sudit; 17.10.2010
comment
@ Стивен, мы начали обсуждать SortedList‹›, он всегда выполняет BinarySearch перед вставкой. Проверьте рефлектор или опорный источник. Но вы переключились в обсуждении (когда?) на LinkedList‹›, потому что его поведение отличается. Просто посмотрите, как это реализовано командой Microsoft, и ваши вопросы получат ответы - person Nick Martyshchenko; 17.10.2010
comment
@Steven, и вы правы, структуры связанного списка превосходят массив производительности на основе вставки / удаления, поскольку им не нужно изменять размер (или, по крайней мере, перемещать) исходный массив. Но, как обычно, здесь у нас есть выбор, хотим ли мы быть быстрыми при вставке (SortedDictionary‹›, реализованный как двоичное дерево поиска) или мы хотим быть эффективными с памятью. - person Nick Martyshchenko; 17.10.2010
comment
Я переключился, когда сказал, что SortedList и SortedDictionary не подходят, поэтому лучше использовать LinkedList или ConcurrentQueue. Я не уверен, что коллекция на основе массива будет значительно более эффективной с точки зрения использования памяти ни в принципе, ни на практике. Во-первых, сборщик мусора делает размещение дешевым. Во-вторых, если вы реализуете свой собственный односвязный список, он на самом деле занимает меньше памяти. И, наконец, даже если бы был такой компромисс, мы можем позволить себе сжечь несколько байтов, но не тормозить. Сказав это, .NET определенно не является подходящей средой для критических моментов. - person Steven Sudit; 17.10.2010
comment
Понимаю. Но вы не правы, предполагая, что решения на основе массива неэффективны с точки зрения памяти. Каждая ссылка на класс стоит 24 байта служебных данных в 32-битном формате. Таким образом, когда у вас есть 100 000 000 ссылок на классы, вы получаете 2,4 ГБ накладных расходов, чтобы хранить их в памяти. Массив структур не имеет таких накладных расходов. Но в моем коде, поскольку Job является классом, почти нет разницы (вы правы), поскольку они оба содержат ссылку на класс. Я чувствую, что мы идем в неправильном направлении, начав обсуждать абстрактные идеи оптимизации без реальных технических требований к поведению системы. Мы оба правы. Это зависит от... правила :) - person Nick Martyshchenko; 17.10.2010
comment
Да, это всегда зависит от деталей. Самая большая деталь в том, что .NET совсем не идеален для этой задачи. Сборщик мусора может сработать в любой момент, точность таймера низкая, и нет никакой гарантии, что таймер сработает вовремя. Выполнение этого на С++ помогло бы, но более глубокая проблема заключается в самой Windows, поскольку она просто не предназначена для такого рода вещей. - person Steven Sudit; 17.10.2010
comment
Через .NET идите в правильном направлении :) Сама Windows никогда не была ОС жесткого реального времени. GC становится более управляемым в .NET, он начинает объявлять о полном GC, и есть несколько вариантов, чтобы почти избежать полного сбора, если приложение предназначено для этого, есть возможность создать собственный хост CLR, но все это далеко от нашей первоначальной темы. @Steven спасибо за отличные комментарии и обсуждение, я читаю и учусь у них с большим удовольствием. - person Nick Martyshchenko; 18.10.2010
comment
Если хотите, можем продолжить :) Я, как обычно, перед этим прокомментирую ваши комментарии :) Я просто подумал, что мы уводим наших читателей с правильного направления, и они потеряли в деталях :) - person Nick Martyshchenko; 18.10.2010

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

изменить

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

изменить

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

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

person Steven Sudit    schedule 15.10.2010
comment
Это звучит как стандартный способ решения этой проблемы, у вас есть пример шаблона проектирования, который показывает это? - person Jrud; 15.10.2010
comment
Это мой стандартный способ тоже. Никакого шаблона проектирования, это следует из простой логики. Единственное, что вам нужно время, это продолжительность до следующего события. Это занимает один таймер. И список событий, отсортированный по времени. Список может быть сколь угодно большим. - person Hans Passant; 15.10.2010
comment
Как вы думаете, ребята, как работают системные таймеры? Они делают именно это. Определите, когда истечет срок действия следующего самого быстрого события, засните на это время, пробудитесь и обработайте событие, а затем повторите. - person Ziffusion; 15.10.2010
comment
Так что же происходит, когда более 1000 событий должны быть выполнены в течение секунды? Класс таймера имеет минимальное время истечения 1 миллисекунду. Я также не могу легко создавать несколько очередей, потому что события могут динамически зависать в порядке. - person Jrud; 15.10.2010
comment
@Hans: Вы правы, что это нельзя назвать шаблоном проектирования. Это просто алгоритм, причем довольно старый и очевидный. - person Steven Sudit; 15.10.2010
comment
@Jrud: это даже не с точностью до 1 мс. В большинстве систем разрешение с точностью до 16 мс. Есть такая вещь, как таймер высокого разрешения, но я считаю, что он в некотором роде зависит от системы. См. msdn.microsoft.com/en-us/ библиотека/ - person Steven Sudit; 15.10.2010
comment
Избегайте создания множества системных таймеров System.Timers.Timer / System.Threading.Timer, поскольку они являются оболочкой объектов таймера ожидания Win32 (см. aspx" rel="nofollow noreferrer">msdn.microsoft.com/en-us/magazine/cc164015.aspx). Лучше всего в этой ситуации подходит колесо таймера. Также я бы рекомендовал избегать нити span каждый раз, когда вам нужна обработка, вместо этого используйте ThreadPool. Проверьте msdn.microsoft.com/en-us/magazine/cc164139.aspx подробности. - person Nick Martyshchenko; 15.10.2010
comment
@Sentinel: Да, вы определенно хотите отправлять события в пул потоков, чтобы избежать накладных расходов на создание / удаление потоков и разрешить одновременные события. - person Steven Sudit; 15.10.2010
comment
@Ziff: Как предполагает Sentinel, таймеры ОС немного тяжеловесны. Не годится для тысяч. - person Steven Sudit; 15.10.2010
comment
@Steven Sudit Разрешение 16 мс еще больше снижает мою уверенность в использовании этого алгоритма для моего проекта. Колесо таймера отлично подходит для нескольких таймеров, которые вы можете либо поставить в очередь последовательно (где несколько таймеров можно легко использовать), либо в ситуациях, когда события не должны срабатывать несколько тысяч раз в секунду. Мне на самом деле немного грустно, что похоже, что я не могу легко это реализовать, выглядит интересно. - person Jrud; 15.10.2010
comment
@Jrud: Это не следует. Вы можете использовать колесо таймера со спин-блокировкой вместо использования объекта таймера ОС. - person Steven Sudit; 15.10.2010
comment
@Sentinel: Еще один хороший совет. Да, весь смысл возможности планировать так много частей состоит в том, чтобы разбить задачу на куски, которые делают свое дело, и передать поток чему-то другому, как в случае с асинхронным вводом-выводом. Если у вас много событий и они длятся долго, пул потоков будет перегружен. Что касается задач .NET 4.0, они великолепны, но здесь они не кажутся напрямую актуальными. - person Steven Sudit; 16.10.2010
comment
@Sentinel: Если вы выделите свой совет в отдельный ответ, я буду рад проголосовать за него. - person Steven Sudit; 16.10.2010
comment
Я не в восторге от дополнительных циклов ЦП, которые поначалу будет представлять спин-блокировка, но это может быть решением в долгосрочной перспективе. Поскольку в очереди больше элементов, не должно быть так много циклов, потраченных впустую на спин-блокировку, они все будут использованы. - person Jrud; 16.10.2010
comment
@Steven: Спасибо :) Я думаю, вам будет лучше окончательно отредактировать свой пост, когда мы соберем разные мнения из комментариев. Я начал описывать колесо таймера, когда заметил ваш ответ, и решил не публиковать свой собственный. - person Nick Martyshchenko; 16.10.2010
comment
@Jrud: Если следующее событие будет через 10 минут, вы будете спать, а не вращаться. Но вы бы проснулись немного раньше и позаботились бы о том, чтобы быть точно вовремя. - person Steven Sudit; 16.10.2010
comment
@Sentinel: решать вам. Я, безусловно, объединим лучшие ответы в один, но это не помешает вам получить полную оценку, имея собственный ответ. - person Steven Sudit; 16.10.2010
comment
@Sentinel & Steven Sudit Я отредактировал исходный пост, это то, что вы имели в виду для колеса таймера? - person Jrud; 16.10.2010

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

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

person Ziffusion    schedule 15.10.2010
comment
У него таймер ОС Windows, не очень удачная реализация. :-) - person Steven Sudit; 15.10.2010

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

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

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

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

person Mike Dunlavey    schedule 15.10.2010
comment
Вы можете сделать это как угодно сложным, но я не вижу в этом особой пользы. - person Steven Sudit; 17.10.2010
comment
Я имею в виду предложение разделить задания на несколько очередей. Хотя этот подход, безусловно, применим в реальном примере с авиакомпанией, я уверен, что для решения проблемы ОП лучше всего подходит простое, доказуемо правильное решение всего с одной очередью. Например, наличие очереди для каждого потока может легко привести к большему сходству, чем нам бы хотелось, позволяя откладывать задания, даже если доступен другой поток. - person Steven Sudit; 17.10.2010