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

在此,我们用多个线程来完成一个计数任务,简单地测量系统的多线程基准性能,以下的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/

.NET中测量多线程基准性能的更多相关文章

  1. 使用chrome开发者工具中的network面板测量网站网络性能

    前面的话 Chrome 开发者工具是一套内置于Google Chrome中的Web开发和调试工具,可用来对网站进行迭代.调试和分析.使用 Network 面板测量网站网络性能.本文将详细介绍chrom ...

  2. [转载]ArcGIS Engine 中的多线程使用

    ArcGIS Engine 中的多线程使用 原文链接 http://anshien.blog.163.com/blog/static/169966308201082441114173/   一直都想写 ...

  3. OS X 和iOS 中的多线程技术(上)

    OS X 和iOS 中的多线程技术(上) 本文梳理了OS X 和iOS 系统中提供的多线程技术.并且对这些技术的使用给出了一些实用的建议. 多线程的目的:通过并发执行提高 CPU 的使用效率,进而提供 ...

  4. 第35节:Java面向对象中的多线程

    Java面向对象中的多线程 多线程 在Java面向对象中的多线程中,要理解多线程的知识点,首先要掌握什么是进程,什么是线程?为什么有多线程呢?多线程存在的意义有什么什么呢?线程的创建方式又有哪些?以及 ...

  5. 【worker】js中的多线程

    因为下个项目中要用到一些倒计时的功能,所以就提前准备了一下,省的到时候出现一下界面不友好和一些其他的事情.正好趁着这个机会也加深一下html5中的多线程worker的用法和理解. Worker简介 J ...

  6. 公司HBase基准性能测试之结果篇

    上一篇文章<公司HBase基准性能测试之准备篇>中详细介绍了本次性能测试的基本准备情况,包括测试集群架构.单台机器软硬件配置.测试工具以及测试方法等,在此基础上本篇文章主要介绍HBase在 ...

  7. C#中的多线程 - 并行编程 z

    原文:http://www.albahari.com/threading/part5.aspx 专题:C#中的多线程 1并行编程Permalink 在这一部分,我们讨论 Framework 4.0 加 ...

  8. C#中的多线程 - 高级多线程 z

    原文:http://www.albahari.com/threading/part4.aspx 专题:C#中的多线程 1非阻塞同步Permalink 之前,我们描述了即使是很简单的赋值或更新一个字段也 ...

  9. C#中的多线程 - 同步基础 z

    原文:http://www.albahari.com/threading/part2.aspx 专题:C#中的多线程 1同步概要Permalink 在第 1 部分:基础知识中,我们描述了如何在线程上启 ...

  10. C#中的多线程 - 多线程的使用 z

    原文:http://www.albahari.com/threading/part3.aspx 专题:C#中的多线程 1基于事件的异步模式Permalink 基于事件的异步模式(event-based ...

随机推荐

  1. 如何开启Apache SkyWalking的自监控?

    1. 开启Prometheus遥测数据 默认情况下, 遥测功能(telemetry)是关闭的(selector 为 none),像这样: telemetry: selector: ${SW_TELEM ...

  2. 【HarmonyOS】【ArkTS】如何使用HTTP网络请求获取动态数据刷新UI界面

    ​ [关键字] HttpRequest.ArkTS.网络数据请求.@ohos.net.http [前言] 在使用ArkTS开发HarmonyOS应用时,需要调用HTTP网络请求 @ohos.net.h ...

  3. ModelBox实战开发:RK3568实现摄像头虚拟背景

    摘要:本文将使用ModelBox端云协同AI开发套件(RK3568)实现摄像头虚拟背景AI应用的开发. 本文分享自华为云社区<ModelBox开发案例 - RK3568实现摄像头虚拟背景[玩转华 ...

  4. js修改css样式

    修改宽度: document.getElementById("div1").style.width="200px"; 修改border-radius: docu ...

  5. 【Netty】一个RPC实例

    Netty实现简易RPC调用 总体流程: 客户端发起rpc调用请求,封装好调用的接口名,函数名,返回类型,函数参数类型,函数参数值等属性,将消息发送给服务器. 服务器的handler解析rpc请求,调 ...

  6. 记一次DNS问题排查

    一.问题:域名flow.nzkong.com解析很慢: 排查过程 抓包分析:tcpdump -i eth0 -n -s 500 port domain 1 14:40:44.548553 IP 10. ...

  7. vCenter报错:Vmware vAPI Endpoint

    vCenter报错:Vmware vAPI Endpoint 问题现象: 平台警报1:设备管理运行状况警报 平台警报2:Vmware vAPI Endpoint服务运行状况警报 vcenter版本:v ...

  8. rocketmq-console基本使用

    rocketmq-console基本使用 作用:rocketmq-console是rocketmq的一款可视化工具,提供了mq的使用详情等功能. 一.安装部署 下载rocketmq组件 rocketm ...

  9. ROS的通信机制

    通信机制 节点--执行单元 执行具体任务的进程.独立运行的可执行文件: 不同节点可以使用不同的编程语言,可分布式运行在不同的主机上. 节点在系统中的名称是必须是唯一的. 节点管理器(ROS Maste ...

  10. Spring的Bean标签配置(一)

    Bean标签基本配置 由于配置对象交由Spring来创建 默认情况下它调用的的是类中的无参构造函数,如果没有无参构造函数则不会创建成功 id:唯一标识符号,反射是通过无参构造创建对象的. class: ...