(20)线程安全问题:Lock,双锁问题,Monitor,死锁

news/2024/5/19 3:26:31 标签: 线程安全, lock, monitor, mutex, 死锁

一、Lock


    1、用多线程给变量自增,10000个线程自增

        List<Task> tasks = new List<Task>();
        int AsyncNum = 0;
        for (int i = 0; i < 10000; i++)
        {
            tasks.Add(Task.Run(() =>
            {
                AsyncNum++;
            }));
        }
        Task.WaitAll(tasks.ToArray());
        Console.WriteLine($"AsyncNum is {AsyncNum}");


        最后的结果:AsyncNum is 9980 。不是10000。
        为什么?
        多个任务并行地对AsyncNum进行递增操作。由于这些任务是并行执行的,可能会出现竞态条件(race condition)。
        竞态条件是指多个线程同时访问和修改共享资源,而没有适当的同步措施来保证操作的原子性。在这种情况下,多个任务可能会同时读取和修改AsyncNum的值,导致结果不确定。
    
    
    2、什么是竞态条件?
        竞态条件(Race Condition)是指当多个线程或进程同时访问和操作共享资源时,最终的结果依赖于它们执行的相对时间顺序,而不是预期的顺序。竞态条件可能导致不确定的行为,破坏程序的正确性和可靠性。
        竞态条件发生的原因是并发执行的线程或进程之间的交互和竞争。当多个线程同时访问和修改共享资源时,如果没有适当的同步机制来保证操作的原子性,就会出现竞态条件。这些共享资源可以是内存中的变量、文件、网络连接、数据库等。

        int count = 0;
        void Increment()
        {
            int temp = count;
            temp++;
            count = temp;
        }


        上面,多个线程同时调用Increment方法来递增count变量的值。由于没有适当的同步机制,多个线程可能会同时读取和修改count的值,导致结果不确定。例如,如果两个线程同时读取count的值为10,然后将其递增,并将结果写回count,那么最终的结果可能是11,而不是预期的12。
    
    
    3、改进,用lock

        List<Task> tasks = new List<Task>();
        int AsyncNum = 0;
        object lockobject = new object();
        for (int i = 0; i < 10000; i++)
        {
            tasks.Add(Task.Run(() =>
            {
                lock (lockobject)
                {
                    AsyncNum++;
                }
            }));
        }
        Task.WaitAll(tasks.ToArray());
        Console.WriteLine($"AsyncNum is {AsyncNum}");  

 
        为了解决竞态条件,可以使用线程同步机制,如互斥锁、信号量、条件变量等,来确保对共享资源的访问是原子的。这些同步机制可以保证只有一个线程能够访问共享资源,并且在修改共享资源之前,其他线程必须等待。
        通过避免竞态条件,您可以确保多线程或多进程的程序能够正确地访问和操作共享资源,从而提高程序的正确性和可靠性。
    
    
    4、什么是lock?
        lock是C#中的一个关键字,用于实现线程同步和互斥访问共享资源。它提供了一种简单的方式来确保在同一时间只有一个线程可以访问被保护的代码块。lock关键字的语法:

        lock (lockObject)
        {
            // 被保护的代码块
        }


        上面lockObject是一个用于同步的对象。当一个线程进入lock代码块时,它会尝试获取lockObject的锁。如果锁可用,该线程将进入临界区,执行被保护的代码块。其他线程在这个时候会被阻塞,直到锁被释放。
        
        当线程执行完被保护的代码块后,会自动释放锁,允许其他线程进入临界区。这样就确保了在同一时间只有一个线程可以执行被保护的代码块,从而避免了竞态条件。
        lock关键字使用的是独占锁(exclusive lock),也称为互斥锁(mutex。它保证了在任何给定的时刻,只有一个线程能够持有锁,并且其他线程必须等待锁的释放。
        
        注意,lock关键字只能用于引用类型的对象。通常情况下,可以使用一个专门用于同步的对象作为锁。例如,可以使用一个object类型的变量作为锁:

        object lockObject = new object();
        lock (lockObject)
        {
            // 被保护的代码块
        }


        
        
        在使用lock关键字时,需要注意以下几点:
        (1)尽量将锁的范围限制在最小的代码块内,以减少对其他线程的阻塞时间。
        (2)锁对象应该是一个私有的、只在同步上下文中使用的对象,以避免外部代码对锁的访问。
        (3)避免在锁内部调用可能引发异常的代码,以免锁无法被释放。
        lock关键字是一种简单而有效的线程同步机制,可以确保共享资源的安全访问。但在一些复杂的情况下,可能需要使用更高级的同步机制,如Monitor类、互斥体、信号量等。
        
        lock使用静态、只读和私有的引用对象(不能是string,不能是null)是一个常见的做法。
        (1)静态:
        使用静态对象作为锁可以确保在多个实例之间共享同一个锁,从而实现跨实例的线程同步。如果使用实例级别的对象作为锁,那么每个实例都会有一个独立的锁,无法实现跨实例的同步。
        (2)只读:
        使用只读对象作为锁可以确保锁对象的引用不会被修改。这是因为lock语句需要一个不可变的锁对象,以便在多个线程之间共享同一个锁。如果锁对象可变,那么在多个线程之间可能会出现竞争条件,导致同步失效。
        (3)私有:
        将锁对象声明为私有可以限制对锁对象的访问,避免外部代码对锁对象的误用。这可以提高代码的可维护性和安全性。

        public static int count = 0;
        private static readonly object obj = new object();//静态,私有,只读,引用

        private static async Task Main(string[] args)
        {
            if (!File.Exists(@"E:\1.txt"))
            { File.Create(@"E:\1.txt").Close(); }
            else
            {
                try
                {
                    using (var f = File.Open(@"E:\1.txt", FileMode.Open, FileAccess.Read, FileShare.None))
                    {
                    }
                    File.WriteAllText(@"E:\1.txt", string.Empty);
                }
                catch (Exception)
                { Console.WriteLine("文件正被使用!"); return; }
            }
            List<Task> tasks = new List<Task>();

            for (int i = 0; i < 100; i++)
            {
                tasks.Add(Task.Run(() =>
                {
                    //lock (obj)
                    {
                        WriteTxt();
                    }
                }));
            }

            await Task.WhenAll(tasks);
            Console.WriteLine("任务完成");
            Console.ReadKey();
        }

        private static void WriteTxt()
        {
            string s = (++count).ToString("000");
            s = $"[{s}]:{DateTime.Now}\r\n";
            File.AppendAllText(@"E:\1.txt", s);
        }


        加上锁后,写入txt文件就不会出错,否则多个线程竞争抛异常。
    
    
    5、理解lock
        正如单人厕所一样,多个人(线程)想要上厕所,那么A先来就进去关门锁上,其它人一看,有人!只有等待A上完。A上完后开锁打开门出去,其它人就按一定的规则,又进去一个人,然后关门锁上。。。如此,直到所人的都上完。
        
        上锁与开锁就一个信号灯一样。
        
        这个锁就是一个锚定对象,是一个信号灯,与真正做事(上厕所)无关联。
        要控制锁的范围,不能无规则地打开或锁上(上面4(2)),同时上锁后上厕所的时间要控制好(上面4(1)),不能占用太久,只写关键性的小片代码,以免门外的多人(等待线程)等待得太久。当然正在上厕所的人(执行线程)也不要发病(异常),来个脑梗,外面的人(等待线程)就不知道里面细节,大家一直死等。
    
    
    6、使用lock的注意点
        在使用lock关键字时,lockObject并不需要具有特定的有意义的值,它只是一个用于同步的锚定对象。可以将其看作是一个信号灯或互斥锁,用于控制对被保护代码块的访问。

        当一个线程进入lock代码块时,它会尝试获取lockObject的锁。如果锁可用,该线程将进入临界区,执行被保护的代码块。其他线程在这个时候会被阻塞,直到锁被释放。

        只有当锁被释放,即被保护的代码块执行完毕后,其他线程才能够进入临界区执行被保护的代码块。

        lock关键字的作用是确保在同一时间只有一个线程可以进入被保护的代码块,从而避免了多个线程同时访问共享资源导致的竞态条件。

        注意,为了确保同步,应该使用一个专门用于同步的对象作为锁,而不是使用共享资源本身作为锁。这是因为锁对象应该是私有的,并且只在同步上下文中使用,以避免外部代码对锁的访问。
        
        举例:不用锁时

        public static readonly object obj = new object();
        private static void Main(string[] args)
        {
            int count = 0;
            for (int i = 0; i < 10; i++)
            {
                Task.Run(() =>
                {
                    Console.WriteLine(count++);
                });
            }
            Console.WriteLine("任务完成.");
            Console.ReadKey();
        }


        结果:
            任务完成.
            1
            2
            9
            7
            4
            3
            5
            6
            0
            8
        问:上面并发后是乱序,但并没有竞争,为什么呢?
        答:由于 Console.WriteLine 方法内部包含了线程同步机制,每次只能有一个线程执行该方法。因此,当多个线程同时执行 Console.WriteLine(count) 时,会按照顺序进行输出,避免输出结果中的数字重复。这就是为什么在 lock 语句块内部输出的结果中没有重复数字。
        
        修改一下:

        int count = 0;
        for (int i = 0; i < 20; i++)
        {
            Task.Run(() =>
            {
                //lock (obj)
                {
                    Console.WriteLine(count);
                    count++;
                }
            });
        }
        Console.WriteLine("任务完成.");


        结果:
            任务完成.
            0
            0
            1
            3
            4
            2
            2
            6
            2
            5
        将 count++ 放在 lock 语句块外面时,多个线程可以同时执行 count++ 操作,这就产生了竞争条件。多个线程同时增加 count 的值,可能会导致输出结果中出现重复的数字。
        
        因此,为了避免竞争条件,应该将 count++ 操作放在 lock 语句块内部,确保每次只有一个线程能够执行该操作。这样可以保证输出结果中不会出现重复的数字。
            
        用锁后:

        public static readonly object obj = new object();

        private static void Main(string[] args)
        {
            int count = 0;
            for (int i = 0; i < 10; i++)
            {
                Task.Run(() =>
                {
                    lock (obj)
                    {
                        Console.WriteLine(count);
                        count++
                    }
                });
            }
            Console.WriteLine("任务完成.");
            Console.ReadKey();
        }    


        结果:
            0
            1
            2
            3
            4
            5
            6
            7
            任务完成.
            8
            9
        
        
    
    7、上面程序可以用并发:

        List<Task> tasks = new List<Task>();
        int pNum = 0;
        object lockObject = new object();
        ParallelOptions po = new ParallelOptions();
        po.MaxDegreeOfParallelism = 10;
        Parallel.ForEach(Enumerable.Range(0, 10000), po, i =>
        {
            lock (lockObject)
            {
                pNum++;
            }
        });
        Console.WriteLine(pNum);


        上面用Parallel并发执行,但同样也有竞态条件,需要用lock进行锁定,确保同时只有一个线程执行。
        上面foreach中第三个参数省略了括号。当委托的参数列表只有一个参数时,可以省略参数的括号。这是一种简化语法的写法。省略参数括号可以使代码更加简洁,但也可能降低代码的可读性,因此需要根据具体情况进行权衡。
    
    
    8、什么是Parallel ?
        Parallel类是.NET Framework中提供的一个用于并行编程的工具类。它提供了一组方法和类型,可以简化并行任务的创建和执行,并充分利用多核处理器的性能。

        使用Parallel类可以将一个任务分解为多个子任务,并并行地执行这些子任务。这样可以加快任务的执行速度,提高系统的响应性能。

        Parallel类提供了以下常用的方法:
        (1)Parallel.For:
        (2)Parallel.ForEach:用于并行地遍历一个集合。可以提供一个委托来定义对集合元素的操作。
        (3)Parallel.Invoke:用于并行地执行多个操作。可以提供多个委托,每个委托定义一个操作。

        这些方法都会自动将任务分配给可用的处理器核心,并根据系统资源的情况进行动态调整。它们还提供了一些选项,可以控制并行执行的行为,如最大并行度、取消操作等。

        使用Parallel类编写并行代码时,需要注意以下几点:

        (1)**任务的独立性**:
        要确保并行执行的任务之间是相互独立的,不会产生竞态条件或数据依赖关系。这样才能确保并行执行的正确性和性能提升。

        (2)**共享资源的同步**:
        如果多个任务需要访问共享资源(如共享变量),则需要使用线程同步机制来确保对共享资源的访问是线程安全的。可以使用锁、互斥量、信号量等机制来实现线程同步。

        (3)**性能评估和调优**:
        并行执行的性能往往受到多个因素的影响,如任务的粒度、任务之间的通信开销、系统资源的利用率等。在编写并行代码时,需要进行性能评估和调优,以获得最佳的性能提升。
    
    
    9、什么是ParallelOptions?
        ParallelOptions是Parallel类中的一个类,用于指定并行执行的一些选项和参数。通过创建一个ParallelOptions对象,并将其作为参数传递给Parallel类的方法,可以对并行执行的行为进行定制和控制。

        ParallelOptions类提供了以下常用的属性和方法:

        (1)MaxDegreeOfParallelism:
        用于设置并行执行的最大并行度。可以通过设置此属性的值来限制并行任务所使用的处理器核心数量。默认情况下,MaxDegreeOfParallelism的值为-1,表示使用系统可用的所有处理器核心。

        (2)CancellationToken:
        用于设置一个取消标记,以便在需要时取消并行执行的操作。可以使用CancellationTokenSource类创建一个取消标记,并将其传递给ParallelOptions的构造函数或CancellationToken属性。

        (3)TaskScheduler:
        用于设置并行任务的调度器。可以通过TaskScheduler类的静态方法来创建一个自定义的调度器,并将其传递给ParallelOptions的构造函数或TaskScheduler属性。

        通过创建一个新的ParallelOptions对象,可以为每个并行操作提供不同的选项和参数,以满足具体的需求。
    
    
    10、对上面程序再优化一下:

        List<Task> tasks = new List<Task>();
        int pNum = 0;
        object lockObject = new object();
        ParallelOptions po = new ParallelOptions();
        po.MaxDegreeOfParallelism = 10;
        Parallel.ForEach(Enumerable.Range(0, 10000), po, (i) =>
        {
            Interlocked.Increment(ref pNum);
        });
        Console.WriteLine(pNum);
        Console.ReadKey();


        ref关键字用于传递参数的引用。在该代码中,Interlocked.Increment(ref pNum)使用了ref关键字来将pNum参数按引用传递给Interlocked.Increment方法。

        通过使用ref关键字,可以使参数按引用传递而不是按值传递。这意味着在方法内部对参数的修改会影响到调用方法时传递的变量本身。

        在Interlocked.Increment(ref pNum)中,pNum是一个变量(通常是整数类型),Interlocked.Increment方法将该变量的值原子性地增加1,并返回新的值。通过使用ref关键字,可以确保对pNum的操作作用于原始变量,而不仅仅是传递了一个副本。
    
    
    11、什么是Interlocked?
        Interlocked是C#中的一个类,提供了一组原子操作方法,用于对共享变量进行原子操作。这些原子操作是在硬件级别上实现的,可以确保操作的原子性,即在执行期间不会被其他线程中断。

        相比于使用锁来实现线程安全,Interlocked的原子操作具有以下优势:

        (1)**无需获取锁**:
        使用锁需要线程在进入临界区之前获取锁对象,而在离开临界区之后释放锁对象。这个过程涉及到线程的上下文切换和内核模式的开销,可能会影响性能。而使用Interlocked的原子操作,不需要获取锁对象,因此可以避免这些开销。

        (2)**原子性保证**:
        Interlocked的原子操作是在硬件级别上实现的,可以确保操作的原子性。这意味着在执行原子操作期间,不会有其他线程对共享变量进行修改,从而避免了竞态条件的发生。

        (3)**高性能**:
        由于Interlocked的原子操作是在硬件级别上实现的,它们通常比使用锁的方式具有更高的性能。这是因为原子操作不涉及线程的上下文切换和内核模式的开销,而且可以在多核处理器上并行执行。
        
        Interlocked.Decrement: 原子地将指定变量递减1,并返回递减后的值。
        int count = 10;
        Interlocked.Decrement(ref count);
        Interlocked.CompareExchange: 原子地比较并交换变量的值。可以用来实现原子的读取-修改-写入操作。
        int value = 5;
        int newValue = 10;
        int oldValue = Interlocked.CompareExchange(ref value, newValue, 5);
        注意,Interlocked的原子操作只能应用于特定类型的共享变量,例如int、long、float等。对于复杂的数据结构或需要进行多个操作的情况,可能仍然需要使用锁来保证线程安全
        
        
        问:Interlocked能用在类上吗?
        答:不能。
            Interlocked类提供的原子操作方法主要适用于基本数据类型(如int、long、float等)的操作。它们是在硬件级别上实现的,可以确保操作的原子性。

            对于复杂的数据结构或类的操作,Interlocked类的原子操作方法并不直接适用。如果需要在多线程环境下对类进行操作,通常需要使用锁或其他线程同步机制来确保线程安全
        
            注意,lock关键字会引入一定的开销,因为它涉及到线程的上下文切换和内核模式的开销。因此,在使用锁时需要权衡性能和线程安全性。

            总结,Interlocked类的原子操作方法适用于基本数据类型的操作,而对于复杂的数据结构或类的操作,通常需要使用锁或其他线程同步机制来确保线程安全


二、Lock的死锁


    1、多把锁也可能造成相互死锁
        例:可能同时使用刀与叉,有时只用刀或叉。给刀一把锁和叉一把锁。线程A与B分别使用这两把锁。

        private static readonly object obj1 = new object();
        private static readonly object obj2 = new object();

        private static void Main(string[] args)
        {
            int n = 0;
            Task t1 = Task.Run(() =>
            {
                for (int i = 0; i < 5; i++)
                {
                    lock (obj1)
                    {
                        Console.WriteLine("第A线程上1锁" + n);
                        lock (obj2)
                        {
                            n++;
                            Console.WriteLine("第A线程    " + n);
                        }
                    }
                }
            });
            Task t2 = Task.Run(() =>
            {
                for (int i = 0; i < 5; i++)
                {
                    lock (obj2)
                    {
                        Console.WriteLine("第B线程上2锁" + n);
                        lock (obj1)
                        {
                            n++;
                            Console.WriteLine("第B线程----" + n);
                        }
                    }
                }
            });

            Console.ReadKey();
        }


        结果:
            第A线程上1锁0
            第B线程上2锁0
        因为A准务进行等待第2锁,但此时第2锁被B线程占用,而B线程占用2锁又等待1锁,AB两线程相互等待对方的锁,形成死锁
        
        解决办法:
        把锁的顺序改为一致即可。

        private static readonly object obj1 = new object();
        private static readonly object obj2 = new object();

        private static void Main(string[] args)
        {
            int n = 0;
            Task t1 = Task.Run(() =>
            {
                for (int i = 0; i < 5; i++)
                {
                    lock (obj1)
                    {
                        Console.WriteLine("第A线程上1锁" + n);
                        lock (obj2)
                        {
                            n++;
                            Console.WriteLine("第A线程    " + n);
                        }
                    }
                }
            });
            Task t2 = Task.Run(() =>
            {
                for (int i = 0; i < 5; i++)
                {
                    lock (obj1)
                    {
                        Console.WriteLine("第B线程上2锁" + n);
                        lock (obj2)
                        {
                            n++;
                            Console.WriteLine("第B线程----" + n);
                        }
                    }
                }
            });

            Console.ReadKey();
        }


        结果:
            第A线程上1锁0
            第A线程    1
            第A线程上1锁1
            第A线程    2
            第B线程上2锁2
            第B线程----3
            第B线程上2锁3
            第B线程----4
            第B线程上2锁4
            第B线程----5
            第B线程上2锁5
            第B线程----6
            第B线程上2锁6
            第B线程----7
            第A线程上1锁7
            第A线程    8
            第A线程上1锁8
            第A线程    9
            第A线程上1锁9
            第A线程    10
        
    
    9、问:lock(obj)如果修改了obj,会怎样?
        答:如果修改了 obj,那么锁定的对象就会发生变化,此时 lock(obj) 将不再起作用。即起不到锁定的作用。


三、Monitor


    1、Monitor和lock也是实现同步与互斥的。
        其实lock的底层就是使用Monitor来实现的。
        
        Enter指的是Monitor.Enter(获取指定对象上的排他锁。);
        Exit指的是Monitor.Exit(释放指定对象上的排他锁。)    
    2、实例认识

        public static readonly object obj = new object();

        private static void Main(string[] args)
        {
            int count = 0;
            for (int i = 0; i < 10; i++)
            {
                Task.Run(() =>
                {
                    Monitor.Enter(obj);
                    try
                    {
                        Console.WriteLine(count);
                        count++;
                    }
                    finally
                    {
                        Monitor.Exit(obj);
                    }
                });
            }
            Console.WriteLine("任务完成.");
            Console.ReadKey();
        }


        结果:
            任务完成.
            0
            1
            2
            3
            4
            5
            6
            7
            8
            9
        
        
    3、问:为什么改为Monitor.TryEnter输出的结果不对?

        public static readonly object obj = new object();

        private static void Main(string[] args)
        {
            int count = 0;
            for (int i = 0; i < 10; i++)
            {
                Task.Run(() =>
                {
                    if (Monitor.TryEnter(obj))
                    {
                        try
                        {
                            Console.WriteLine(count);
                            count++;
                        }
                        finally
                        {
                            Monitor.Exit(obj);
                        }
                    }
                });
            }
            Console.WriteLine("任务完成.");
            Console.ReadKey();
        }


        结果:
            任务完成.
            0
            1
            2
            3
        可以看到有些结果不见了。。。。
        
        答:简言之,TryEnter是非阻塞,任务取不到锁,就向下执行了。而Enter是阻塞,取不到锁就阻塞别想走,真正起到锁的作用。
        
            Monitor.TryEnter方法是一个非阻塞的方法。如果锁不可用,它会立即返回false,而不会等待锁可用。因此,如果一个任务尝试获取锁但失败了,它会跳过临界区的代码,继续执行后续的语句。

            上面,由于多个任务几乎同时尝试获取锁,只有一个任务能够成功获取锁并进入临界区。其他任务由于锁不可用,会跳过临界区的代码,直接执行后续的语句。因此,你只看到了计数器的值(0-3),其它值的任务已经执行走了,无法再看到其他任务的计数器值。

            如果想要确保所有任务都能够进入临界区,可以使用Monitor.Enter方法来获取锁。Monitor.Enter方法是一个阻塞的方法,如果锁不可用,它会等待直到锁可用为止。这样,每个任务都能够按顺序进入临界区,避免了竞态条件的问题。
        
        
    4、问:什么是临界区?
        答:临界区(Critical Section)是指一段代码或一块共享资源,在同一时间只能被一个线程访问的区域。
        
            在临界区内部,线程可以对共享资源进行读取、写入或其他操作。临界区的目的是保护共享资源的一致性,避免多个线程同时访问共享资源导致的竞态条件和数据不一致性。

            在多线程编程中,当多个线程同时访问共享资源时,如果没有适当的同步机制来保护临界区,就会发生竞态条件(Race Condition)。竞态条件可能导致不可预测的结果,如数据损坏、数据丢失、死锁等问题。

            通过使用同步机制,如锁(Lock)或信号量(Semaphore),可以限制只有一个线程可以进入临界区。这样,当一个线程进入临界区时,其他线程必须等待,直到该线程退出临界区。这种同步机制确保了共享资源的一致性和正确性。

            临界区的正确使用对于多线程编程的正确性至关重要。它可以确保线程安全,避免竞态条件和数据不一致性的问题。
        
        
    5、Monitor类介绍
        
        Monitor类用于实现线程同步和互斥的一个工具类。
        
        它提供了一些方法来控制对共享资源的访问,以确保多个线程能够安全地访问共享资源。

        (1) Enter:用于获取锁定对象,如果对象已经被其他线程锁定,则当前线程会被阻塞,直到锁定对象被释放。

        (2)Exit:用于释放锁定对象,允许其他线程获取该对象的锁。

        (3)TryEnter:尝试获取锁定对象,取得锁则返回true,失败(已经被其它锁定)则为false.

        (4)Wait:使当前线程等待,直到其他线程通过调用Monitor.Pulse或Monitor.PulseAll方法唤醒它。

        (5)Pulse:唤醒等待在锁定对象上的一个线程。

        (6)PulseAll:唤醒等待在锁定对象上的所有线程。

        注意,Monitor类是基于内核对象的,因此在使用时需要谨慎,避免出现死锁等问题。同时,应该尽量使用较小的锁定对象,以减少线程等待的时间和提高性能。
        
        
    6、问:同步互斥时用lock还是Monitor?
        答:推荐使用lock
            
            一般使用lock关键字来实现线程同步和互斥。lock关键字是基于Monitor类实现的,而Monitor类提供了更底层的线程同步功能。

            使用lock关键字可以更简单地实现线程同步,它会自动获取和释放锁。在使用lock关键字时,需要传入一个对象作为锁定的对象,多个线程对于同一个锁定对象的lock操作会被互斥执行,保证了线程安全

            而Monitor类提供了更多底层的线程同步方法,可以手动调用Monitor.Enter和Monitor.Exit方法来实现锁定和释放锁。使用Monitor类可以更加灵活地控制线程同步,但也需要更多的手动操作。            因此,一般情况下推荐使用lock,它更简单、更易于使用,并且在性能上与Monitor类相当。只有在需要更高级的线程同步功能时,才需要使用Monitor类。
        
        
    7、问:多个任务用Task还是Thread来实现?
        比如:上面的Task改写成下面

        public static readonly object obj = new object();

        private static void Main(string[] args)
        {
            int count = 0;
            for (int i = 0; i < 10; i++)
            {
                new Thread(() =>
                 {
                     Monitor.Enter(obj);
                     try
                     {
                         Console.WriteLine(count);
                         count++;
                     }
                     finally
                     {
                         Monitor.Exit(obj);
                     }
                 }).Start();
            }
            Console.WriteLine("任务完成.");
            Console.ReadKey();
        }


        结果:
            0
            1
            2
            3
            4
            5
            6
            7
            8
            任务完成.
            9
        
        答:在C#中,多个任务执行的选择可以根据具体的需求来决定使用Thread还是Task。

            (1) 如果任务是CPU密集型的,即需要大量的计算和处理,那么使用Thread可能更合适。
            Thread是基于操作系统的线程,可以直接利用多核处理器的能力,同时也可以更细粒度地控制线程的执行。但是需要注意的是,使用Thread需要手动管理线程的生命周期和同步,需要更多的编码工作。

            (2)如果任务是I/O密集型的,即涉及到大量的输入输出操作,那么使用Task可能更合适。
            Task是基于线程池的任务调度机制,可以有效地利用线程资源,并且提供了更高级的任务管理和调度功能。Task可以通过使用异步和等待的方式,简化了编程模型,使得代码更易于编写和维护。

            注意,Task是建立在Thread之上的,它是一种更高层次的抽象,可以更好地利用并发性能。在一般情况下,推荐使用Task来管理和调度多个任务的执行,因为它提供了更好的可扩展性和灵活性,同时也更符合现代异步编程的趋势。

            总结,如果任务是CPU密集型的,可以考虑使用Thread;如果任务是I/O密集型的,或者对任务管理和调度有更高级的需求,可以考虑使用Task。
        
        
    8、问:Task不一定开线程,即使开了线程,也不一定占用CPU核心?
        答:对的。
            Thread是基于操作系统的线程,它直接占用一个CPU核心并执行任务。每个Thread都有自己的堆栈和上下文,因此可以直接利用多核处理器的能力。
        
            而Task则是基于线程池的任务调度机制。线程池是一组预先创建好的线程,这些线程可以被多个任务共享。当任务需要执行时,线程池会从池中选择一个空闲的线程来执行任务。这种方式可以避免频繁地创建和销毁线程,提高了性能和资源利用率。
            
            这也是为什么Thread适合CPU跑,而Task适合I/O跑。
            
            
    9、问:线程不一定占用CPU核心?
        答:是的。线程与Thread是有区别的.
            
            线程池中的线程并不一定会直接占用CPU核心。线程池是一种预先创建好的线程集合,这些线程可以被多个任务共享。当有任务需要执行时,线程池会从池中选择一个空闲的线程来执行任务。线程池的目的是为了提高性能和资源利用率,避免频繁地创建和销毁线程。

            线程池中的线程会被操作系统调度到可用的CPU核心上执行任务。具体来说,操作系统会根据当前的系统负载情况,决定将线程调度到哪个CPU核心上执行。这样可以充分利用多核处理器的能力,提高并发性能。

            注意:线程池的线程并不是一定会占用CPU核心。
            如果任务是I/O密集型的,即涉及到大量的输入输出操作,线程可能会处于等待状态,不会占用CPU核心。而当有其他任务需要执行时,线程池会将等待的线程唤醒并分配任务给它们。

            总结:线程池中的线程会根据系统负载情况被调度到可用的CPU核心上执行任务,以提高并发性能。但并不是所有线程都会一直占用CPU核心,具体是否占用取决于任务的类型和当前的系统负载情况。

    
    10、问:Wait与Pulse(PulseAll)是冤家对头吗?
        答:是的。wait是深度睡眠的等待,pulse中唤醒。
        
            正如医院叫号一样,wait如坐在椅子上的病人,深度睡眠,不能主动去就诊。当用pulse进行叫号,23号!于是23号就唤醒,去就诊,但不一定医生会成功给他看病,但他有机会成功,比如同时唤醒了3个人就诊,只能有一个人成功,剩下的2人就会自动进入wait(深度睡眠,需要再次被唤醒)。
            
            在使用Monitor控制线程同步时,线程可以处于以下三个状态之一:
            (1)运行状态(Running):线程正在执行其任务代码。
            (2)等待状态(Waiting):线程调用了Monitor.Wait方法,释放了锁并进入等待状态,直到其他线程调用了相同对象上的Monitor.Pulse或Monitor.PulseAll方法来唤醒它。
            (3)阻塞状态(Blocked):线程尝试获取锁,但锁已被其他线程占用,因此线程被阻塞,等待锁的释放。

            这些状态是Monitor控制的一种常见模式,用于实现线程间的通信和同步。通过Monitor.Wait和Monitor.Pulse方法的配合使用,可以实现线程的等待和唤醒,以及线程间的协调和同步。
        
        
        问:阻塞也就是等待了,而且等待并非一定在临界区??
        答:是的。实际上两者意思相同,只是表达的角度。等待是对外面的线程而言,阻塞是对自身而言。
            
            无论是在Monitor中的等待状态还是阻塞状态,都表示线程暂停执行,直到某种条件满足。这种机制可以用来实现线程之间的协调和同步,确保线程在合适的时机进行操作,避免竞争条件和资源争用的问题。
            
            在Monitor中,线程可以在临界区之前、临界区门口或临界区内部调用Wait方法进行等待阻塞。Wait方法的调用会释放当前线程持有的锁定,并使线程进入等待状态,直到其他线程调用Monitor的Pulse或PulseAll方法来唤醒它。

            无论线程在临界区的哪个位置调用Wait方法,它都会进入等待状态,并且在被唤醒后需要重新获取锁定才能继续执行。这种等待阻塞的机制可以用来实现线程之间的协调和同步,确保线程在合适的时机进行操作,避免竞争条件和资源争用的问题。
        
            详细过程(了解):
            
            线程在使用Monitor控制线程同步时,可以从运行状态转变为等待状态,也可以从等待状态转变为运行状态。

            (1)运行状态(Running)转变为等待状态(Waiting):
                当线程调用Monitor.Wait方法时,它会释放锁并进入等待状态。线程会等待其他线程调用相同对象上的Monitor.Pulse或Monitor.PulseAll方法来唤醒它。

            (2)等待状态(Waiting)转变为运行状态(Running):
                当其他线程调用相同对象上的Monitor.Pulse方法时,等待的线程将被唤醒,并尝试重新获取锁。一旦获取到锁,线程将从等待状态转变为运行状态,继续执行。

                当其他线程调用相同对象上的Monitor.PulseAll方法时,所有等待的线程都会被唤醒,并竞争获取锁。只有一个线程能够获取到锁并进入运行状态,其他线程将继续等待。

            另外,线程也可以从等待状态转变为阻塞状态(Blocked):

                当线程调用Monitor.Wait方法后,它会释放锁并进入等待状态。如果此时其他线程已经获取了锁并且没有释放锁,那么等待的线程将无法获取锁,进而被阻塞。线程将一直处于阻塞状态,直到获取到锁并进入运行状态,或者被中断(即其他线程调用了该线程的Interrupt方法)。    
    11、wait/pulse实例

        public static readonly object obj = new object();

        private static void Main(string[] args)
        {
            Thread thA = new Thread(MyMethod);
            Thread thB = new Thread(MyMethod);
            thA.Start();
            thB.Start();
            Thread.Sleep(1000);//a
            lock (obj)
            {
                Monitor.Pulse(obj);//b
            }

            thA.Join();//c
            thB.Join();
            Console.ReadKey();
        }

        private static void MyMethod()
        {
            Console.WriteLine($"{Environment.CurrentManagedThreadId}方法开始");
            lock (obj)
            {
                Monitor.Wait(obj);//d
            }
            Console.WriteLine($"{Environment.CurrentManagedThreadId}方法结束");
        }


        大概意思就是,启动AB线程,但都等待wait,然后再唤醒,逐个完成。
        
        结果是:
            4方法开始
            3方法开始
            4方法结束
        
        上面每步都很紧凑。
        d处让每一个线程进入临界区后,马上释放锁进入等待状态(阻塞),这样A和B线程都在阻塞等待状态(一直等待别人的唤醒)
        a处的睡眠的目的就是让AB都在等待状态,因为两个线程的执行大约小于10毫秒,但为了确信两都处于等待,所以这里设置了1000毫秒。
        b处为什么又能进入临界区呢?因为d处AB两者进入后释放了锁,并处于等待,因为锁其实没有人在用,所以1000毫秒后,锁又可以用了,这时的pulse为什么必须放在锁内呢?起到保险的作用,万一AB都还没有从锁里出来,唤醒就起不到作用,直接一闪而过,用上锁,表示的先后顺序,AB锁上了又释放后,在b处就再上锁再唤醒。次序就不会错乱。
        
        c并不知道是哪个完成了,所以把两个子线程join加入到当前线程(主线程)进行等待,直到子线程完成后,主线程才继续向下执行。如果AB已经完成,再用join也不会报错,子线程会立即返回,主线程不用再等待直接向下执行。
        
        
        问:为什么上面的结果少了一个3结束?
        答:因为程序有bug,3已经死锁了。
            刚开始AB即3与4都进入wait等待状态,经b后只能唤醒一个,上面唤醒的是4,所以4结束。而3没有唤醒,所以3还是wait,主程序中用join等待子线程3的完成,而3还在等待主线程的唤醒,两个相互等待形成死锁
            
            所以还需要再唤醒剩下的等待的线程一次:

        Thread thA = new Thread(MyMethod);
        Thread thB = new Thread(MyMethod);
        thA.Start();
        thB.Start();
        Thread.Sleep(1000);//a
        lock (obj)
        {
            Monitor.Pulse(obj);//b
        }
        lock (obj)
        {
            Monitor.Pulse(obj);//f
        }
        Console.ReadKey();


    
    
    12、经典的动态平衡
        工厂生产产品,只能存储5批货物,满5批就停产等待消费,少于5批就生产。
        销售消费每批产品,只要有就消费,没有就只有等工厂生产。

        public static readonly object obj = new object();
        public static Queue<int> buffer = new Queue<int>();//队列排队,先进先出
        public static int maxSize = 5;//最大仓库

        private static void Main(string[] args)
        {
            Thread producerThread = new Thread(Producer);
            Thread consumerThread = new Thread(Consumer);
            producerThread.Start();
            consumerThread.Start();

            producerThread.Join();
            consumerThread.Join();
            Console.ReadKey();
        }

        private static void Producer()//工厂
        {
            Random r = new Random();
            while (true)
            {
                lock (obj)
                {
                    if (buffer.Count >= maxSize)
                    {
                        Console.WriteLine("仓库已满,工厂等待消费...");
                        Monitor.Wait(obj);
                    }
                    int item = r.Next(100);
                    buffer.Enqueue(item);
                    Console.WriteLine($"工厂生成出一批产品:{item}个===========仓库共{buffer.Count}批.");
                    Monitor.PulseAll(obj);
                }
                Thread.Sleep(r.Next(1000));
            }
        }

        private static void Consumer()//消费
        {
            Random r = new Random();
            while (true)
            {
                lock (obj)
                {
                    if (buffer.Count == 0)
                    {
                        Console.WriteLine("消费完毕,等待生产...");
                        Monitor.Wait(obj);
                    }

                    int item = buffer.Dequeue();
                    Console.WriteLine($"消费了一批产品:{item}个");
                    Monitor.PulseAll(obj);
                }
                Thread.Sleep(r.Next(1000));
            }
        }


        程序会一直执行下去,维持着仓库5批货物的标准,如同现在工厂一样,生产与销售同时进行。
        结果:


        可以看到,满5就停止生产。
        但为什么有些满5没有停止生产?
        因为此时正好消费者消费了一批,到了工厂lock生产时,就变成了4于是它就会再生产。
        
        
    
 


http://www.niftyadmin.cn/n/5032859.html

相关文章

数据分析的概念

一、数据分析的目的&#xff1a;把隐藏在一大批看来杂乱无章的数据中的信息集中和提炼出来&#xff0c;从而找出研究对象的内在规律。&#xff08;主要在于分析目的及过滤脏数据&#xff09; 1.数据分析是有组织有目的地收集数据、分析数据&#xff0c;使之成为信息的过程。&a…

天然气跟踪监管系统具体实现

物资跟踪监管系统具体实现 系统开发环境框架设计&#xff08;1&#xff09;在VS2017中创建一个项目工程&#xff08;2&#xff09;在web目录下新建一个index.htm页面&#xff0c;② 与前端界面和操作相关框架文件③ 自定义文件。 物资跟踪监管系统基于Leaflet开发库实现&#x…

封装统一响应结果类和消息枚举类

在开发中&#xff0c;响应结果都需要统一格式&#xff0c;下面给出一个例子&#xff0c;可自行修改。 package com.lili.utils;import com.fasterxml.jackson.annotation.JsonInclude; import com.lili.enums.AppHttpCodeEnum;import java.io.Serializable;/*** author YLi_Ji…

百度SEO优化TDK介绍(分析下降原因并总结百度优化SEO策略)

TDK是SEO优化中很重要的部分&#xff0c;包括标题&#xff08;Title&#xff09;、描述&#xff08;Description&#xff09;和关键词&#xff08;Keyword&#xff09;&#xff0c;为百度提供网页内容信息。其中标题是最重要的&#xff0c;应尽量突出关键词&#xff0c;同时描述…

【微信小程序开发】宠物预约医疗项目实战-注册实现

【微信小程序开发】宠物预约医疗项目实战-注册实现 第二章 宠物预约医疗项目实战-注册实现 文章目录 【微信小程序开发】宠物预约医疗项目实战-注册实现前言一、打开项目文件二、编写wxss代码2.1 什么是wxss2.2 配置主程序全局样式 三. 在sign文件下的wxml文件中编写如下代码并…

HuggingFace Transformer

NLP简介 HuggingFace简介 hugging face在NLP领域最出名&#xff0c;其提供的模型大多都是基于Transformer的。为了易用性&#xff0c;Hugging Face还为用户提供了以下几个项目&#xff1a; Transformers(github, 官方文档): Transformers提供了上千个预训练好的模型可以用于不…

Java手写最长递增子序列算法和最长递增子序列算法应用拓展案例

Java手写最长递增子序列算法和最长递增子序列算法应用拓展案例 1. 算法思维 最长递增子序列算法的实现原理如下&#xff1a; 创建一个长度与原始序列相同的动态规划数组dp&#xff0c;用于记录以每个元素为结尾的最长递增子序列的长度。初始化dp数组的所有元素为1&#xff0…

Linux C/C++实现SSL的应用层VPN (MiniVPN)

SSL协议和VPN&#xff08;虚拟私人网络&#xff09;原理是网络安全领域中的两个重要概念。 SSL协议&#xff0c;全称安全套接层&#xff08;Secure Sockets Layer&#xff09;&#xff0c;是一种广泛应用于互联网的安全协议&#xff0c;主要在两个通信端点之间建立安全连接&am…