C#多线程系列之线程等待

 更新时间:2022年02月14日 09:12:36   作者:痴者工良  
本文详细讲解了C#多线程中的线程等待,文中通过示例代码介绍的非常详细。对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

前言

volatile 关键字

volatile 关键字指示一个字段可以由多个同时执行的线程修改。

我们继续使用《C#多线程(3):原子操作》中的示例:

        static void Main(string[] args)
        {
            for (int i = 0; i < 5; i++)
            {
                new Thread(AddOne).Start();
            }
            Thread.Sleep(TimeSpan.FromSeconds(5));
            Console.WriteLine("sum = " + sum);
            Console.ReadKey();
        }
        private static int sum = 0;
        public static void AddOne()
        {
            for (int i = 0; i < 100_0000; i++)
            {
                sum += 1;
            }
        }

运行后你会发现,结果不为 500_0000,而使用 Interlocked.Increment(ref sum);后,可以获得准确可靠的结果。

你试试再运行下面的示例:

        static void Main(string[] args)
        {
            for (int i = 0; i < 5; i++)
            {
                new Thread(AddOne).Start();
            }
            Thread.Sleep(TimeSpan.FromSeconds(5));
            Console.WriteLine("sum = " + sum);
            Console.ReadKey();
        }
        private static volatile int sum = 0;
        public static void AddOne()
        {
            for (int i = 0; i < 100_0000; i++)
            {
                sum += 1;
            }
        }

你以为正常了?哈哈哈,并没有。

volatile 的作用在于读,保证了观察的顺序和写入的顺序一致,每次读取的都是最新的一个值;不会干扰写操作。

详情请点击:https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/volatile

其原理解释:https://theburningmonk.com/2010/03/threading-understanding-the-volatile-modifier-in-csharp/

三种常用等待

这三种等待分别是:

Thread.Sleep();
Thread.SpinWait();
Task.Delay();

Thread.Sleep(); 会阻塞线程,使得线程交出时间片,然后处于休眠状态,直至被重新唤醒;适合用于长时间的等待;

Thread.SpinWait(); 使用了自旋等待,等待过程中会进行一些的运算,线程不会休眠,用于微小的时间等待;长时间等待会影响性能;

Task.Delay(); 用于异步中的等待,异步的文章后面才写,这里先不理会;

这里我们还需要继续 SpinWait 和 SpinLock 这两个类型,最后再进行总结对照。

再说自旋和阻塞

前面我们学习过自旋和阻塞的区别,这里再来撸清楚一下。

线程等待有内核模式(Kernel Mode)和用户模式(User Model)。

因为只有操作系统才能控制线程的生命周期,因此使用 Thread.Sleep() 等方式阻塞线程,发生上下文切换,此种等待称为内核模式。

用户模式使线程等待,并不需要线程切换上下文,而是让线程通过执行一些无意义的运算,实现等待。也称为自旋。

SpinWait 结构

微软文档定义:为基于自旋的等待提供支持。

SpinWait 是结构体;Thread.SpinWait() 的原理就是 SpinWait 。
如果你想了解 Thread.SpinWait() 是怎么实现的,可以参考 https://www.tabsoverspaces.com/233735-how-is-thread-spinwait-actually-implemented

线程阻塞是会耗费上下文切换的,对于过短的线程等待,这种切换的代价会比较昂贵的。在我们前面的示例中,大量使用了 Thread.Sleep() 和各种类型的等待方法,这其实是不合理的。

SpinWait 则提供了更好的选择。

属性和方法

老规矩,先来看一下 SpinWait 常用的属性和方法。

属性:

属性说明
Count获取已对此实例调用 SpinOnce() 的次数。
NextSpinWillYield获取对 SpinOnce() 的下一次调用是否将产生处理器,同时触发强制上下文切换。

方法:

方法说明
Reset()重置自旋计数器。
SpinOnce()执行单一自旋。
SpinOnce(Int32)执行单一自旋,并在达到最小旋转计数后调用 Sleep(Int32) 。
SpinUntil(Func)在指定条件得到满足之前自旋。
SpinUntil(Func, Int32)在指定条件得到满足或指定超时过期之前自旋。
SpinUntil(Func, TimeSpan)在指定条件得到满足或指定超时过期之前自旋。

