计时器效率 [英] Timer Efficiency

查看:314
本文介绍了计时器效率的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我的规划,发展与对象的话,几万,这将每人最多可有42(但更可能下跌约4或5)单独行动,他们将可能被执行定期的系统。我还打算写code,将取消计时器,直到对象来使用。空闲时,对象将只需要每1个定时器,但活跃的时候,其他的定时器都将开始一次。起初对象的数量将是小,也许几百,但我希望它成倍增长,并在短短几个月内,就达到了在数以万计。

所以,我很担心的code我会写的计时器效率,并为这些对象。有三个层次中,我可以写,这个应用程序将全部顺利完成所需的任务。另外,我计划运行此系统中的四核服务器上的,所以我想利用多线程尽可能。

为此,我已经决定要使用的触发一个新的线程为每个经过事件System.Timers.Timer的类。

以上是3个级别我正在考虑:

  1. 一组定时器操作整个应用程序,它通过每个对象的遍历,检查,看看是否有任何其他的行动需要被解雇,如果是这样,运行它们,然后移动到下一个。

  2. 多层定时器,每个对象都有一个主计时器,检查所有的对象可能需要履行的职能,运行任何准备,然后设置一个计时器的时间间隔下所需作用时间。

  3. 递归层定时器,其中在每个对象的每个动作都有它自己的定时器将被触发,然后设置运行在下一次是可用的。

与选项1的问题是,有这么多的对象和操作,一个奇异的定时器经过以这种方式可以运行可能超过20秒(当它执行了几百万行的环状code),其中,这应该可能会被勾选每隔1秒。如果对象没有保持同步,系统将可能无法正常工作。

与选项2的问题是,这将是一个有点难度比选3来写,但不是很多,这也将意味着运行的系统(每个对象)上也许10,000也许计时器,创建和销毁每个线程经过像它的人的业务(这我不知道,如果这是一个问题或没有)。每个定时器将不得不解雇至少每秒一次在这种情况下,用code运行也许有好几百行(最多可能有一千个极端的例子)。

与选项3的问题是计时器有可能被引入到系统中的绝对数量。我说的是一起在同一时间运行近100,000定时器的潜在平均10000定时器。每个经过的事件可能只需要运行但小于或等于50行code,使他们非常短。的经过事件将有一个第二的百分之一之间的延迟上一个极端,五分钟,另一方面,与平均可能被周围的1秒

我精通Visual Basic .NET中,并计划在这写的,但我还可以恢复到我的高中天,并尝试写在C ++的效率,它是否会做出多大的差别(请让我知道,如果你有语言之间code效率任何来源)。另外一个集群Linux服务器,而不是我的四核Windows服务器上运行此概念玩弄,但我不知道如果我能得到我的任何.NET应用程序,以这样的Linux集群上运行(喜欢的任何信息上也是)。

的主要问题为这个主题来回答是:

我使用选项1,2,或3,为什么?

〜审议意见后,编辑〜

所以涉及到计时器轮自旋锁的第四选择。下面是一个作业类:

 公共类职位
私人dFireTime为DATETIME
私人objF作为CrossAppDomainDelegate
私人objParams()为对象

公共子新(BYVAL Func键作为CrossAppDomainDelegate,BYVAL PARAMS()为对象,BYVAL FireTime为DATETIME)
    objF = Func键
    dFireTime = FireTime
    objParams = PARAMS
结束小组

公共只读属性FireTime()
    得到
        返回dFireTime
    最终获取
高端物业

公共只读属性Func键()作为CrossAppDomainDelegate
    得到
        返回objF
    最终获取
高端物业

公共只读属性PARAMS()作为对象()
    得到
        返回objParams
    最终获取
高端物业
末级
 

然后主循环的实现:

 私人任务链表(岗位)

私人小组RunTasks()
    虽然真
        昏暗CURRENTTIME的日期时间= Datetime.Now

        如果尚未Tasks.Count = 0 AndAlso任务(0).FireTime> CURRENTTIME然后
            昏暗T作为工作=任务(0)
            Tasks.RemoveFirst()
            T.Func.Invoke()
        其他
            昏暗MillisecondDif作为双

            MillisecondDif =任务(0).FireTime.Subtract(CURRENTTIME).Milliseconds
            如果MillisecondDif> 30然后
                Threading.Thread.Sleep(MillisecondDif)
            结束如果
        结束如果

    结束在
结束小组
 

我是不是正确的?

EpicClanWars.com

〜编辑2〜

切换单词任务出去工作,因此脂肪酶可以停止抱怨吧)

〜编辑3〜

新增变量跟踪时间和放大器;在需要的时候确保spinloops发生

解决方案

编辑:我记得有趣的采访definetely值得观点:<一href="http://channel9.msdn.com/Shows/Going+Deep/Arun-Kishan-Farewell-to-the-Windows-Kernel-Dispatcher-Lock"相对=nofollow>阿伦·基尚:深入了解Windows 7 - 告别Windows内核调度锁

由于@Steven Sudit说,我再次提醒:只使用它作为演示你如何定时器轮作品和一些任务去在乎,而实现它。不作为参考实现。在现实世界中,你必须编写更复杂的逻辑来考虑到现有的资源,调度逻辑等。


由史蒂芬Sudit说这里好点(阅读细节发表评论):

