Эффективность таймера
Я планирую разработать систему с десятками тысяч объектов, в каждом из которых будет до 42(но более вероятно, примерно на 4 или 5) отдельных действий, которые они потенциально будут выполнять через регулярные промежутки времени. Я также планирую написать код, который будет деактивировать таймеры до тех пор, пока объект не начнет использоваться. В режиме ожидания объектам потребуется только 1 таймер каждый, но когда они активны, все остальные таймеры запускаются сразу. Сначала количество объектов будет небольшим, возможно, несколько сотен, но я ожидаю, что оно будет расти в геометрической прогрессии, и через несколько месяцев оно достигнет десятков тысяч.
Поэтому я очень обеспокоен эффективностью кода, который буду писать для таймеров и для этих объектов. Есть три уровня, на которых я мог бы написать это приложение, чтобы все успешно выполняли необходимые задачи. Кроме того, я планирую запустить эту систему на сервере Quad Core, поэтому я хотел бы использовать многопоточность везде, где это возможно.
С этой целью я решил использовать класс System. Timers. Timer, который запускает новый поток для каждого события elapse.
Это 3 уровня, которые я рассматриваю:
Один таймер управляет всем приложением, он перебирает каждый объект, проверяет, нужно ли запускать какие-либо другие действия, и если да, запускает их, а затем переходит к следующему.
Многоуровневый таймер, где каждый объект имеет главный таймер, который проверяет все функции, которые может понадобиться объекту, запускает все готовые, а затем устанавливает следующий интервал таймера на следующее требуемое время действия.
Таймер рекурсивного уровня, где каждое действие в каждом объекте имеет свой собственный таймер, который будет запущен, а затем настроен на запуск при следующем доступе.
Проблема с вариантом 1 заключается в том, что при таком количестве объектов и действий один единственный таймер истечения времени таким образом может работать, возможно, более 20 секунд (в то время как он выполнил несколько миллионов строк зацикленного кода), где это, вероятно, должно происходить каждые 1 секунду., Если объекты не синхронизированы, система, вероятно, не будет работать хорошо.
Проблема с вариантом 2 заключается в том, что писать его будет немного сложнее, чем в варианте 3, но ненамного, это также будет означать, возможно, более 10 000 или более таймеров, работающих в системе (по одному для каждого объекта), создавая и уничтожая потоки с каждым Похоже, это никого не касается (что я не уверен, является ли это проблемой или нет). В этом случае каждый таймер должен срабатывать как минимум раз в секунду, при этом может быть запущено несколько сотен строк кода (в крайнем случае - до тысячи).
Проблема с вариантом 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
Я правильно понял?
~ Редактировать 2~
Поменял слово "Задача" на "Работа", чтобы люди могли перестать жаловаться на это;)
~ Редактировать 3~
Добавлены переменные для отслеживания времени и обеспечения возникновения циклов
4 ответа
РЕДАКТИРОВАТЬ: Я помню интересное интервью определенно стоит посмотреть: Арун Кишан: Внутри Windows 7 - Прощание с блокировкой диспетчера ядра Windows
Как сказал @Steven Sudit, я снова предупреждаю: используйте его только в качестве демонстрации того, как работает колесо таймера, и о некоторых задачах, которые вам нужно учитывать при его реализации. Не в качестве эталонной реализации. В реальном мире вы должны написать гораздо более сложную логику, чтобы учесть доступные ресурсы, логику планирования и т. Д.
Вот хорошие моменты, изложенные Стивеном Судитом (подробности см. В комментариях к постам):
1) Выберите правильную структуру, чтобы сохранить список вакансий (как обычно, зависит):
SortedList<> (или SortedDictionary<>) хорош для потребления памяти и индексации, но должен реализовывать синхронизированный доступ
ConcurrentQueue<> поможет вам избежать блокировки, но вам придется реализовать упорядочение. Это также очень эффективно для памяти
LinkedList<> хорош при вставке и извлечении (в любом случае нам нужен только заголовок), но требует синхронизированного доступа (через него легко реализовать без блокировок) и не так эффективно использует память, поскольку хранит две ссылки (prev / next). Но это становится проблемой, когда у вас есть миллионы рабочих мест, поэтому все они занимают значительное количество памяти.
Но:
Я полностью согласен с @Steven:
Это не имеет значения: ни один из них не подходит. Правильный ответ будет состоять в том, чтобы использовать обычную очередь и поддерживать ее порядок самостоятельно, поскольку нам чаще всего необходимо получать к ней доступ только из головы или хвоста.
Как правило, я бы рекомендовал использовать наиболее полнофункциональную коллекцию из библиотеки, но здесь это неприменимо, потому что это код системного уровня. Нам нужно было бы свернуть свою собственную, либо с нуля, либо поверх менее функциональной коллекции.
2) Чтобы упростить логику обработки одновременных заданий, вы можете добавить список делегатов (например, через ConcurrentQueue, чтобы сделать его свободным от блокировки) в исходный класс заданий, поэтому, когда вам нужно другое задание в то же время, вы просто добавляете другого делегата для запуска.
@Steven:
Если две задачи фактически запланированы на одно и то же время (как на самом деле, так и эффективно), это нормальный случай, который не требует усложнения нашей структуры данных. Другими словами, нам не нужно группировать одновременные задания, чтобы нам приходилось обходить две разные коллекции; мы можем просто сделать их смежными
3) Запуск / остановка диспетчера не так прямолинейен, как может быть, и поэтому может привести к ошибкам. Вместо этого вы можете ждать события, используя тайм-аут.
@Steven:
Таким образом, он будет либо просыпаться, когда следующее задание готово, либо когда новое задание будет вставлено перед заголовком. В последнем случае может потребоваться запустить его сейчас или установить другое ожидание. Если представлено, скажем, 100 заданий, запланированных на одно и то же мгновение, лучшее, что мы можем сделать, это поставить их в очередь.
Если нам нужно было установить приоритеты, это задача для очереди диспетчеризации с приоритетами и нескольких пулов в отношениях производитель / потребитель, но она по-прежнему не оправдывает диспетчер запуска / остановки. Диспетчер всегда должен быть включен, работать в одном цикле, который иногда уступает ядро
4) Об использовании галочек:
@Steven:
Хорошо придерживаться одного типа тиков, но микширование и сопоставление становится уродливым, особенно потому, что это зависит от аппаратного обеспечения. Я уверен, что тики будут немного быстрее, чем миллисекунды, потому что он хранит первое и должно делиться на константу, чтобы получить второе. Является ли эта операция дорогостоящей - это другой вопрос, но я хорошо использую галочки, чтобы избежать риска.
Мои мысли:
Еще один хороший момент, я согласен с вами. Но иногда деление на константу становится дорогостоящим и не таким быстрым, как может показаться. Но когда мы говорим о 100 000 DateTimes, это не имеет значения, вы правы, спасибо, что укажете.
5) "Управление ресурсами":
@Steven:
Проблема, которую я пытаюсь подчеркнуть, состоит в том, что вызов GetAvailableThreads дорог и наивен; ответ устарел, прежде чем вы сможете его использовать. Если бы мы действительно хотели отслеживать, мы могли бы получить начальные значения и сохранить счетчик выполнения, вызвав задание из оболочки, которая использует Interlocked.Increment/Decrement. Даже в этом случае предполагается, что остальная часть программы не использует пул потоков. Если мы действительно хотим точный контроль, то правильный ответ здесь - это запустить наш собственный пул потоков.
Я абсолютно согласен с тем, что обращение к GetAvailableThreads является наивным методом для отслеживания доступных ресурсов через CorGetAvailableThreads, не столь дорогостоящим. Я хочу продемонстрировать, что есть необходимость управлять ресурсами, и, кажется, выбирает плохой пример.
Любые средства, представленные в примере с исходным кодом, не должны рассматриваться как правильный способ мониторинга доступных ресурсов. Я просто хочу продемонстрировать, что вы должны думать об этом. Через, может быть, закодирован не такой хороший кусок кода, как пример.
6) Использование Interlocked.CompareExchange:
@Steven:
Нет, это не обычная модель. Наиболее распространенная схема - это кратковременная блокировка. Менее распространенным является отметить переменную как 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 говорит, что у него, вероятно, будет много задач для планирования, я думаю, что у них может быть тяжелый параллелизм, поэтому я демонстрирую, как его решить. Но моя точка зрения: даже если у вас низкий уровень параллелизма, у вас все равно есть шанс получить события одновременно. Также это легко возможно в многопоточной среде или когда есть много источников, желающих запланировать задания.
Взаимосвязанные функции не так сложны, дешевы и, так как.NET 4.0 встроены, так что нет проблем, чтобы добавить защиту в такой ситуации.
3) Метод IsWorking должен просто использовать барьер памяти, а затем непосредственно читать значение.
Я не уверен, что ты прав. Я бы порекомендовал прочитать две хорошие статьи: Часть 4: Расширенные потоки в C# Джозефа Албахари и Как блокировать блокировки? Джефф Мозер. И, конечно, глава 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();
включают в себя следующее:
- Перебирать значения в массиве ссылок на List. Индексирование массива очень быстрое.
- итерации по каждому списку (который также реализован как массив). Это тоже очень быстро.
- создать новый список ссылок (через ToList()), который тоже очень быстрый (просто динамический массив) (.NET имеет очень надежную и быструю реализацию)
- построить оболочку только для чтения (без копии, только обертка итератора)
следовательно, мы только что сгладили список ссылок только для чтения на объекты Иова. Это очень быстро, даже если у вас есть миллионы задач. Попробуйте измерить себя.
В любом случае я добавил его, чтобы показать, что происходит во время цикла выполнения (для целей отладки), но я думаю, что это может быть полезно.
5) Очередь без блокировки доступна в.NET 4.0.
Я бы порекомендовал прочитать образцы параллельного программирования Стефана Тауба и Thread-safe Collections в.NET Framework 4 и их характеристики производительности, а также здесь много интересных статей.
Итак, я цитирую:
ConcurrentQueue(T) - это структура данных в.NET Framework 4, которая обеспечивает поточно-ориентированный доступ к упорядоченным элементам FIFO (первым пришел-первым вышел). Под капотом ConcurrentQueue(T) реализован с использованием списка небольших массивов и операций без блокировки над головным и хвостовым массивами, следовательно, он довольно сильно отличается от Queue (T), который поддерживается массивом и зависит от внешнего использования. мониторов для обеспечения синхронизации. ConcurrentQueue(T), безусловно, более безопасен и удобен, чем ручная блокировка очереди (T), но для определения относительной производительности двух схем требуются некоторые эксперименты. В оставшейся части этого раздела мы будем ссылаться на заблокированную вручную очередь (T) как автономный тип, называемый SynchronizedQueue(T).
У него нет методов для поддержания упорядоченной очереди. Ни одна из новой многопоточной коллекции, все они поддерживают неупорядоченную коллекцию. Но, читая оригинальное описание @Jrud, я думаю, что мы должны вести упорядоченный список времени, когда нужно выполнить задание. Я ошибся?
6) я бы не стал запускать и останавливать диспетчер; просто дай поспать до следующей работы
Знаете ли вы хороший способ сделать нить Sleep ThreadPool? Как вы это будете реализовывать?
Я думаю, что диспетчер уходит в "спящий режим", когда он не обрабатывает какую-либо задачу и не намечает ее. Во всяком случае, нет никакой специальной обработки, чтобы усыпить или проснуться, поэтому в моих мыслях этот процесс равняется "сну".
Если вы сказали, что я должен просто перепланировать RunJobs через ThreadPool, когда нет доступных заданий, если вы ошибаетесь, это израсходует слишком много ресурсов и может повлиять на запущенные задания. Попробуй себя. Зачем делать ненужную работу, когда мы можем легко ее избежать.
7) Вместо того, чтобы беспокоиться о разных видах тиков, вы можете просто придерживаться миллисекунд.
Ты не прав. Либо вы придерживаетесь клещей, либо вас это совершенно не волнует. Проверьте реализацию DateTime, каждый доступ к свойству в миллисекундах включает преобразование внутреннего представления (в тиках) в мс, включая деление. Это может ухудшить производительность на старых (класс Pentium) компиляторах (я измеряю это сам, и вы тоже).
В общем я с тобой соглашусь. Мы не заботимся о представлении здесь, потому что это не дает нам заметного повышения производительности.
Это просто мой привычка. Я обрабатываю миллиарды DateTime в недавнем проекте, так закодированном в соответствии с ним. В моем проекте есть заметная разница между обработкой тиками и другими компонентами DateTime.
8) Попытка отследить доступные потоки не кажется эффективной
Я просто хочу продемонстрировать, что ты должен заботиться об этом. В реальном мире вы должны реализовать далеко от моей прямой логики планирования и мониторинга ресурсов.
Я хочу продемонстрировать алгоритм колеса таймера и указать на некоторую проблему, о которой автор должен подумать при его реализации.
Вы абсолютно правы, я должен предупредить об этом. Я думал, что "быстро ptototype" будет достаточно. Мое решение ни в коем случае нельзя использовать в производстве.
Ни один из вышеперечисленных. Стандартное решение состоит в том, чтобы вести список событий, чтобы каждое из них указывало на следующее событие. Затем вы используете один таймер, и он включается только во время следующего события.
редактировать
Похоже, это называется колесо таймера.
редактировать
Как указал Sentinel, события должны отправляться в пул потоков. Обработчик этих событий должен выполнить небольшую часть работы как можно быстрее и без блокировки. Если ему нужно выполнить ввод / вывод, он должен запустить асинхронную задачу и завершиться. В противном случае пул потоков будет переполнен.
.NET 4.0 Task
Класс может быть полезен здесь, особенно для его методов продолжения.
Это напоминает мне о старых системах продажи авиабилетов, где у вас были очереди. Запросы на покупку билетов помещались в разные очереди в зависимости от того, какое внимание им нужно.
Поэтому, возможно, у вас может быть очередь объектов, требующих частого внимания, и очередь объектов, требующих нечастого внимания. При необходимости вы перемещаете их от одного к другому.
Вы можете иметь таймер для частой очереди и таймер для нечастой очереди. Для частой очереди вы можете разбить ее на несколько очередей, по одной для каждого потока.
Для сокращения частых очередей у вас не должно быть больше потоков, чем ядер. Если у вас есть два ядра, то вы хотите, чтобы оба они запускались. Больше потоков, чем это, не сделает вещи быстрее. Фактически, если для обработки объектов требуется дисковый ввод-вывод или подключение к другому совместному оборудованию, это может даже не помочь запустить оба ядра.
Компромисс в ваших трех вариантах между памятью и процессором. Чем больше таймеров, тем больше узлов таймеров (памяти), а объединение этих таймеров в меньшее количество таймеров означает больше ЦП, так как вы проверяете события, которые требуют обслуживания во время выполнения. Загрузка ЦП при запуске слишком большого количества таймеров (и их истечении) не слишком большая проблема с приличной реализацией таймера.
Так что, на мой взгляд, если у вас есть хорошая реализация таймера, выберите для запуска столько таймеров, сколько вам нужно (будьте как можно более детализированы). Но если любой из этих таймеров для каждого объекта является взаимоисключающим, рассмотрите возможность повторного использования узла таймера.