美文网首页通往成功之路
[C# 线程处理系列]专题四:线程同步

[C# 线程处理系列]专题四:线程同步

作者: 此十八 | 来源:发表于2018-09-14 09:46 被阅读9次

    目录:

    一、线程同步概述

    二、线程同步的使用

    三 、总结

    一、线程同步概述

    前面的文章都是讲创建多线程来实现让我们能够更好的响应应用程序,然而当我们创建了多个线程时,就存在多个线程同时访问一个共享的资源的情况,在这种情况下,就需要我们用到线程同步,线程同步可以防止数据(共享资源)的损坏。

    然而我们在设计应用程序还是要尽量避免使用线程同步, 因为线程同步会产生一些问题:

    1. 它的使用比较繁琐。因为我们要用额外的代码把多个线程同时访问的数据包围起来,并获取和释放一个线程同步锁,如果我们在一个代码块忘记获取锁,就有可能造成数据损坏。

    2. 使用线程同步会影响性能,获取和释放一个锁肯定是需要时间的吧,因为我们在决定哪个线程先获取锁时候, CPU必须进行协调,进行这些额外的工作就会对性能造成影响

    3. 因为线程同步一次只允许一个线程访问资源,这样就会阻塞线程,阻塞线程会造成更多的线程被创建,这样CPU就有可能要调度更多的线程,同样也对性能造成了影响。

    所以在实际的设计中还是要尽量避免使用线程同步,因此我们要避免使用一些共享数据,例如静态字段。

    二、线程同步的使用

    2.1 对于使用锁性能的影响

    上面已经说过使用锁将会对性能产生影响, 下面通过比较使用锁和不使用锁时消耗的时间来说明这点

    using System;
    using System.Diagnostics;
    using System.Threading;
    
    namespace InterlockedSample
    {
        // 比较使用锁和不使用锁锁消耗的时间
        // 通过时间来说明使用锁性能的影响
        class Program
        {
            static void Main(string[] args)
            {
                int x = 0;
                // 迭代次数为500万
                const int iterationNumber = 5000000; 
                // 不采用锁的情况
                // StartNew方法 对新的 Stopwatch 实例进行初始化,将运行时间属性设置为零,然后开始测量运行时间。
                Stopwatch sw = Stopwatch.StartNew();
                for (int i = 0; i < iterationNumber; i++)
                {
                    x++;
                }
    
                Console.WriteLine("Use the all time is :{0} ms", sw.ElapsedMilliseconds);
    
                sw.Restart();
                // 使用锁的情况
                for (int i = 0; i < iterationNumber; i++)
                {
                    Interlocked.Increment(ref x);
                }
    
                Console.WriteLine("Use the all time is :{0} ms", sw.ElapsedMilliseconds);
                Console.Read();
            }
        }
    }
    

    运行结果(这是在我电脑上运行的结果)从结果中可以看出加了锁的运行速度慢了好多(慢了11倍 197/18 ):

    image

    2.2 Interlocked实现线程同步

    Interlocked类提供了为多个线程共享的变量提供原子操作,当我们在多线程中对一个整数进行递增操作时,就需要实现线程同步。

    因为增加变量操作(++运算符)不是一个原子操作,需要执行下列步骤:

    1)将实例变量中的值加载到寄存器中。

    2)增加或减少该值。

    3)在实例变量中存储该值。

    如果不使用 Interlocked.Increment方法,线程可能会在执行完前两个步骤后被抢先。然后由另一个线程执行所有三个步骤,此时第一个线程还没有把变量的值存储到实例变量中去,而另一个线程就可以把实例变量加载到寄存器里面读取了(此时加载的值并没有改变),所以会导致出现的结果不是我们预期的,相信这样的解释可以帮助大家更好的理解Interlocked.Increment方法和 原子性操作,

    下面通过一段代码来演示下加锁和不加锁的区别(开始讲过加锁会对性能产生影响,这里将介绍加锁来解决线程同步的问题,得到我们预期的结果):

    不加锁的情况:

    class Program
        {
            static void Main(string[] args)
            {     for (int i = 0; i < 10; i++)
                {
                    Thread testthread = new Thread(Add);
                    testthread.Start();
                }
    
                Console.Read();
            }
    
            // 共享资源
            public static int number = 1;
    
            public static void Add()
            {
                Thread.Sleep(1000);
                Console.WriteLine("the current value of number is:{0}", ++number);
            }
    }
    

    运行结果(不同电脑上可能运行的结果和我的不一样,但是都是得到不是预期的结果的):

    image

    为了解决这样的问题,我们可以通过使用 Interlocked.Increment方法来实现原子的自增操作。

    代码很简单,只需要把++number改成Interlocked.Increment(ref number)就可以得到我们预期的结果了,在这里代码和运行结果就不贴了。

    总之Interlocked类中的方法都是执行一次原子读取以及写入的操作的。

    2.3 Monitor实现线程同步

    对于上面那个情况也可以通过Monitor.Enter和Monitor.Exit方法来实现线程同步。C#中通过lock关键字来提供简化的语法(lock可以理解为Monitor.Enter和Monitor.Exit方法的语法糖),代码也很简单:

    using System;
    using System.Threading;
    
    namespace MonitorSample
    {
        class Program
        {
            static void Main(string[] args)
            {
                for (int i = 0; i < 10; i++)
                {
                    Thread testthread = new Thread(Add);
                    testthread.Start();
                }
    
                Console.Read();
            }
    
            // 共享资源
            public static int number = 1;
    
            public static void Add()
            {
                Thread.Sleep(1000);
                //获得排他锁
                Monitor.Enter(number);
    
                Console.WriteLine("the current value of number is:{0}", number++);
    
                // 释放指定对象上的排他锁。
                Monitor.Exit(number);
            }
        }
    }
    

    运行结果当然是我们所期望的:

    image

    在 Monitor类中还有其他几个方法在这里也介绍,只是让大家引起注意下,一个Wait方法,很明显Wait方法的作用是:释放某个对象上的锁以便允许其他线程锁定和访问这个对象。第二个就是TryEnter方法,这个方法与Enter方法主要的区别在于是否阻塞当前线程,当一个对象通过Enter方法获取锁,而没有执行Exit方法释放锁,当另一个线程想通过Enter获得锁时,此时该线程将会阻塞,直到另一个线程释放锁为止,而TryEnter不会阻塞线程。具体代码就不不写出来了。

    2.4 ReaderWriterLock实现线程同步

    如果我们需要对一个共享资源执行多次读取时,然而用前面所讲的类实现的同步锁都只允许一个线程允许,所有线程将阻塞,但是这种情况下肯本没必要堵塞其他线程, 应该让它们并发的执行,因为我们此时只是进行读取操作,此时通过ReaderWriterLock类可以很好的实现读取并行。

    演示代码为:

    using System;
    using System.Collections.Generic;
    using System.Threading;
    
    namespace ReaderWriterLockSample
    {
        class Program
        {
            public static List<int> lists = new List<int>();
    
            // 创建一个对象
            public static ReaderWriterLock readerwritelock = new ReaderWriterLock();
            static void Main(string[] args)
            {
                //创建一个线程读取数据
                Thread t1 = new Thread(Write);
                t1.Start();
                // 创建10个线程读取数据
                for (int i = 0; i < 10; i++)
                {
                    Thread t = new Thread(Read);
                    t.Start();
                }
    
                Console.Read();
                    
            }
    
            // 写入方法
            public static void Write()
            {
                // 获取写入锁,以10毫秒为超时。
                readerwritelock.AcquireWriterLock(10);
                Random ran = new Random();
                int count = ran.Next(1, 10);
                lists.Add(count);
                Console.WriteLine("Write the data is:" + count);
                // 释放写入锁
                readerwritelock.ReleaseWriterLock();
            }
    
            // 读取方法
            public static void Read()
            {
                // 获取读取锁
                readerwritelock.AcquireReaderLock(10);
    
                foreach (int li in lists)
                {
                    // 输出读取的数据
                    Console.WriteLine(li);
                }
    
                // 释放读取锁
                readerwritelock.ReleaseReaderLock();
            }
        }
    }
    

    运行结果:

    image

    三、总结

    本文中主要介绍如何实现多线程同步的问题, 通过线程同步可以防止共享数据的损坏,但是由于获取锁的过程会有性能损失,所以在设计应用过程中尽量减少线程同步的使用。本来还要介绍互斥(Mutex), 信号量(Semaphore), 事件构造的, 由于篇幅的原因怕影响大家的阅读,所以这剩下的内容放在后面介绍的。

    相关文章

      网友评论

        本文标题:[C# 线程处理系列]专题四:线程同步

        本文链接:https://www.haomeiwen.com/subject/ukingftx.html