1)选择合适的结构,让您的作业列表(这取决于为通常情况下):

  • 排序列表&LT;>(或SortedDictionary&LT;>)良好的内存消耗和索引,但必须实现同步访问

  • ConcurrentQueue&LT;>将帮助你避免锁定,但你必须执行顺序。它也非常的内存使用效率

  • LinkedList的&LT;>良好的插入和检索(反正我们只需要头),但需要同步访问并没有那么内存使用效率(直通很容易通过无锁实现的),因为它存储两个引用(preV /下一个)。但它成为一个问题,当你有百万个就业机会,以便他们都采取显著的存储量。

不过:<​​/ P>

我完全同意@Steven:

  

没关系:没有,其中之一是一个不错的选择。正确的答案是使用常规的队列,并维持其秩序自己,因为我们经常需要只能从头部或尾部访问它。

     

通常情况下,我会建议使用功能最全收集从库,但在这里并不适用,因为这是系统级的code。我们就需要推出我们自己的,无论是从头开始还是不太功能丰富的集合顶部

2)为了简化并行作业的处理逻辑,你可以添加委托列表(例如,通过ConcurrentQueue,使其无锁)到原来的工作类,所以,当你需要同时另一份工作,你只需要添加另一名代表开始。

@Steven:

  

如果实际安排在同一时间两项任务(是否实际或有效),这是一种正常情况下,它不需要复杂我们的数据结构。换句话说,我们并不需要将多达并行作业,使我们不得不遍历两个不同的集合;我们可以只让他们靠近

3)启动/回采调度不那么straightful,因为它可以,因此可能会导致错误。相反,你可以在一个事件,而使用超时等待。

@Steven:

  

这样,它要么醒来时,接下来的工作就是准备好,或者当一个新的工作是头前插入。在后一种情况下,它可能需要将现在运行它,或者设置不同的等待。如果presented用,比方说,100个职位全部安排在同一时刻,我们能做到的最好的事情就是排队他们都放弃了。

     

如果我们需要提供优先,这是一个优先级调度队列,多个泳池的生产者/消费者关系的工作,但它仍然不能证明一个开始/停止调度。调度程序应该始终,在单个循环中运行,有时割让芯

4)关于使用蜱:

@Steven:

  

坚持一种类型的蜱是好的,但混合和匹配变得难看,特别是因为它的硬件相关的。我敢肯定,蜱虫会比毫秒稍快一些,因为它存储了前者,并有一个恒定的划分,以获得后者。这是否操作蜿蜒被昂贵又是另一回事,但我很好使用蜱,以避免风险。

我的想法:

  

另一个好一点,我同意你的看法。但有时除以常数变得昂贵,它不会那么快,因为它可能似乎是。但是,当我们谈论创建DateTime对象的约10万不要紧,你是对的,谢谢你来点。

5)的管理资源:

@Steven:

  

我想强调的问题是,调用GetAvailableThreads是昂贵的,幼稚的;答案是过时之前,你甚至可以用它。如果我们真的想跟踪,我们可以得到初始值,并保持运行计数通过调用使用Interlocked.Increment /递减的包装工作。即使如此,这$该程序的其余部分没有使用线程资源池P $ psumes。如果我们真的想要很好的控制,那么,正确的答案在这里推出我们自己的线程池

我绝对同意打电话给GetAvailableThreads是幼稚的方法来监测直通CorGetAvailableThreads不那么昂贵的可用资源。我想demontrate有需要来管理资源,似乎选择不好的榜样。

通过在源$ C ​​$ C例如提供任何手段是不能被视为正确的方式来监控可用资源。我只是想证明,你必须考虑一下。直通也许codeD没有那么好的一块code为例。

6)使用Interlocked.CompareExchange:

@Steven:

  

没有,这不是一个常见的​​模式。最常见的模式是简单地锁。较少见的是标志变量波动。更常见的是使用VolatileRead或MemoryBarrier。使用Interlocked.CompareExchange这种方式是模糊的,即使发生里氏做的。使用它没有说明注释绝对保证混淆,因为这个词比较意味着我们正在做一个比较的时候,其实我们不是。

您是对的,我不得不点一下它的使用方法。


 使用系统;
使用的System.Threading;

// Job.cs

// 警告!你的工作(任务)必须是异步或至少真正短版活
//否则会毁掉整个设计和线程池的使用,由于潜在的重并发耗尽可用的工作线程

// BTW,!=作业量,通过线程池调度的工作线程的数量
//作业可能会(通过异步调用开始/结束)等待任何IO在某一时刻
//所以免费的工作线程到另一个等待亚军

//如果不能达到这个要求,就用通常的Thread类
//但是你会失去所有线程池的优势,会得到显着的开销

//读取http://msdn.microsoft.com/en-us/magazine/cc164139.aspx的一些细节

//我命名,而不是任务级作业,以避免混乱与.NET 4任务
公共类职位
{
    公开日期时间FireTime {获得;私定; }

    公共WaitCallback DoAction {获得;私定; }
    公共对象参数{获得;私定; }

    //请使用UTC日期时间,以避免不同的时区的问题
    //还要考虑到_永远_使用DateTime.Now在重复任务,因为它显著慢
    //比DateTime.UtcNow(由于使用的时区和转换时间根据它)

    //这里,我们始终与UTC合作
    //它会为你节省大量的时间,当你的项目将获得工作(任务)宣布从不同的时区
    公共静态工作在(日期时间fireTime,WaitCallback doAction,对象参数= NULL)
    {
        返回新的工作{FireTime = fireTime.ToUniversalTime(),DoAction = doAction,参数=参数};
    }

    公共重写字符串的ToString()
    {
        返回的String.Format({0}({1})的{2},DoAction = NULL DoAction.Method.Name:!?的String.Empty,参数,
                             FireTime.ToLocalTime()的ToString(O))。
    }
}
 


 使用系统;
 使用System.Collections.Generic;
 使用System.Diagnostics程序;
 使用System.Linq的;
 使用的System.Threading;

