首页 > 其他分享 >.NET中测量多线程基准性能

.NET中测量多线程基准性能

时间:2023-09-17 14:35:24浏览次数:44  
标签:Task 代码 threadCount totalCount 线程 NET 多线程 基准

.NET中测量多线程基准性能

 

多线程基准性能是用来衡量计算机系统或应用程序在多线程环境下的执行能力和性能的度量指标。它通常用来评估系统在并行处理任务时的效率和性能。测量中通常创建多个线程并在这些线程上执行并发任务,以模拟实际应用程序的并行处理需求。

在此,我们用多个线程来完成一个计数任务,简单地测量系统的多线程基准性能,以下的5种测量代码(代码1,代码4,代码5,代码6,代码7)中,都设置了计数器,每一秒计数器的计数量体现了系统的性能。通过对比这些测量方法,可以直观地理解多线程、如何通过多线程充分利用系统性能,以及运行多线程可能存在的瓶颈。

测量方法

先用一个多线程的共享变量自增例子来做多线程基准性能测量:

//代码1:简单的多线程测量多线程基准性能
long totalCount = 0;
int threadCount = Environment.ProcessorCount;

Task[] tasks = new Task[threadCount];
for (int i = 0; i < threadCount; ++i)
{
    tasks[i] = Task.Run(DoWork);
}
while (true)
{
    long t = totalCount;
    Thread.Sleep(1000);
    Console.WriteLine($"{totalCount - t:N0}");
}
void DoWork()
{
    while (true)
    {
        totalCount++;
    }
}

//结果
48,493,031
48,572,321
47,788,843
48,128,734
50,461,679
……

因为在多线程环境中,线程之间的切换会导致一些开销,例如保存和恢复线程上下文的时间。如果上下文切换频繁发生,可能会对性能测试结果产生影响,因此上面的代码根据系统的CPU内核数设定启动测试线程的线程数量,这些线程对一个共享的变量进行自增操作。

有多线程编程经验的人不难看出,上面的代码没有正确地保护共享资源,会出现竞态条件。这可能导致数据不一致,操作顺序不确定,或者无法重现一致的性能结果。我们将用代码展示这种情况。

//代码2:展示出竞态条件的代码
long totalCount = 0;
int threadCount = Environment.ProcessorCount;

Task[] tasks = new Task[threadCount];
for (int i = 0; i < threadCount; ++i)
{
    tasks[i] = Task.Run(DoWork);
}
void DoWork()
{
    while (true)
    {
        totalCount++;
        Console.Write($"{totalCount}"+",");
    }
}
//结果
1,9,10,3,12,13,4,14,15,16……270035,269913,270037,270038,270036,270040,269987,270042,270043……

代码2的运行结果可以看到,由于被不同线程操作,这些线程同时访问和修改totalCount的值,打印出来的totalCount不是顺序递增的。

可见,代码1没有线程同步机制,我们不能准确测量多线程基准性能。
C#中线程的同步方式,比如传统的锁机制(如lock语句、Monitor类、Mutex类、Semaphore类等)通常使用互斥机制来保护共享资源,以确保同一时间只有一个线程可以访问资源,避免竞争条件。这些锁机制会在代码块被锁定期间阻塞其他线程的访问,使得同一时间只有一个线程可以执行被锁定的代码。
这里使用lock锁作为线程同步机制,修正上面的代码,对共享的变量进行保护,避免共享变量同时被多个线程修改。

//代码3:使用lock锁
long totalCount = 0;
int threadCount = Environment.ProcessorCount;
object totalCountLock = new object();

Task[] tasks = new Task[threadCount];
for (int i = 0; i < threadCount; ++i)
{
    tasks[i] = Task.Run(DoWork);
}

void DoWork()
{
    while (true)
    {
        lock (totalCountLock)
        {
            totalCount++;
            Console.Write($"{totalCount}"+",");
        }
    }
}

//结果
1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30……

这时的结果就是顺序输出。

我们用含lock的代码来测量多线程基准性能:

//代码4:运用含lock锁的代码测量多线程基准性能
long totalCount = 0;
int threadCount = Environment.ProcessorCount;
object totalCountLock = new object();

Task[] tasks = new Task[threadCount];
for (int i = 0; i < threadCount; ++i)
{
    tasks[i] = Task.Run(DoWork);
}
while (true)
{
    long t = totalCount;
    Thread.Sleep(1000);
    Console.WriteLine($"{totalCount - t:N0}");
}
void DoWork()
{
    while (true)
    {
        lock (totalCountLock)
        {
            totalCount++;
        }
    }
}

//结果
16,593,517
16,694,824
16,514,421
16,517,431
16,652,867
……

