• 同步


    要避免同步的问题,最好不要在线程之间共享数据。当然,这并不说是可行的。如果需要共享数据,就必须使用同步技术,确保一次只有一个线程访问和改变共享状态。注意同步问题与争用条件和死锁有关。如果不注意这些问题,就很难在应用程序中找到问题的原因,因为线程问题是不定期发生的。
    1.1、lock和线程安全
    C#为多个线程的同步提供了自己的关键字:lock语句。lock语句是设置锁定和解除锁定的一种简单方式。在添加lock语句之前,先进入另一个争用条件。SharedState类说明了如何使用线程之间的共享状态,并共享一个整数值。

       public class SharedState
        {
            public int state { get; set; }
        }
    

    Job类包含DoJob()方法,该方法是新任务的入口点。通过其实现代码,将SharedState变量的State递增5000次。sharedState变量在这个类的构造函数中初始化。

      public class job
        {
            SharedState sharedState;
    
            public job(SharedState sharedState)
            {
                this.sharedState = sharedState;
            }
            public void doJob()
            {
                for (int i = 0; i < 5000; i++)
                {
                    sharedState.state++;
                }
            }
        }
    

    在Main()方法中,创建一个SharedState对象,并把它传递给20个Task对象的构造函数。在启动的所有任务后,Main()方法进入另一个循环,等待20个任务都执行完毕。任务执行完毕后,把共享状态的合计值写入控制台中。因为执行了5000次循环,有20个任务,所以写入控制台的值应该是100000.但是,事实常常并非如此。

            int numTask = 20;
            var state = new SharedState();
            var task =  new Task[numTask];
    
            for (int i = 0; i < numTask; i++)
            {
                task[i] = Task.Run(()=>new job(state).doJob());
            }
            for (int i = 0; i < numTask; i++)
            {
                task[i].Wait();
            }
            Console.WriteLine( "summarized  {0}",state.state);
    

    可以得到的是每次运行的结果都不同,但没有一个结果是正确的。如前所述,调式版本和发布版本的区别很大。根据使用的CPU类型,其结果也不一样。必须在这个程序中添加同步功能,这可以用lock关键字实现。用lock语句定义的对象表示,要等待指定对象的锁定。只能传递引用类型,锁定值类型只是锁定了一个副本,这没有什么意义。如果对值类型使用了lock语句,C#编译器就会发出一个错误。进行了锁定后---只锁定了一个线程,就可以运行lock语句块。在lock语句块的最后,对象的锁定被解除,另一个等待锁定的线程就可以获得该锁定块了。

          lock(obj)
              {
                //sunchronized region
               }
    

    要锁定静态成员,可以把锁放在object类型上:

       lock(object)
             {
    
              }
    

    使用lock关键字可以将类的实例成员设置为线程安全的。这样,一次只有一个线程能访问相同实例的DoThis()和DoThat()方法。

        public class Demo
        {
            public void DoThis()
            {
                lock(this)
                {
                    //only one thread at a time can access the DoThis and DoThat method
                }
            }
            public void DoThat()
            {
                lock (this)
                {
    
                }
            }
        }
    

    但是,因为实例的对象也可以用于外部的同步访问,而且我们不能在类自身中控制这种访问,所以应采用SyncRoot模式。通过SyncRoot模式,创建一个私有对象,将这个对象用于lock语句

       public class Demo
        {
            private object syncRoot = new object();
            public void DoThis()
            {
                lock(syncRoot)
                {
                    //only one thread at a time can access the DoThis and DoThat method
                }
            }
            public void DoThat()
            {
                lock (syncRoot)
                {
    
                }
            }
        }
    

    使用锁定需要时间,且并不总是必须的。前面的实例中将dojob方法里面的适当位置加lock,这样得到的结果总是对的。

      public class job
        {
            SharedState sharedState;
    
            public job(SharedState sharedState)
            {
                this.sharedState = sharedState;
            }
            public void doJob()
            {
                for (int i = 0; i < 5000; i++)
                {
                    lock(sharedState)
                    sharedState.state++;
                }
            }
        }
    


    1.2、Interlocked类
    Interlocked类用于使变量的简单语句原子化,I++不是线程安全的,他的操作包括从内存中获取一个值,给该值递增1,再将它存储到内存中。这些操作可能被线程调度器打断。Interlocked类提供了以线程安全的方式递增、递减、交换和读取值的方法。
    与其同步技术相比,使用Interlocked类会快的多。但是,它只能用于简单的同步问题。 Interlocked.Increment();
    1.3、Monitor类
    lock语句由C#编译器解析为使用Monitor类。lock语句被解析为调用Enter()方法,该方法会一直等待,直到线程锁定对象为止。一次只有一个线程锁定对象,只要解除了锁定,线程就可以进入同步阶段。Monitor类的Exit()方法解除了锁定。编译器把Exit()方法放在try块的finally处理程序中,所以如果抛出了异常,就也会解除该锁定

         Monitor.Enter(obj);
            try
            {
    
            }
                finally
            {
                Monitor.Exit(obj);
            }
    

    与C#的lock语句相比,Monitor类的主要优点是:可以添加一个等待被锁定的超时值。这样就不会无限期的等待锁定,而可以像下面的例子那样使用TryEnter()方法,其中给它传递一个超时值,指定等待被锁定的最长时间。如果obj被锁定,TryEnter()方法就把布尔型的引用参数设置为true,并同步地访问由对象obj锁定的状态。如果另一个线程锁定obj的时间超过了500毫秒,TryEnter()方法就把变量lockTaken设置为false,线程不再等待,而是用于执行其他操作。也许在以后,该线程会尝试再次获得锁定。

           bool lockTaken = false;
            Monitor.TryEnter(o,500,ref lockTaken);
            if (lockTaken)
            {
                try
                {
                        //acquried the lock
                }
                finally
                {
                    Monitor.Exit(o);
                }
            }
            else
            {
                  //did not get the lock,do something else
            }
    

    1.4、SpinLock结构
    如果基于对象的锁定对象(Monitor)的系统开销由于垃圾过高,就可以使用SpinLock结构。SpinLock结构是在.NET4开始引入的。如果有大量的锁定,且锁定的时间总是非常短,SpinLock结构就很有用。应避免使用多个SpinLock结构,也不要调用任何可能阻塞的内容。除了体系结构上的区别之外,SpinLock结构的用法非常类似于Monitor类。获得锁定使用Enter()或TryEnter()方法,释放锁定使用Exit()方法。SpinLock结构还提供了属性IsHeldByCurrentThread,指定它当前是否是锁定的。

  • 相关阅读:
    (第十二周)Bug修正报告
    (第十二周)团队项目19
    (第十二周)新功能WBS
    (第十二周)团队项目18
    (第十二周)团队项目17
    (第十二周)Debug阶段成员贡献分
    (第十一周)工作总结
    学习进度
    第九周
    第八周
  • 原文地址:https://www.cnblogs.com/caozhengze/p/10092463.html
Copyright © 2020-2023  润新知