// Dispatcher.cs

//看看System.Runtime IOThreadTimer.cs和IOThreadScheduler.cs
//在Microsoft参考源,其有趣的阅读

公共类调度
{
    //你需要用火的时间排序的任务。我用蜱作为重点争取在检查一些速度提升
    //有在同一时间可能超过一个任务
    私人只读排序列表&LT;长名单,其中,招聘&GT;&GT; _jobs;

    //同步对象访问_jobs(和_timer)并使其线程安全
    //见注释中ScheduleJob有关锁定
    私人只读对象_syncRoot;

    //队列(RunJobs法)正在运行的标志
    私人诠释_queueRun;

    //下垂至prevent污染线程池有很多次计划JobsRun
    私人诠释_jobsRunQueuedInThreadPool;

    //我会用秒表测量经过时间间隔。它是围绕QueryPerformanceCounter的包装
    //它不消耗任何额外的资源,从操作系统到数

    //用于检查OS多少蜱(不DateTime.Ticks!)已经过去
    私人只读秒表_curTime;

    //调度开始时间。它用于建造时间增量作业
    私人只读长_startTime;

    // System.Threading.Timer安排下一个活动时间
    //你必须实现syncronized访问,因为它不是线程安全的
    // http://msdn.microsoft.com/en-us/magazine/cc164015.aspx
    私人只读定时器_timer;

    //最小定时器增量安排,通过定时器来代替线程池下一个电话
    //读取http://www.microsoft.com/whdc/system/pnppwr/powermgmt/Timer-Resolution.mspx
    //默认情况下,约15毫秒
    //如果你想知道通过互操作是完全使用GetSystemTimeAdjustment(http://msdn.microsoft.com/en-us/library/ms724394(VS.85).aspx)
    //你想从那里TimeIncrement参数
    私人常量长MinIncrement = 15 * TimeSpan.TicksPerMillisecond;

    //每个队列运行允许的最大预定作业(指定自己合适的值!)
    //计划将增加线程池的队列(因此​​将它们算作处理)没有比这更不变

    //这是之间如何快速作业被计划后,经过的时间在一边平衡,
    //多久JobsList就会被阻塞,RunJobs拥有的线程池的线程
    私人const int的MaxJobsToSchedulePerCheck = 10;

    //队列长度
    公众诠释长度
    {
        得到
        {
            锁定(_syncRoot)
            {
                返回_jobs.Count;
            }
        }
    }

    公共调度()
    {
        _syncRoot =新的对象();

        _timer =新的定时器(RunJobs);

        _startTime = DateTime.UtcNow.Ticks;
        _curTime = Stopwatch.StartNew();

        _jobs =新的排序列表&LT;长名单,其中,招聘&GT;&GT;();
    }


    //是调度员还在工作
    // 警告!队列结束其工作的时候没有更多的工作来安排,但启动的作业仍然可以工作
    公共BOOL IsWorking()
    {
        返回Interlocked.CompareExchange(参考_queueRun,0,0)== 1;
    }

    //只是方便的方法来获取当前的工作目录
    公开的IEnumerable&LT;工作&GT; GetJobs()
    {
        锁定(_syncRoot)
        {
            //我们复制原来的价值和回报为只读集合(线程安全的原因)
            返回_jobs.Values​​.SelectMany(名单=&GT;清单)。.ToList()AsReadOnly();
        }
    }

    //添加作业调度队列(安排它)
    公共无效ScheduleJob(工作任务)
    {
        // 警告!如果你有大并发这将引入瓶颈。
        //你必须实现无锁解决方案,以避免botleneck但这是另外一个复杂的话题。
        //你也可以通过使用杰弗里里希特的ReaderWriterGateLock避免锁(http://msdn.microsoft.com/en-us/magazine/cc163532.aspx)
        //但它可以引入显著延迟重负载下(由于线程池的性质)
        //我建议实施或重用合适的无锁算法。
        //这将是沉重的并发性最好的解决方案(如果您有安排每秒足够大的作业计数)
        //否则锁定或者ReaderWriterLockSlim是够便宜
        锁定(_syncRoot)
        {
            //我们将转向开始时间快速检查时,它的过去我们_curTime
            VAR shiftedTime = job.FireTime.Ticks  -  _startTime;

            名单&LT;工作&GT;就业机会;
            如果(!_jobs.TryGetValue(shiftedTime,出来工作))
            {
                工作=新的名单,其中,招聘&GT; {工作};
                _jobs.Add(shiftedTime,作业);
            }
            其他jobs.Add(工作);


            如果(Interlocked.CompareExchange(参考_queueRun,1,0)== 0)
            {
                //队列不运行,计划启动
                Interlocked.CompareExchange(参考_jobsRunQueuedInThreadPool,1,0);
                ThreadPool.QueueUserWorkItem(RunJobs);
            }
            其他
            {
                //别的队列已经启动并运行,但也许我们需要以调整开始时间
                //见RunJobs详细评论

                长firetime = _jobs.Keys [0];
                长三角洲= firetime  -  _curTime.Elapsed.Ticks;

                如果(增量&LT; MinIncrement)
                {
                    如果(Interlocked.CompareExchange(参考_jobsRunQueuedInThreadPool,1,0)== 0)
                    {
                        _timer.Change(Timeout.Infinite,Timeout.Infinite);
                        ThreadPool.QueueUserWorkItem(RunJobs);
                    }
                }
                其他
                {
                    Console.WriteLine(DEBUG:唤醒时间改为下一个事件{0},TimeSpan.FromTicks(增量));
                    _timer.Change(增量/ TimeSpan.TicksPerMillisecond,Timeout.Infinite);
                }
            }

        }
    }


    //招聘亚军
    私人无效RunJobs(对象状态)
    {
        // 警告!在这里,我阻止列表中,直到整个过程完成后,
        //也许更会用ReadWriterLockSlim或有点(如无锁)
        //因为通常是这取决于......

        //这里的处理是非常快(几操作只),所以,直到你有安排每秒不要紧许多就业机会
        锁定(_syncRoot)
        {
            //我们准备在需要时重新运行RunJobs
            Interlocked.CompareExchange(参考_jobsRunQueuedInThreadPool,0,1);

            INT availWorkerThreads;
            INT availCompletionPortThreads;

            //当前线程统计
            ThreadPool.GetAvailableThreads(出availWorkerThreads,出availCompletionPortThreads);

            //你可以检查最大线程限制
            // ThreadPool.GetMaxThreads(出maxWorkerThreads,出maxCompletionPortThreads);

            INT jobsAdded = 0;

            而(jobsAdded&其中; MaxJobsToSchedulePerCheck&安培;&安培; availWorkerThreads&GT; MaxJobsToSchedulePerCheck +1安培;&安培; _jobs.Count大于0)
            {
                //排序列表&LT;&GT;实现为两个数组键和值,以便在索引键/值将快速
                //第一个元素
                名单&LT;工作&GT; curJobs = _jobs.Values​​ [0];
                长firetime = _jobs.Keys [0];

                // 警告!秒表滴答来自DateTime.Ticks不同
                //所以我们使用_curTime.Elapsed.Ticks代替_curTime.ElapsedTicks

                //在DateTime.Ticks商品转口货值presents一个100纳秒的时间间隔每一个滴答声。
                //在ElapsedTicks值重新$ P $每个蜱psents的时间间隔等于1秒的频率划分。
                如果(_curTime.Elapsed.Ticks&LT; = firetime)打破;

                而(curJobs.Count大于0&安培;&安培; jobsAdded&其中; MaxJobsToSchedulePerCheck&安培;&安培; availWorkerThreads&GT; MaxJobsToSchedulePerCheck + 1)的
                {
                    变种作业= curJobs [0];

                    //时间过去了,我们准备开始工作
                    如果(job.DoAction!= NULL)
                    {
                        //计划新运行

                        //我强烈建议寻找新的.NET 4的任务类,因为它提供出色的解决方案,用于管理任务
                        // 例如。取消运行,异常处理,继续等
                        ThreadPool.QueueUserWorkItem(job.DoAction,工作);
                        ++ jobsAdded;

                        //它可能看起来我们可以只减1 availWorkerThreads
                        //但不要忘了开始工作,他们还可以消耗线程池的线程
                        ThreadPool.GetAvailableThreads(出availWorkerThreads,出availCompletionPortThreads);
                    }

                    //从并行作业的列表中删除工作
                    curJobs.Remove(工作);
                }

                如果空//删除整个列表
                如果(curJobs.Count&小于1)_jobs.RemoveAt(0);
            }

            如果(_jobs.Count大于0)
            {
                长firetime = _jobs.Keys [0];

                //时间到下一个事件
                长三角洲= firetime  -  _curTime.Elapsed.Ticks;

                如果(增量&LT; MinIncrement)
                {
                    通过线程池//调度下一个队列检查(立即)
                    //它可能看起来我们开始消耗所有resouces当我们运行的可用线程(由于无限reschdule)
                    //因为我们通过通过我们的while循环,只是重新安排RunJobs
                    //但是这是不对的,因为RunJobs将再次启动之前
                    //所有其​​他线程会提前一点,甚至完成任务
                    //所以它的安全只是重新安排RunJobs,因此,当我们得到一些资源等待
                    如果(Interlocked.CompareExchange(参考_jobsRunQueuedInThreadPool,1,0)== 0)
                    {
                        _timer.Change(Timeout.Infinite,Timeout.Infinite);
                        ThreadPool.QueueUserWorkItem(RunJobs);
                    }
                }
                否则//计划通过定时器的回调下一个检查
                {
                    Console.WriteLine(DEBUG:下一个事件{0},TimeSpan.FromTicks(增量)); //只是一些调试输出
                    _timer.Change(增量/ TimeSpan.TicksPerMillisecond,Timeout.Infinite);
                }
            }
            否则//关闭队列,没有更多的就业机会
            {
                Console.WriteLine(DEBUG:队列结束);
                Interlocked.CompareExchange(参考_queueRun,0,1);
            }
        }
    }
}
 


