.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/
.NET中测量多线程基准性能的更多相关文章
- 使用chrome开发者工具中的network面板测量网站网络性能
前面的话 Chrome 开发者工具是一套内置于Google Chrome中的Web开发和调试工具,可用来对网站进行迭代.调试和分析.使用 Network 面板测量网站网络性能.本文将详细介绍chrom ...
- [转载]ArcGIS Engine 中的多线程使用
ArcGIS Engine 中的多线程使用 原文链接 http://anshien.blog.163.com/blog/static/169966308201082441114173/ 一直都想写 ...
- OS X 和iOS 中的多线程技术(上)
OS X 和iOS 中的多线程技术(上) 本文梳理了OS X 和iOS 系统中提供的多线程技术.并且对这些技术的使用给出了一些实用的建议. 多线程的目的:通过并发执行提高 CPU 的使用效率,进而提供 ...
- 第35节:Java面向对象中的多线程
Java面向对象中的多线程 多线程 在Java面向对象中的多线程中,要理解多线程的知识点,首先要掌握什么是进程,什么是线程?为什么有多线程呢?多线程存在的意义有什么什么呢?线程的创建方式又有哪些?以及 ...
- 【worker】js中的多线程
因为下个项目中要用到一些倒计时的功能,所以就提前准备了一下,省的到时候出现一下界面不友好和一些其他的事情.正好趁着这个机会也加深一下html5中的多线程worker的用法和理解. Worker简介 J ...
- 公司HBase基准性能测试之结果篇
上一篇文章<公司HBase基准性能测试之准备篇>中详细介绍了本次性能测试的基本准备情况,包括测试集群架构.单台机器软硬件配置.测试工具以及测试方法等,在此基础上本篇文章主要介绍HBase在 ...
- C#中的多线程 - 并行编程 z
原文:http://www.albahari.com/threading/part5.aspx 专题:C#中的多线程 1并行编程Permalink 在这一部分,我们讨论 Framework 4.0 加 ...
- C#中的多线程 - 高级多线程 z
原文:http://www.albahari.com/threading/part4.aspx 专题:C#中的多线程 1非阻塞同步Permalink 之前,我们描述了即使是很简单的赋值或更新一个字段也 ...
- C#中的多线程 - 同步基础 z
原文:http://www.albahari.com/threading/part2.aspx 专题:C#中的多线程 1同步概要Permalink 在第 1 部分:基础知识中,我们描述了如何在线程上启 ...
- C#中的多线程 - 多线程的使用 z
原文:http://www.albahari.com/threading/part3.aspx 专题:C#中的多线程 1基于事件的异步模式Permalink 基于事件的异步模式(event-based ...
随机推荐
- Kubernetes 研究笔记
Kubernetes 研究笔记 在接下来的这篇笔记中,我将会介绍 Kubernetes 这一强大的容器编排工具,并学习其基本使用方法.该笔记将会被存储在https://github.com/owlma ...
- 【python基础】日常知识点整理
[三种方法调用] 1. 类中的方法区分为普通方法(self),静态方法(@staticMenthod),类方法@classMenthod,隐式参数(cls) <1> 普通方法:第一个参数 ...
- 多线程的未捕获异常类 UncaughtExceptionHandler 的使用
一.需要 UncaughtExceptionHandler 的原因 1. 主线程可轻松的发现异常,子线程的异常比较隐蔽,难以发现 程序运行时,子线程发生了异常,并不影响主线程,也不会终止主线程的程序, ...
- 【HMS Core】华为帐号服务,获取Access Token报错{sub_error:20152,error_description:invalid code,error:1101}
[问题描述] 华为账号服务,接口获取Access Token报错:{sub_error:20152,error_description:invalid code,error:1101} [问题分析 ...
- 如何通过数据warehouse更好地支持企业数字化转型战略
目录 1. 引言 2. 技术原理及概念 3. 实现步骤与流程 4. 应用示例与代码实现讲解 5. 优化与改进 <如何通过数据 warehouse 更好地支持企业数字化转型战略> 随着企业数 ...
- 让AI支持游戏更自然、更直观:基于情感分析的AI游戏体验和交互设计
目录 引言 随着游戏市场的不断发展,人工智能技术的应用也越来越广泛.其中,情感分析技术在游戏中的应用,可以让游戏更加自然.直观,同时提高游戏的用户体验.本文将介绍如何让 AI 支持游戏更自然.更直观: ...
- LLM技术在自然语言处理中的实践与改进
目录 <LLM技术在自然语言处理中的实践与改进> 引言 自然语言处理 (NLP) 是人工智能领域的一个重要分支,它研究如何将计算机程序与人类语言进行交互,从而理解.分析.生成和翻译文本.近 ...
- Mybatis(Map)
Map 假设,我们的实体类,或者数据库中的表,字段或参数过多,我们应当考虑使用map 创建接口 //万能的mapper,我们不需要知道数据库里面有什么,是一个键值对的表现 //我们只需查询对应的字段 ...
- Elementary OS old version download 旧版本下载
Elementary OS 号称是最漂亮的Linux发行版,没有之一.确实,他的整体风格看起来就是特别舒服,说不出哪里特别好,但也挑不出什么毛病.相比之下,其他Linux的界面总感觉不太和谐.比如特别 ...
- Qt 生成应用程序(二)软件多图标与文件操作
目录 关联某种文件的默认打开方式 assoc ftype 解决方案 设置文件默认图标 应用软件添加多个图标 综合方法 嘿,各位Qt桌面应用开发的同学们(应该Qt大部分应用场景就是这个吧),上一篇文章中 ...