同步

要避免同步的问题,最好不要在线程之间共享数据。当然,这并不说是可行的。如果需要共享数据,就必须使用同步技术,确保一次只有一个线程访问和改变共享状态。注意同步问题与争用条件和死锁有关。如果不注意这些问题,就很难在应用程序中找到问题的原因,因为线程问题是不定期发生的。
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,指定它当前是否是锁定的。

posted @ 2018-12-09 18:40  泽哥的学习笔记  阅读(127)  评论(0编辑  收藏  举报