用法简单的例子:

  //测试在职职工
    静态无效SomeJob(对象参数)
    {
        VAR工作=参数的工作;
        如果(作业== NULL)回报;

        Console.WriteLine(工作开始:{0},[预定:{1},参数:{2}],DateTime.Now.ToString(O),
                          job.FireTime.ToLocalTime()的ToString(O),job.Param)。
    }

    静态无效的主要(字串[] args)
    {
        VAR CURTIME = DateTime.UtcNow;
        Console.WriteLine(当前时间:{0},curTime.ToLocalTime()的ToString(O));
        Console.WriteLine();

        VAR调度=新调度();

        //计划10秒未来
        dispatcher.ScheduleJob(Job.At(CURTIME + TimeSpan.FromSeconds(10),SomeJob,10秒:1));
        dispatcher.ScheduleJob(Job.At(CURTIME + TimeSpan.FromSeconds(10),SomeJob,10秒:2));

        //几乎立即开始
        dispatcher.ScheduleJob(Job.At(CURTIME  -  TimeSpan.FromMinutes(1),SomeJob,过去));

        //而在去年的工作进行测试
        dispatcher.ScheduleJob(Job.At(CURTIME + TimeSpan.FromSeconds(25),SomeJob,25秒));

        Console.WriteLine(队列长度:{0},{1},dispatcher.Length,dispatcher.IsWorking()工作:完成);
        Console.WriteLine();

        Console.WriteLine(作业)的foreach(在dispatcher.GetJobs()VAR的工作);
        Console.WriteLine();

        到Console.ReadLine();

        Console.WriteLine(?dispatcher.IsWorking()调度员仍然在工作:队列中没有更多的就业机会);

        Console.WriteLine();
        Console.WriteLine(作业)的foreach(在dispatcher.GetJobs()VAR的工作);

        到Console.ReadLine();
    }
 