自旋示例

下面来实现一个让当前线程等待其它线程完成任务的功能。

其功能是开辟一个线程对 sum 进行 +1,当新的线程完成运算后,主线程才能继续运行。

    class Program
    {
        static void Main(string[] args)
        {
            new Thread(DoWork).Start();

            // 等待上面的线程完成工作
            MySleep();

            Console.WriteLine("sum = " + sum);
            Console.ReadKey();
        }

        private static int sum = 0;
        private static void DoWork()
        {
            for (int i = 0; i < 1000_0000; i++)
            {
                sum++;
            }
            isCompleted = true;
        }

        // 自定义等待等待
        private static bool isCompleted = false;
        private static void MySleep()
        {
            int i = 0;
            while (!isCompleted)
            {
                i++;
            }
        }
    }

新的实现

我们改进上面的示例,修改 MySleep 方法,改成:

        private static bool isCompleted = false;        
        private static void MySleep()
        {
            SpinWait wait = new SpinWait();
            while (!isCompleted)
            {
                wait.SpinOnce();
            }
        }

或者改成

        private static bool isCompleted = false;        
        private static void MySleep()
        {
            SpinWait.SpinUntil(() => isCompleted);
        }

SpinLock 结构

微软文档:提供一个相互排斥锁基元,在该基元中,尝试获取锁的线程将在重复检查的循环中等待,直至该锁变为可用为止。

SpinLock 称为自旋锁,适合用在频繁争用而且等待时间较短的场景。主要特征是避免了阻塞,不出现昂贵的上下文切换。

笔者水平有限,关于 SpinLock ,可以参考 https://www.c-sharpcorner.com/UploadFile/1d42da/spinlock-class-in-threading-C-Sharp/

另外,还记得 Monitor 嘛?SpinLock 跟 Monitor 比较像噢~https://www.jb51.net/article/237307.htm

在《C#多线程(10:读写锁)》中,我们介绍了 ReaderWriterLock 和 ReaderWriterLockSlim ,而 ReaderWriterLockSlim 内部依赖于 SpinLock,并且比 ReaderWriterLock 快了三倍。

属性和方法

SpinLock 常用属性和方法如下:

属性:

属性说明
IsHeld获取锁当前是否已由任何线程占用。
IsHeldByCurrentThread获取锁是否已由当前线程占用。
IsThreadOwnerTrackingEnabled获取是否已为此实例启用了线程所有权跟踪。

方法:

方法说明
Enter(Boolean)采用可靠的方式获取锁,这样,即使在方法调用中发生异常的情况下,都能采用可靠的方式检查 lockTaken 以确定是否已获取锁。
Exit()释放锁。
Exit(Boolean)释放锁。
TryEnter(Boolean)尝试采用可靠的方式获取锁,这样,即使在方法调用中发生异常的情况下,都能采用可靠的方式检查 lockTaken 以确定是否已获取锁。
TryEnter(Int32, Boolean)尝试采用可靠的方式获取锁,这样,即使在方法调用中发生异常的情况下,都能采用可靠的方式检查 lockTaken 以确定是否已获取锁。
TryEnter(TimeSpan, Boolean)尝试采用可靠的方式获取锁,这样,即使在方法调用中发生异常的情况下,都能采用可靠的方式检查 lockTaken 以确定是否已获取锁。

示例

SpinLock 的模板如下:

        private static void DoWork()
        {
            SpinLock spinLock = new SpinLock();
            bool isGetLock = false;     // 是否已获得了锁
            try
            {
                spinLock.Enter(ref isGetLock);
                // 运算
            }
            finally
            {
                if (isGetLock)
                    spinLock.Exit();
            }
        }

这里就不写场景示例了。

需要注意的是, SpinLock 实例不能共享,也不能重复使用。

等待性能对比

大佬的文章,.NET 中的多种锁性能测试数据:http://kejser.org/synchronisation-in-net-part-3-spinlocks-and-interlocks/

这里我们简单测试一下阻塞和自旋的性能测试对比。

