• C#基础:线程同步


    一、前言

    我们先来看下面一个例子:

    using System;
    using System.Threading;
    
    namespace ThreadSynchDemo
    {
        class Program
        {
            private static int Counter = 0;
            static void Main(string[] args)
            {
                Thread t1 = new Thread(() => {
                    for (int i = 0; i < 1000; i++)
                    {
                        Counter++;
                        Thread.Sleep(1);
                    }
                });
                t1.Start();
    
                Thread t2 = new Thread(() => {
                    for (int i = 0; i < 1000; i++)
                    {
                        Counter++;
                        Thread.Sleep(1);
                    }
                });
                t2.Start();
    
                Thread.Sleep(3000);
                Console.WriteLine(Counter);
                Console.ReadKey();
            }
        }
    }

    我们猜想一下程序的输出结果是多少?2000?我们运行程序看一下输出结果:

    我们看到,程序最后输出的结果跟我们预测的完全不一样,这是什么原因呢?这就是由线程同步引起的问题。

    线程同步问题:是 解决多个线程同时操作一个资源的问题 。

    在上面的例子中,t1和t2两个线程里面都是让变量Counter的值自增1,假设这时t1线程读取到Counter的值为200,可能t2线程执行非常快,t1线程读取Counter值的时候,t2线程已经把Counter的值改为了205,等t1线程执行完毕以后,Counter的值又被变为了201,这样就会出现线程同步的问题了。那么该如何解决这个问题呢?

    二、解决线程同步问题

    1、lock

    解决线程同步问题最简单的是使用lock。lock可以解决多个线程同时操作一个资源引起的问题。lock是C#中的关键字,它要锁定一个资源,lock的特点是:同一时刻只能有一个线程进入lock的对象的范围,其它lock的线程都要等待。我们看下面优化后的代码:

    using System;
    using System.Threading;
    
    namespace ThreadSynchDemo
    {
        class Program
        {
            private static int Counter = 0;
            // 定义一个locker对象
            private static Object locker = new Object();
            static void Main(string[] args)
            {
                #region 存在线程同步问题
                //Thread t1 = new Thread(() => {
                //    for (int i = 0; i < 1000; i++)
                //    {
                //        Counter++;
                //        Thread.Sleep(1);
                //    }
                //});
                //t1.Start();
    
                //Thread t2 = new Thread(() => {
                //    for (int i = 0; i < 1000; i++)
                //    {
                //        Counter++;
                //        Thread.Sleep(1);
                //    }
                //});
                //t2.Start(); 
                #endregion
    
                #region 使用Lock解决线程同步问题
                Thread t1 = new Thread(() => {
                    for (int i = 0; i < 1000; i++)
                    {
                        lock(locker)
                        {
                            Counter++;
                        }
                        Thread.Sleep(1);
                    }
                });
                t1.Start();
    
                Thread t2 = new Thread(() => {
                    for (int i = 0; i < 1000; i++)
                    {
                        lock (locker)
                        {
                            Counter++;
                        }
                        Thread.Sleep(1);
                    }
                });
                t2.Start();
                #endregion
    
                Thread.Sleep(3000);
                Console.WriteLine(Counter);
                Console.ReadKey();
            }
        }
    }

    这时我们在运行程序,查看输出结果:

    这时输出结果是正确的。

    注意:lock只能锁住同一个对象,如果是不同的对象,还是会有线程同步的问题。 lock锁定的对象必须是引用类型的对象。

    我们在定义一个Object类型的对象,lock分别锁住两个对象,看看是什么结果:

    using System;
    using System.Threading;
    
    namespace ThreadSynchDemo
    {
        class Program
        {
            private static int Counter = 0;
            // 定义一个locker对象
            private static Object locker = new Object();
            // 定义locker2
            private static Object locker2 = new Object();
            static void Main(string[] args)
            {
                #region 存在线程同步问题
                //Thread t1 = new Thread(() => {
                //    for (int i = 0; i < 1000; i++)
                //    {
                //        Counter++;
                //        Thread.Sleep(1);
                //    }
                //});
                //t1.Start();
    
                //Thread t2 = new Thread(() => {
                //    for (int i = 0; i < 1000; i++)
                //    {
                //        Counter++;
                //        Thread.Sleep(1);
                //    }
                //});
                //t2.Start(); 
                #endregion
    
                #region 使用Lock解决线程同步问题
                //Thread t1 = new Thread(() => {
                //    for (int i = 0; i < 1000; i++)
                //    {
                //        lock(locker)
                //        {
                //            Counter++;
                //        }
                //        Thread.Sleep(1);
                //    }
                //});
                //t1.Start();
    
                //Thread t2 = new Thread(() => {
                //    for (int i = 0; i < 1000; i++)
                //    {
                //        lock (locker)
                //        {
                //            Counter++;
                //        }
                //        Thread.Sleep(1);
                //    }
                //});
                //t2.Start();
                #endregion
    
                #region 使用lock锁住不同的对象也会有线程同步问题
                Thread t1 = new Thread(() => {
                    for (int i = 0; i < 1000; i++)
                    {
                        lock (locker)
                        {
                            Counter++;
                        }
                        Thread.Sleep(1);
                    }
                });
                t1.Start();
    
                Thread t2 = new Thread(() => {
                    for (int i = 0; i < 1000; i++)
                    {
                        lock (locker2)
                        {
                            Counter++;
                        }
                        Thread.Sleep(1);
                    }
                });
                t2.Start();
                #endregion
                Thread.Sleep(3000);
                Console.WriteLine(Counter);
                Console.ReadKey();
            }
        }
    }

    程序运行结果:

    可以看到,这时还是会有线程同步的问题。虽然使用了lock,但是我们锁住的是不同的对象,这样也会有线程同步问题。lock必须锁住同一个对象才可以。

    我们下面在来看一个多线程同步问题的例子:

    using System;
    using System.Threading;
    
    namespace ThreadSynchDemo2
    {
        class Program
        {
            static int Money = 100;
    
            /// <summary>
            /// 定义一个取钱的方法
            /// </summary>
            /// <param name="name"></param>
            static void QuQian(string name)
            {
                Console.WriteLine(name + "查看一下余额" + Money);
                int yue = Money - 1;
                Console.WriteLine(name + "取钱");
                Money = yue;
                Console.WriteLine(name + "取完了,剩" + Money);
            }
    
            static void Main(string[] args)
            {
                Thread t1 = new Thread(() => {
                    for (int i = 0; i < 10; i++)
                    {
                        QuQian("t2");
                    }
                });
                Thread t2 = new Thread(() => {
                    for (int i = 0; i < 10; i++)
                    {
                        QuQian("t2");
                    }
                });
                t1.Start();
                t2.Start();
                t1.Join();
                t2.Join();
                Console.WriteLine("余额" + Money);
                Console.ReadKey();
            }
        }
    }

    我们看一下输出结果:

    可以看到,最终的余额并不是80,这也是线程同步带来的问题,如何解决。解决思路就是使用同步的技术避免两个线程同时修改一个余额。

    1、最大粒度——同步方法

     在方法上面使用[MethodImpl(MethodImplOptions.Synchronized)],标记该方法是同步方法,这样一个方法只能同时被一个线程访问。我们在QuQian的方法上面标记,修改后的代码如下:

    using System;
    using System.Runtime.CompilerServices;
    using System.Threading;
    
    namespace ThreadSynchDemo2
    {
        class Program
        {
            static int Money = 100;
    
            /// <summary>
            /// 定义一个取钱的方法,在上面标记为同步方法
            /// </summary>
            /// <param name="name"></param>
            [MethodImpl(MethodImplOptions.Synchronized)]
            static void QuQian(string name)
            {
                Console.WriteLine(name + "查看一下余额" + Money);
                int yue = Money - 1;
                Console.WriteLine(name + "取钱");
                Money = yue;
                Console.WriteLine(name + "取完了,剩" + Money);
            }
    
            static void Main(string[] args)
            {
                Thread t1 = new Thread(() => {
                    for (int i = 0; i < 10; i++)
                    {
                        QuQian("t2");
                    }
                });
                Thread t2 = new Thread(() => {
                    for (int i = 0; i < 10; i++)
                    {
                        QuQian("t2");
                    }
                });
                t1.Start();
                t2.Start();
                t1.Join();
                t2.Join();
                Console.WriteLine("余额" + Money);
                Console.ReadKey();
            }
        }
    }

    程序输出结果:

    现在的方法就是“线程安全”的了。什么是“线程安全”呢?“线程安全”是指方法可以被多个线程随意调用,而不会出现混乱。如果出现了混乱,那么就是“线程不安全”的。“线程安全”的方法可以在多线程里面随意的使用。

    2、对象互斥锁

    对象互斥锁就是我们上面讲的lock。我们在用lock来修改上面QuQian的例子:

    using System;
    using System.Runtime.CompilerServices;
    using System.Threading;
    
    namespace ThreadSynchDemo2
    {
        class Program
        {
            static int Money = 100;
    
            /// <summary>
            /// 定义一个取钱的方法,在上面标记为同步方法
            /// </summary>
            /// <param name="name"></param>
            //[MethodImpl(MethodImplOptions.Synchronized)]
            //static void QuQian(string name)
            //{
            //    Console.WriteLine(name + "查看一下余额" + Money);
            //    int yue = Money - 1;
            //    Console.WriteLine(name + "取钱");
            //    Money = yue;
            //    Console.WriteLine(name + "取完了,剩" + Money);
            //}
    
            private static object locker = new object();
            static void QuQian(string name)
            {
                Console.WriteLine(name + "查看一下余额" + Money);
                int yue = Money - 1;
                Console.WriteLine(name + "取钱");
                Money = yue;
                Console.WriteLine(name + "取完了,剩" + Money);
            }
    
            static void Main(string[] args)
            {
                Thread t1 = new Thread(() => {
                    for (int i = 0; i < 10; i++)
                    {
                        // 使用对象互斥锁
                        lock(locker)
                        {
                            QuQian("t1");
                        }
                    }
                });
                Thread t2 = new Thread(() => {
                    for (int i = 0; i < 10; i++)
                    {
                        lock (locker)
                        {
                            QuQian("t2");
                        }
                    }
                });
                t1.Start();
                t2.Start();
                t1.Join();
                t2.Join();
                Console.WriteLine("余额" + Money);
                Console.ReadKey();
            }
        }
    }

    程序输出结果:

    可以看到,最终的输出结果还是80。

    同一时刻只能有一个线程进入同一个对象的lock代码块。必须是同一个对象才能起到互斥的作用。lock后必须是引用类型,不一定是object,只要是对象就行。

    锁对象选择很重要,选不对就起不到同步的作用;选不对还有可能会造成其他地方被锁,比如用字符串做锁(因为字符串缓冲池导致导致可能用的是其他地方正在使用的锁),所以不建议使用字符串做锁。下面的代码就是不允许的:

    lock("locker")

    两个方法如果都用一个对象做锁,那么访问A的时候就不能访问B,因此锁选择很重要。

    3、Monitor

    其实lock关键字就是对Monitor的简化调用,lock最终会被编译成Monitor,因此一般不直接使用Monitor类,看下面代码:

    using System;
    using System.Threading;
    
    namespace MonitorDemo
    {
        class Program
        {
            static int Money = 100;
            private static object locker = new object();
            static void QuQian(string name)
            {
                // 等待没有人锁定locker对象,就锁定它,然后继续执行
                Monitor.Enter(locker);
                try
                {
                    Console.WriteLine(name + "查看一下余额" + Money);
                    int yue = Money - 1;
                    Console.WriteLine(name + "取钱");
                    Money = yue;
                    Console.WriteLine(name + "取完了,剩" + Money);
                }
                finally
                {
                    // 释放locker对象的锁
                    Monitor.Exit(locker);
                }
            }
    
            static void Main(string[] args)
            {
                Thread t1 = new Thread(() => {
                    for (int i = 0; i < 10; i++)
                    {
                            QuQian("t1");
                    }
                });
                Thread t2 = new Thread(() => {
                    for (int i = 0; i < 10; i++)
                    {
                            QuQian("t2");
                    }
                });
                t1.Start();
                t2.Start();
                t1.Join();
                t2.Join();
                Console.WriteLine("余额" + Money);
                Console.ReadKey();
            }
        }
    }

    程序输出结果:

    Monitor类里面还有TryEnter方法,如果Enter的时候有人在占用锁,它不会等待,而是会返回false。看下面的示例代码:

    using System;
    using System.Threading;
    
    namespace MonitorDemo
    {
        class Program
        {
            static int Money = 100;
            private static object locker = new object();
            static void QuQian(string name)
            {
                // 等待没有人锁定locker对象,就锁定它,然后继续执行
                Monitor.Enter(locker);
                try
                {
                    Console.WriteLine(name + "查看一下余额" + Money);
                    int yue = Money - 1;
                    Console.WriteLine(name + "取钱");
                    Money = yue;
                    Console.WriteLine(name + "取完了,剩" + Money);
                }
                finally
                {
                    // 释放locker对象的锁
                    Monitor.Exit(locker);
                }
            }
    
            static void F1(int i)
            {
                if (!Monitor.TryEnter(locker))
                {
                    Console.WriteLine("有人在锁着呢");
                    return;
                }
                Console.WriteLine(i);
                Monitor.Exit(locker);
            }
    
    
            static void Main(string[] args)
            {
                //Thread t1 = new Thread(() => {
                //    for (int i = 0; i < 10; i++)
                //    {
                //            QuQian("t1");
                //    }
                //});
                //Thread t2 = new Thread(() => {
                //    for (int i = 0; i < 10; i++)
                //    {
                //            QuQian("t2");
                //    }
                //});
                Thread t1 = new Thread(() => {
                    for (int i = 0; i < 10; i++)
                    {
                        F1(i);
                    }
                });
                Thread t2 = new Thread(() => {
                    for (int i = 0; i < 10; i++)
                    {
                        F1(i);
                    }
                });
    
                t1.Start();
                t2.Start();
                t1.Join();
                t2.Join();
                Console.WriteLine("余额" + Money);
                Console.ReadKey();
            }
        }
    }

    程序输出结果:

  • 相关阅读:
    ES6, CommonJS, AMD, CMD,UMD模块化规范介绍及使用
    前端项目开发框架选型需考虑的4个方面
    初识webSocket及其使用
    动态组件 —— 2种方式实现动态组件的切换
    mac下anaconda安装新包
    新版docker设置国内镜像
    记一次解决Original error: UiAutomator quit before it successfully launched
    linux clion cmakelisits undefined reference 未定义引用
    苹果设备插入PC不能识别问题解决办法
    用Cucumber理解BDD行为驱动开发
  • 原文地址:https://www.cnblogs.com/dotnet261010/p/12335538.html
Copyright © 2020-2023  润新知