希望这会有所帮助。


@Steven Sudit点我一些问题,所以在这里我试图给我的视野。

  

1)我不建议使用排序列表在这里,或任何其他地方,因为它是一个过时的.NET 1.1类

排序列表&LT;> 通过任何手段没有过时。它仍然存在于.NET 4.0和引进 .NET 2.0 时,仿制药被引入到语言。我看不到任何一点从.NET中删除。

但这里真正的问题,我要回答:什么的数据结构,可以在排序为了储存的值,将是高效的存储索引的。有两种适合准备使用的数据结构: SortedDictionary&LT;> 和< A HREF =htt​​p://msdn.microsoft.com/en-us/library/ms132319.aspx相对=nofollow>排序列表&LT;> 。 这里有关如何选择一些信息。我只是不想浪费实现我自己的code和隐藏的主要算法。在这里,我可以实现优先级阵列或其他的东西,但它需要更多的线,code。我看不出有任何理由不使用排序列表&LT;>在这里...

顺便说一句,我无法理解的为什么的你不推荐它? 什么的是什么原因?

  

2)一般情况下,有没有必要与同时发生的事件特殊情况复杂code。

在@Jrud说,他可能会有很多的任务来安排我觉得他们可能有大并发,所以我将演示如何解决它。但我的观点:即使你有低并发,你还是老样子有机会获得在同一时间的事件。这也很容易可以在多线程环境现状或当有许多来源要调度作业。

互锁功能没有那么复杂,价格便宜,因为.NET 4.0内联,所以没有问题补充后卫在这样的情况下。

  

3)IsWorking方法应该只使用一个内存屏障,然后直接读取值。

我不是那么肯定在这里,你是对的。我会推荐阅读两个漂亮的文章: 4部分:线程在C#高级线程通过约瑟夫阿尔巴哈利和如何操作锁锁?杰夫·莫泽的。 CLR的通过C#(第3版)原因第28章(原始线程同步构造)由杰弗里里希特和。

下面一些qoute:

  

该MemoryBarrier方法不能访问内存,但它迫使任何早期programorder   加载和存储调用MemoryBarrier前完成。而且它也   强制将所有后面的程序顺序加载和存储调用后完成   MemoryBarrier。 MemoryBarrier比其他两种方法少得多有用

     

重要的,我知道,这可能是非常混乱,所以让我总结它作为一个简单的规则:   当线程被彼此通过共享存储器进行通信,写由最后值   调用VolatileWrite并通过调用VolatileRead读取第一个值。

我也将推荐:英特尔®64和IA-32架构软件开发人员手册。

因此​​,我并不在我的code既不volatile关键字使用VolatileRead / VolatileWrite,我不认为Thread.MemoryBarrier会更好这里。也许你可以指向我什么,我错过?有些文章或深入讨论?

  

4)GetJobs方法看起来可能锁定较长时间。有必要吗?

它的一切都只是方便的方法

首先,有时就必须得到队列中的所有任务,至少进行调试。

不过,你是不是对的。正如我在code提到的评论排序列表&LT;>实现为两个数组可以通过参考源,或只是通过查看反射检查。从参考源在这里的一些意见:

  //一个排序列表内部维护的存储密钥和两个数组
//值条目。
 

我从.NET 4.0了,但它不是因为2-3.5太大的变化

所以,我的code:

  _jobs.Values​​.SelectMany(名单=&GT;清单).ToList()AsReadOnly()。
 

包括以下内容:

  • 在迭代通在引用列表的数组值。索引阵列是非常快的。
  • 在迭代直通每个列表(在内部实现为数组太)。它非常快了。
  • 在建引用的新名单(通过了ToList()),这是非常快的太(只是动态数组)(.NET具有非常扎实和快速实现)
  • 建立只读包装(没有复制,只是迭代器封装)

所以因此我们刚刚拉平引用招聘的对象为只读列表。它非常快,即使你有百万的任务。试着衡量自己。

无论如何,我把它添加到显示在执行周期内发生了什么(用于调试),但我认为它可能是有用的。

  

5)无锁队列可以在.NET 4.0中。

