zoukankan      html  css  js  c++  java
  • 《CLR Via C# 第3版》笔记之(二十) 计时器及伪共享

    计时器在很多应用场景中广泛应用,System.Threading命名空间下,有个Timer类可以完成计时器的操作。

    下面来讨论下Timer类的使用及多个CPU内核同时运行线程时如何共享高速缓存(cache)的。

    主要内容:

    • 计时器的使用
    • CPU高速缓存的伪共享

    1. 计时器的使用

    1.1 计时器的创建及改变设置

    计时器创建的API都类似,有以下5种:

    public Timer(TimerCallback callback);
    public Timer(TimerCallback callback, object state, int dueTime, int period);
    public Timer(TimerCallback callback, object state, long dueTime, long period);
    public Timer(TimerCallback callback, object state, TimeSpan dueTime, TimeSpan period);
    public Timer(TimerCallback callback, object state, uint dueTime, uint period);

    其中各个参数的含义如下:

    1. callback:定时执行的操作
    2. state:传递给callback的参数
    3. dueTime:首次调用callback时,需要等待多少毫秒
    4. period:每隔多少毫秒执行一次callback

    下面例子演示如何创建一个计时器,并且在运行10秒后如何改变计时器的dueTimeperiod

    using System;
    using System.Threading;
    
    public class CLRviaCSharp_20
    {
        static void Main(string[] args)
        {
            // 定义一个定时器,每2秒执行一次
            Timer t = new Timer(PrintTime, "Timer Test", 0, 2000);
    
            // 主线程等待10秒(让计时器先执行6次)
            Thread.Sleep(10000);
    
            // 改变计时器的设置,停3秒后,每2秒执行一次
            t.Change(3000, 1000);
            Console.ReadKey(true);
        }
    
        private static void PrintTime(object state)
        {
            Thread.Sleep(3000);
            Console.WriteLine("{0}: Now is {1}", state, DateTime.Now);
        }
    }

    运行上面的例子,我们发现有以下现象:

    1. 刚开始每隔2秒打印一次当前时间
    2. 10秒后,每隔1秒打印一次当前时间
    3. callback(即PrintTime函数)中虽然有Sleep(3000);但计时器的下一次callback并没有等待本次callback完成就开始运行了。

    对于上述的第三点,是由于Timer在每个period到期时,如果上个callback没完成,会在线程池中再启动一个线程来完成本次的callback。

    1.2 耗时操作(操作消耗的时间 > 计时器的时间间隔)

    但是,对于某些应用场景,可能需要callback一个一个顺序执行。

    如果callback的耗时较长,希望下次callback能在本次callback完成后,再过period后执行。这时,就需要我们使用TimerChange方法实现。

    using System;
    using System.Threading;
    
    public class CLRviaCSharp_20
    {
        static Timer t;
    
        static void Main(string[] args)
        {
            // 定义一个计时器,
            // period参数设置为Timeout.Infinite的意思是计时器执行一次就结束
            t = new Timer(PrintTime, "Timer Test", 0, Timeout.Infinite);
            Console.ReadKey(true);
        }
    
        private static void PrintTime(object state)
        {
            Thread.Sleep(3000);
            Console.WriteLine("{0}: Now is {1}", state, DateTime.Now);
    
            // 因为计时器执行一次就结束,所以在callback(即PrintTime)执行完后再启动一个计时器
            // 此计时器仍然设置period参数设置为Timeout.Infinite
            // 并且设置dueTime参数为2000毫秒,从而保证下次callback在本次callback完成2秒后执行
            // 这样不管每个callback耗用多少时间,两个callback之间的间隔时间始终是2秒
            t = new Timer(PrintTime, "Timer Test", 2000, Timeout.Infinite);
        }
    }

    这样运行结果就是每隔5秒打印一次时间了。

    其中2秒表示callback之间的间隔。

    另外3秒表示callback的执行时间。

    2. CPU高速缓存的伪共享

    为了提高CPU反复访问内存的能力,在CPU芯片上集成了高速缓存,它的访问速度非常快。

    CPU从内存读取数据时,会把读取到的数据存在高速缓存中。下次直接从高速缓存中读取,从而提升性能。

    CPU每次读取数据时填充的缓存大小与本机的CPU有关,我的机器是64byte。

    可用CPU-Z来查看。

    image

    在多核的情况下,如何2个内核使用的数据在一个缓存线(即上面的64byte)中,那么反而会影响性能。

    因为内核A如果改变了数据后,内核B需要把内核A使用的缓存线(即上面的64byte)拷入到B的缓存线中。

    反之,内核A再次操作数据时,也要把内核B使用的缓存线(即上面的64byte)拷回来。

    但是,如果内核A和内核B使用的数据不在一个缓存线中,那么性能就会提高,因为少了上面的不同内核之间缓存线的拷贝操作。

    下面用例子来演示两种情况下的性能。

    using System;
    using System.Diagnostics;
    using System.Runtime.InteropServices;
    using System.Threading.Tasks;
    
    public class CLRviaCSharp_20
    {
        // 因为我的机器的缓存线为64byte,
        // 所以两个int型字段应该在同一个缓存线中
        private class ShareData1
        {
            public int field1;
            public int field2;
        }
    
        // 通过设置使得两个int型字段不在一个缓存线中
        [StructLayout(LayoutKind.Explicit)]
        private class ShareData2
        {
            [FieldOffset(0)]
            public int field1;
            [FieldOffset(64)]
            public int field2;
        }
    
        static void Main(string[] args)
        {
            int iterations = 100000000;
            ShareData1 data1 = new ShareData1();
    
            // 情况一:2个数据在一个缓存行中
            long start = Stopwatch.GetTimestamp();
            Task t1 = new Task(() => {
                for (int i = 0; i < iterations; i++)
                    data1.field1++;
            });
    
            Task t2 = new Task(() => {
                for (int i = 0; i < iterations; i++)
                    data1.field2++;
            });
    
            t1.Start();
            t2.Start();
    
            Task.WaitAll();
            Console.WriteLine("Totle time is : {0:N0}", (Stopwatch.GetTimestamp() - start));
    
            // 情况二:2个数据不在一个缓存行中
            ShareData2 data2 = new ShareData2();
            start = Stopwatch.GetTimestamp();
            Task t3 = new Task(() =>
            {
                for (int i = 0; i < iterations; i++)
                    data2.field1++;
            });
    
            Task t4 = new Task(() =>
            {
                for (int i = 0; i < iterations; i++)
                    data2.field2++;
            });
    
            t3.Start();
            t4.Start();
    
            Task.WaitAll();
            Console.WriteLine("Totle time is : {0:N0}", (Stopwatch.GetTimestamp() - start));
            Console.ReadKey(true);
        }
    }

    在我的双核机器上,明显第二种情况性能要优于第一种情况。

  • 相关阅读:
    js 获取浏览器版本号
    怎样写具体设计文档
    android PreferenceScreen使用笔记
    支持向量机通俗导论(理解SVM的三层境地)
    算法导论 第6章 堆排序(简单选择排序、堆排序)
    人脸识别算法初次了解
    循环队列
    ubuntu 下操作文件夹,出现Permission denied的解决的方法
    JFreeChart的使用
    隐藏Activity标题栏
  • 原文地址:https://www.cnblogs.com/wang_yb/p/2245712.html
Copyright © 2011-2022 走看看