我们经常说,Thread.Sleep() 会发生上下文切换,出现比较大的性能损失。具体有多大呢?我们来测试一下。(以下运算都是在 Debug 下测试)

测试 Thread.Sleep(1)

        private static void DoWork()
        {
            Stopwatch watch = new Stopwatch();
            watch.Start();
            for (int i = 0; i < 1_0000; i++)
            {
                Thread.Sleep(1);
            }
            watch.Stop();
            Console.WriteLine(watch.ElapsedMilliseconds);
        }

笔者机器测试,结果大约 20018。Thread.Sleep(1) 减去等待的时间 10000 毫秒,那么进行 10000 次上下文切换需要花费 10000 毫秒,约每次 1 毫秒。

上面示例改成:

            for (int i = 0; i < 1_0000; i++)
            {
                Thread.Sleep(2);
            }

运算,发现结果为 30013,也说明了上下文切换,大约需要一毫秒。

改成 Thread.SpinWait(1000)

            for (int i = 0; i < 100_0000; i++)
            {
                Thread.SpinWait(1000);
            }

结果为 28876,说明自旋 1000 次,大约需要 0.03 毫秒。

到此这篇关于C#多线程系列之线程等待的文章就介绍到这了。希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

相关文章

  • C#多线程与异步的区别详解

    C#多线程与异步的区别详解

    多线程和异步操作两者都可以达到避免调用线程阻塞的目的,从而提高软件的可响应性。甚至有些时候我们就认为多线程和异步操作是等同的概念。但是,多线程和异步操作还是有一些区别的。而这些区别造成了使用多线程和异步操作的时机的区别
    2017-06-06
  • C#抓取网页数据 解析标题描述图片等信息 去除HTML标签

    C#抓取网页数据 解析标题描述图片等信息 去除HTML标签

    本文主要一步一步介绍利用C#抓取页面数据的过程,抓取HTML,获取标题、描述、图片等信息,并去除HTML,希望对大家有所帮助。
    2016-04-04
  • C#后台调用WebApi接口的实现方法

    C#后台调用WebApi接口的实现方法

    本文主要介绍了C#后台调用WebApi接口的实现方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-06-06
  • 基于C#实现的HOOK键盘钩子实例代码

    基于C#实现的HOOK键盘钩子实例代码

    这篇文章主要介绍了基于C#实现的HOOK键盘钩子实例,需要的朋友可以参考下
    2014-07-07
  • automation服务器不能创建对象 解决方法

    automation服务器不能创建对象 解决方法

    本文主要介绍如何解决“automation服务器不能创建对象”错误,从而解决Visual Studio.Net不能正常使用的问题,需要的朋友可以参考下。
    2016-06-06
  • 利用C#实现合并Word文档功能

    利用C#实现合并Word文档功能

    合并Word文档可以快速地将多份编辑好的文档合在一起,避免复制粘贴时遗漏内容,以及耗费不必要的时间。本文将分为以下两部分介绍如何通过C#合并Word文档,并附上VB.NET代码供大家参考,希望对大家有所帮助
    2022-12-12
  • C#实现把图片转换成二进制以及把二进制转换成图片的方法示例

    C#实现把图片转换成二进制以及把二进制转换成图片的方法示例

    这篇文章主要介绍了C#实现把图片转换成二进制以及把二进制转换成图片的方法,结合具体实例形式分析了基于C#的图片与二进制相互转换以及图片保存到数据库的相关操作技巧,需要的朋友可以参考下
    2017-06-06
  • C#将字母或数字加密成字母的方法

    C#将字母或数字加密成字母的方法

    这篇文章主要介绍了C#将字母或数字加密成字母的方法,涉及C#操作字符串的技巧,具有一定参考借鉴价值,需要的朋友可以参考下
    2015-03-03
  • C# wpf简单颜色板的实现

    C# wpf简单颜色板的实现

    wpf本身没有提供颜色板之类的控件,有些业务使用场景需要使用颜色板之类的控件,本文就简单实现,感兴趣的可以了解一下
    2021-10-10
  • C#中感叹号(!) 的作用总结

    C#中感叹号(!) 的作用总结

    这篇文章主要给大家总结介绍了C#中感叹号(!) 的作用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-12-12

最新评论