我会推荐阅读<一个href="http://www.microsoft.com/downloads/en/details.aspx?FamilyID=86b3d32b-ad26-4bb8-a3ae-c1637026c3ee&displaylang=en"相对=nofollow>并行编程通过斯蒂芬Toub和模式<一href="http://services.social.microsoft.com/feeds/FeedItem?feedId=639a99a9-ff25-4062-b61d-a86ea9d66a06&itemId=404a24fa-4820-4895-89ef-58d6e8f08d12&title=Thread-safe+Collections+in+.NET+Framework+4+and+Their+Performance+Characteristics&uri=http%3a%2f%2fdownload.microsoft.com%2fdownload%2fB%2fC%2fF%2fBCFD4868-1354-45E3-B71B-B851CD78733D%2fPerformanceCharacteristicsOfThreadSafeCollection.pdf&k=UlnHGXI3sO5fcC8zyrBezt9FtcGk8fmN6IYqNX2gQFc%3d"相对=nofollow>线程安全的集合在.NET框架4及其性能特点的,也的此处许多有趣的文章。

所以,我报价

  

ConcurrentQueue(T)是.NET Framework 4中的数据结构,提供对FIFO线程安全存取(先入先出)有序元素。在内部,ConcurrentQueue(T)被使用小的阵列和在头部和尾部阵列无锁操作的列表来实现,因此它比队列(T),它是由数组支持,并依赖于外部使用完全不同的的监控提供同步。 ConcurrentQueue(T)肯定是更安全方便比队列(T)的手动锁定,但有些实验需要确定两个方案的相对表现。在本节的剩余部分,​​我们将把手动锁定队列(T)的呼吁SynchronizedQueue(T)自包含的类型。

它没有任何方法来维持的订购队列。无论任何新的线程安全的集合,它们都保持无序集合。但是,读出原始@Jrud描述我认为我们必须保持时间顺序列表,当任务需要被解雇。难道我错了吗?

  

6)我不会理会启动和停止调度;就让它睡觉,直到下一个作业

你知道好方法,使睡眠线程池的线程?您将如何实现它?

我觉得调度进入休眠的时候,他不会处理任何任务和计划任务唤醒它。反正没有特殊的处理,把它睡觉或醒来,所以在我的思想这一过程相当于休眠。

如果你告诉我,我应该只是通过线程池重新安排RunJobs时,当你没有可用的作业错了,它会吃了太多的资源,会影响启动的作业。尝试自己。为什么做无谓工作的时候,我们可以很容易地避免它。

  

7),而不必担心各种蜱,你可以只坚持到毫秒。

您是不正确的。要么你坚持蜱,或你不关心它完全。检查日期时间执行,每次访问毫秒财产涉及转换内部重新presentaion(在蜱),以毫秒,包括分工。这可能会伤害老(奔腾级)compulters性能(我衡量自己,你也可以做到)。

在一般我会同意你的看法。我们不在乎再presentation这里,因为它并没有给我们带来明显的性能提升。

这只是我的习惯中。我在处理最近的项目因此codeD据此将其数十亿日期时间。在我的项目有蜱和日期时间的其他组件处理之间明显的差异。

  

8),以保持可用的线程轨道的尝试似乎并不可能是有效的

我只是想证明你有去关心它。在现实世界中,你必须从调度和监控资源,我straightful逻辑实现为止。

我想演示计时器轮算法,并指向一些的问题,笔者不得不考虑何时实施。

您是绝对正确的我不得不提醒一下吧。我想:赶紧ptototype就足够了。我的任何手段溶液不能在生产中使用。

I am planning to develop a system with tens of thousands of objects in it, which will each have up to 42(but more likely down around 4 or 5) separate actions they will potentially be performing at regular intervals. I also plan to write code that will deactivate the timers until the object comes into use. When idle, the objects will only need 1 timer each, but when active, the other timers will all start at once. At first the number of objects will be small, maybe a few hundred, but I expect it to grow exponentially, and within a few months, start to reach up in the tens of thousands.

So, I am very worried about efficiency of the code I will be writing for the timers and for these objects. There are three levels in which I could write this application on that would all successfully perform the tasks required. Also, I plan to run this system on a Quad Core server, so I would like to make use of multi-threading wherever possible.

To this end, I've decided to use the System.Timers.Timer class which fires a new thread for each elapse event.

These are the 3 levels I am considering:

  1. One single timer operates the entire application, it iterates through each object, checks to see if any other actions need to be fired, and if so, runs them, then moves on to the next.

  2. Multi-tier timer where each object has a master timer that checks all of the functions the object could need to perform, runs any that are ready, and then sets the next timer interval to the next required action time.

  3. Recursive-tier timer where each action in each object has it's own timer that will be triggered, and then set to run the next time it will be available.

The problem with option 1 is that with so many objects and actions, one singular timer elapse in this manner could run for maybe 20+ seconds (while it executed a few million lines of looped code), where this should probably be ticking every 1 second. If the objects aren't kept in synch, the system would likely not work well.

The problem with option 2 is that it would be a little harder to write than option 3, but not by much, it would also mean perhaps 10,000+ maybe timers running on the system (one for each object), creating and destroying threads with each elapse like its nobody's business (which I'm not sure if this is a problem or not). Each timer would have to fire at least once per second in this situation, with perhaps a few hundred lines of code running (up to perhaps a thousand in an extreme case).

The problem with option 3 is the sheer amount of timers that could potentially be introduced into the system. I'm talking about an average of 10,000+ timers with the potential for near 100,000+ timers to be run at the same time. Each elapse event may only have to run 50 or less lines of code though, making them very short. The elapse events would have delays between a hundredth of a second on one extreme, and five minutes on the other, with the average likely being around 1 second.