保证多线程环境下线程安全性,还有一种方式是使用原子操作Interlocked。与传统的锁机制(如lock语句等)不同,Interlocked类提供了一些特殊的原子操作,如Increment、Decrement、Exchange、CompareExchange等,用于对共享变量进行原子操作。这些原子操作是直接在CPU指令级别上执行的,而不需要使用传统的阻塞和互斥机制。它通过硬件级别的操作,确保对共享变量的操作是原子性的,避免了竞争条件和数据不一致的问题。
它更适合用于特定的原子操作,而不是用作通用的线程同步机制。

//代码5:运用原子操作的代码测量多线程基准性能
long totalCount = 0;
int threadCount = Environment.ProcessorCount;

Task[] tasks = new Task[threadCount];
for (int i = 0; i < threadCount; ++i)
{
    tasks[i] = Task.Run(DoWork);
}

while (true)
{
    long t = totalCount;
    Thread.Sleep(1000);
    Console.WriteLine($"{totalCount - t:N0}");
}

void DoWork()
{
    while (true)
    {
        Interlocked.Increment(ref totalCount);
    }
}
//结果
37,230,208
43,163,444
43,147,585
43,051,419
42,532,695
……

除了使用互斥锁、原子操作,我们也可以设法对多个线程进行数据隔离。ThreadLocal类提供了线程本地存储功能,用于在多线程环境下的数据隔离。每个线程都会有自己独立的数据副本,被储存在ThreadLocal实例中,每个ThreadLocal可以被对应线程访问到。

//代码6:运用含ThreadLocal的代码测量多线程基准性能
int threadCount = Environment.ProcessorCount;

Task[] tasks = new Task[threadCount];
ThreadLocal<long> count = new ThreadLocal<long>(trackAllValues: true);

for (int i = 0; i < threadCount; ++i)
{
    int threadId = i;
    tasks[i] = Task.Run(() => DoWork(threadId));
}

while (true)
{
    long old = count.Values.Sum();
    Thread.Sleep(1000);
    Console.WriteLine($"{count.Values.Sum() - old:N0}");
}

void DoWork(int threadId)
{
    while (true)
    {
        count.Value++;
    }
}

//结果
177,851,600
280,076,173
296,359,986
296,140,821
295,956,535
……

上面的代码使用了ThreadLocal类,我们也可以自定义一个类,给每个线程创建一个对象作为上下文,代码如下:

//代码7:运用含自定义上下文的代码测量多线程基准性能
int threadCount = Environment.ProcessorCount;

Task[] tasks = new Task[threadCount];
Context[] ctxs = new Context[threadCount];

for (int i = 0; i < threadCount; ++i)
{
    int threadId = i;
    ctxs[i] = new Context();
    tasks[i] = Task.Run(() => DoWork(threadId));
}

while (true)
{
    long old = ctxs.Sum(v => v.TotalCount);
    Thread.Sleep(1000);
    Console.WriteLine($"{ctxs.Sum(v => v.TotalCount) - old:N0}");
}

void DoWork(int threadId)
{
    while (true)
    {
        ctxs[threadId].TotalCount++;
    }
}

class Context
{
    public long TotalCount = 0;
}

//结果:
1,067,502,570
1,100,966,648
1,145,726,019
1,110,163,963
1,069,322,606
……

系统配置

组件规格
CPU 11th Gen Intel(R) Core(TM) i5-11300H
内存 16 GB DDR4
操作系统 Microsoft Windows 10 家庭中文版
电源选项 已设置为高性能
软件 LINQPad 7.8.5 Beta
运行时 .NET 7.0.10

测量结果

测量方法1秒计数性能百分比
未做线程同步 50,461,679 118.6%
lock锁 16,652,867 39.2%
原子操作(Interlocked) 42,532,695 100%
ThreadLocal 295,956,535 695.8%
自定义上下文(Context) 1,069,322,606 2514.1%

结果分析

未作线程同步测量到的结果是不准确的,不能作为依据。

根据程序运行的结果可以看到,使用传统的lock锁机制,效率不高。使用原子操作Interlocked,效率比传统锁要高近2倍。
而实现了线程间隔离的2种方法,效率都比前面的方法要高。使用自定义上下文的程序效率是最高的。

线程间隔离的两种代码,它们主要区别在于线程安全性的实现方式。代码6使用ThreadLocal 类来实现,而代码7使用了自定义的上下文,用一个数组来为每个线程提供一个唯一的上下文。代码6使用的是线程本地存储(Thread Local Storage,TLS)来实现其功能。它是一种全局变量,可以被正在运行的所有线程访问,但每个线程所看到的值都是私有的。虽然这个特性使ThreadLocal在多线程编程中变得非常有用,但为了实现这个特性,它在内部实现了一套复杂的机制,比如它会创建一个弱引用的哈希表来存储每个线程的数据。这个内部实现细节增加了相应的计算和访问开销。