I am proficient in Visual Basic .NET, and was planning to write it in that, but I could also revert to my high-school days and try to write this in C++ for efficiency if it would make that much of a difference (please let me know if you have any sources on code efficiency between languages). Also toying with the notion of running this on a clustered Linux server instead of my Quad Core Windows server, but I'm not sure if I could get any of my .NET apps to run on a linux cluster like that (would love any info on that as well).

The main question to answer for this topic is:

Do I use option 1, 2, or 3, and why?

~Edit after considering comments~

So the 4th option involving the timer wheel with a spinlock. Here is a job class:

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

And then the main loop implementation:

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

Do I have it right?

EpicClanWars.com

~Edit 2~

Switched the word "Task" out for "Job" so ppl could stop complaining about it ;)

~Edit 3~

Added variables for tracking time & ensuring spinloops happen when needed

解决方案

EDIT: I remember interesting interview definetely worth to view: Arun Kishan: Inside Windows 7 - Farewell to the Windows Kernel Dispatcher Lock

As @Steven Sudit stated I warn again: use it only as demo on how timer wheel works and some tasks you have to care about while implement it. Not as reference implementation. In real world you have to write far more complex logic to take into account available resources, scheduling logic and etc.


Here good points stated by Steven Sudit (read post comments for details):

1) Choose right structure to keep your jobs list (it depends as usually):

  • SortedList<> (or SortedDictionary<>) good on memory consumption and indexing but have to implement synchronized access

  • ConcurrentQueue<> will help you avoid locking but you have to implement ordering. It also very memory efficient

  • LinkedList<> is good on insert and retrieve (anyway we need head only) but requires synchronized access (thru it easily implemented via lock-free) and not so memory efficient as it stores two references (prev/next). But it become an issue when you have millions of jobs so all of them take significant amount of memory.

But:

I totally agree with @Steven:

It doesn't matter: neither one of these is a good fit. The right answer would be to use a regular queue and maintain its order yourself, since we most often need to access it only from the head or tail.

Generally, I would recommend using the most feature-full collection from the library, but that doesn't apply here because this is system-level code. We'd need to roll our own, either from scratch or on top of a less feature-rich collection

2) To simplify processing logic of simultaneous jobs you can add delegate list (e.g. via ConcurrentQueue to make it lock-free) into original Job class so when you need another job at same time you just add another delegate to start.

@Steven:

If two tasks are actually scheduled for the same time (whether actually or effectively), this is a normal case that does not require complicating our data structure. In other words, we don't need to group up simultaneous jobs so that we have to traverse two different collections; we can just make them adjacent

3) Start/stoping dispatcher not so straightful as it can be and so can lead to errors. Instead you can wait on an event while using a timeout.

@Steven:

This way, it will either wake up when the next job is ready or when a new job is inserted before the head. In the latter case, it could need to run it now or set a different wait. If presented with, say, 100 jobs all scheduled for the same instant, the best thing we can do is queue them all up.

If we needed to provide prioritization, that's a job for a priority dispatch queue and multiple pools in a producer/consumer relationship, but it still doesn't justify a start/stop dispatcher. The dispatcher should always be on, running in a single loop that sometimes cedes the core

4) About using ticks:

@Steven:

Sticking to one type of tick is fine, but mixing and matching gets ugly, particularly since it's hardware-dependent. I'm sure that ticks would be slightly faster than milliseconds, because it stores the former and has to divide by a constant to get the latter. Whether this operation winds up being costly is another matter, but I'm fine with using ticks to avoid the risk.

My thoughts:

Another good point, I agree with you. But sometimes division by constant becomes costly and it not so fast as it maybe seems to be. But when we talk about 100 000 of DateTimes it does not matter, you are right, thank you to point.

5) "Managing resources":

@Steven:

The problem I'm trying to highlight is that the call to GetAvailableThreads is expensive and naive; the answer is obsolete before you can even use it. If we really wanted to keep track, we could get initial values and keep a running count by calling the job from a wrapper that uses Interlocked.Increment/Decrement. Even then, it presumes that the rest of the program is not using the thread pool. If we really wanted fine control, then the right answer here is to roll our own thread pool

I absolutely agree that calling to GetAvailableThreads is naive method to monitor available resources thru CorGetAvailableThreads not so expensive. I want to demontrate there are needs to manage resources and seems to choose bad example.

By any means provided in source code example is must not be treated as right way to monitor available resources. I just want to demonstrate you have to think about it. Thru maybe coded no so good piece of code as example.

6) Using Interlocked.CompareExchange:

@Steven:

No, it's not a common pattern. The most common pattern is to briefly lock. Less common is to flag the variable as volatile. Much less common would be to use VolatileRead or MemoryBarrier. Using Interlocked.CompareExchange this way is obscure, even if Richter does it. using it without an explanatory comment is absolutely guaranteed to confuse, as the word "Compare" implies that we're doing a comparison, when in fact we're not.

You are right I have to point about its usage.


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


Quick example of usage:

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


Hope it will be helpful.


@Steven Sudit point me some issues, so here I try to give my vision.

1) I wouldn't recommend using SortedList here, or anywhere else, as it's an obsolete .NET 1.1 class

SortedList<> by any means is not obsolete. It still exists in .NET 4.0 and introduced in .NET 2.0 when generics was introduced into language. I can't see any point to remove it from .NET.

But real question here I trying to answer: What data structure can store values in sorted order and will be efficient in storing and indexing. There are two suitable ready to use data structures: SortedDictionary<> and SortedList<>. Here some info about how to choose. I just don't want waste implementation with my own code and hide main algorithm. Here I can implement priority-array or something other but it takes more lines to code. I don't see any reason do not use SortedList<> here...

BTW, I can't understand why you not recommend it? What are reasons?

2) In general, there's no need to complicate the code with special cases for simultaneous events.

When @Jrud says he probably will have numerous task to schedule I think it they may have heavy concurrency, so I demonstrate how to solve it. But my point: even if you have low concurrency you stil have chance to get events in same time. Also this is easy possible in multithreaded evironment or when there are many sources want to schedule jobs.

Interlocked functions not so complicated, cheap and since .NET 4.0 inlined so there are no problem to add guard in such situation.

3) The IsWorking method should just use a memory barrier and then read the value directly.

Im not so sure here that you are right. I would recommend to read two nice articles: Part 4: Advanced Threading of Threading in C# by Joseph Albahari and How Do Locks Lock? by Jeff Moser. And of cause Chapter 28 (Primitive Thread Synchronization Constructs) of CLR via C# (3rd edition) by Jeffrey Richter.

Here some qoute:

The MemoryBarrier method doesn’t access memory but it forces any earlier programorder loads and stores to be completed before the call to MemoryBarrier. And it also forces any later program-order loads and stores to be completed after the call to MemoryBarrier. MemoryBarrier is much less useful than the other two methods

Important I know that this can be very confusing, so let me summarize it as a simple rule: When threads are communicating with each other via shared memory, write the last value by calling VolatileWrite and read the first value by calling VolatileRead.

I would also recommend: Intel® 64 and IA-32 Architectures Software Developer's Manuals if you care about it seriously.

So I don't use VolatileRead/VolatileWrite in my code neither volatile keyword, I don't think Thread.MemoryBarrier will be better here. Maybe you can point me what I miss? Some articles or in-depth discussion?

4) The GetJobs method looks like it could lock for an extended period. Is it necessary?

First of all its just handy method, sometime it is necessary to get all tasks in queue at least for debugging.

But you are not right. As I mentioned in code comments SortedList<> implemented as two arrays you can check this by Reference Source or just by viewing in Reflector. Here some comments from reference source:

// A sorted list internally maintains two arrays that store the keys and
// values of the entries.  

I got from .NET 4.0 but it not changed much since 2-3.5

So my code:

_jobs.Values.SelectMany(list => list).ToList().AsReadOnly();

involve following:

  • iterate thru values in array of references to List. Indexing array is very fast.
  • iterate thru each List (which is implemented internally as array too). It very fast too.
  • build new List of references (via ToList()) which is very fast too (just dynamic array) (.NET has very solid and fast implementation)
  • build read-only wrapper (no copy, just iterator wrapper)

so consequently we have just flatten read-only list of references to Job's objects. It very fast even you have millions of task. Try to measure yourself.

Any way I added it to show what happens during execution cycle (for debug purposes) but I think it can be useful.

5) A lock-free queue is available in .NET 4.0.

I would recommend to read Patterns of parallel programming by Stephen Toub and Thread-safe Collections in .NET Framework 4 and Their Performance Characteristics, also here many interesting articles.

So I quote:

ConcurrentQueue(T) is a data structure in .NET Framework 4 that provides thread-safe access to FIFO (First-In First-Out) ordered elements. Under the hood, ConcurrentQueue(T) is implemented using a list of small arrays and lock-free operations on the head and tail arrays, hence it is quite different than Queue(T) which is backed by an array and relies on the external use of monitors to provide synchronization. ConcurrentQueue(T) is certainly more safe and convenient than manual locking of a Queue(T) but some experimentation is required to determine the relative performance of the two schemes. In the remainder of this section, we will refer to a manually locked Queue(T) as a self-contained type called SynchronizedQueue(T).

It don't have any methods to maintain ordered queue. Neither any of new thread-safe collection, they all maintain unordered collection. But reading original @Jrud description I think we have to maintain ordered list of time when task need to be fired. Am I wrong?

6) I wouldn't bother starting and stopping the dispatcher; just let it sleep until the next job

Do you know good way to make sleep ThreadPool's thread? How you will implement it?

I think dispatcher goes "sleep" when he does not process any task and schedule job wake-up it. Anyway there are no special processing to put it sleep or wake up so in my thoughts this process equals "sleep".

If you told that I should just reschedule RunJobs via ThreadPool when no jobs available when you are wrong it will eat too many resources and can impact started jobs. Try yourself. Why to do unnecessary job when we can easily avoid it.

7) Rather than worrying about different kinds of ticks, you could just stick to milliseconds.

You are not right. Either you stick to ticks or you don't care about it entirely. Check DateTime implementation, each access to milliseconds property involve converting internal representaion (in ticks) to ms including division. This can hurt performance on old (Pentium class) compulters (I measure it myself and you can too).

In general I will agree with you. We don't care about representation here because it does not give us noticeable performance boost.

It just my habbit. I process billions of DateTime in recent project so coded accordingly to it. In my project there are noticeable difference between processing by ticks and by other components of DateTime.

8) The attempt to keep track of available threads does not seem likely to be effective

I just want to demonstrate you have to care about it. In real world you have to implement far from my straightful logic of scheduling and monitoring resources.

I want to demonstrate timer wheel algorithm and point to some problem that author have to think when implement it.

You are absolutely right I have to warn about it. I thought "quickly ptototype" would be enough. My solution in any means can't be used in production.

这篇关于计时器效率的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