对于代码7,它创建了一个名为Context的类数组,每个线程都有其自己的Context对象,并在执行过程中修改这个对象。由于每个线程自身管理其Context对象,不存在任何线程间冲突,这就减少了许多额外的开销。

因此,虽然代码6代码7都实现了线程数据隔离,但代码7避开了ThreadLocal的额外开销,因此在性能上表现得更好。

结论

如果能实现线程间的隔离,可以大幅提高多线程代码效率,测量出系统的最大性能值。

作者:百宝门-后端组-周智

原文地址:https://blog.baibaomen.com/120-2/

标签:Task,代码,threadCount,totalCount,线程,NET,多线程,基准
From: https://www.cnblogs.com/sexintercourse/p/17708717.html

相关文章

  • 深入了解信号量:多线程同步的得力工具
    随着计算机科学和软件工程的不断发展,多线程编程变得越来越重要。多线程允许程序同时执行多个任务,提高了程序的效率和性能。然而,多线程编程也引入了新的问题,例如竞态条件和数据竞争。为了解决这些问题,同步工具变得至关重要,而信号量是其中一个强大的工具。什么是信号量?信号量是一......
  • Layered Network stack
    3.LayeredNetworkstackModularityDoesnotspecifyanimplementationInstead,tellsushowtoorganizefunctionalityEncapsulationInterfacesdefinecross-layerinteractionLayersonlyrelyonthosebelowthemFlexibilityReuseofcodeacrossth......
  • .NET Core 实现Excel的导入导出
    目录前言NPOI简介一、安装相对应的程序包1.1、在“管理NuGet程序包”中的浏览搜索:“NPOI”二、新建Excel帮助类三、调用3.1、增加一个“keywords”模型类,用作导出3.2、添加一个控制器3.3、编写导入导出的控制器代码3.3.1、重写“Close”函数3.3.2、添加控制器代码3.3.3、Excel......
  • 前端请求参数加密、.NET 后端解密
    本文详细介绍了前端请求参数加密、.NET后端解密,文章较长,请各位看官耐心看完。目录一、前端使用“CryptoJS”,前端AES加密,.NET后端AES解密1.1、加密解密效果图1.2、CryptoJS介绍1.3、准备工作:安装“CryptoJS”1.3.1、使用npm进行安装1.3.2、VisualStudio中安装1.3.2.1、选择“客......
  • 图解几种常见 Kubernetes Pod 驱逐场景
    图解几种常见KubernetesPod驱逐场景sysdig 奇妙的Linux世界 2023-09-1708:20 发表于重庆 1人听过收录于合集#云原生263个#Kubernetes280个#Docker203个#开源461个公众号关注 「奇妙的Linux世界」设为「星标」,每天带你玩转Linux! KubernetesPod被......
  • Java多线程学习(Day01)
    目录线程简介线程实现(重点)线程状态线程同步(重点)线程通信问题进程与线程概念                                     --来自百度百科的解释:        进程(Process)是......
  • ASP.NET MVC5多语言切换快速实现方案
    实现动态切换语言,Demo做了三种语言库可以切换,包括资源文件的定义,实体对象属性设置,后台代码Controller,IAuthorizationFilter,HtmlHelper的实现,做法比较简单易学易用,配合我之前发布的# MVCScaffoldingSmartCode-Engine更新 模板中新增了多语言资源文件的生成功能,发现......
  • NetCore 国际化最佳实践
    NetCore国际化最佳实践ASP.NETCore中提供了一些本地化服务和中间件,可将网站本地化为不同的语言文化。ASP.NETCore中我们可以使用Microsoft.AspNetCore.Localization库来实现本地化。但是默认只支持使用资源文件方式做多语言存储,很难在实际场景中使用。有没有可能支持官方资源......
  • 使用Jenkins自动部署.NET站点
    Jenkins安装:参考文章【Jenkins安装】部署.NET站点1、新建项目1,1、Dashboard页面菜单>点击新建Item>输入名称"TestWebServiceDeploy",并且选择【Freestyleproject】风格  2、配置 2.1、描述和日志确定后可以配置要自动化发布的项目。首先可以填写描述,描述可以根......
  • Net6+Consul的简单使用
    先下载Consul打开cmdconsul.exeagent-dev运行可以在环境变量PATH下添加当前Consul.exe路径全局使用下面试启动ok界面,然后通过localhost:8500看到界面了 然后创建两个api程序,添加如下代码,[HttpGet("start")]publicvoidStart(){......