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

在此,我们用多个线程来完成一个计数任务,简单地测量系统的多线程基准性能,以下的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. 代码随想录算法训练营Day53 动态规划

    代码随想录算法训练营 代码随想录算法训练营Day53 动态规划|●  1143.最长公共子序列 1035.不相交的线 53. 最大子序和 动态规划 1143.最长公共子序列 题目链接:1143.最长公 ...

  2. Windows系统中,如何快速找到端口被占用的进程?

    在本地调试代码时,经常遇到端口被占用导致启动失败的问题,又不能很快找到哪个进程占用了端口,很是恼火. 今天,我们用shell命令轻松搞定. 一.打开命令提示符 window+R 组合键,调出命令窗口. ...

  3. Bash 编程

    原文:https://seankross.com/the-unix-workbench/bash-programming.html[1] 数学 创建math.sh: #!/usr/bin/env ba ...

  4. Galaxy Release_20.09 发布,新增多个数据上传组件

    Galaxy Project(https://galaxyproject.org/)是在云计算背景下诞生的一个生物信息学可视化分析开源项目. 该项目由美国国家科学基金会(NSF).美国国家人类基因组研 ...

  5. 在 RedHat 使用 gdc-client 下载 TCGA 数据

    今天,只聊一下 RedHat/CentOS 下 gdc-client 安装的那些事. gdc-client,官网地址:https://gdc.cancer.gov/access-data/gdc-da ...

  6. 用R来分析洛杉矶犯罪

    由于微信不允许外部链接,你需要点击文章尾部左下角的 "阅读原文",才能访问文中链接. 洛杉矶市(Los Angeles)或"爵士乐的诞生地(The Birthplace ...

  7. Python运维开发之路《编程》

    一.编程思想介绍 1.编程范式 编程是程序员用特定的语法+数据结构+算法组成的代码来告诉计算机如何执行任务的过程,一个程序是程序员为了得到一个任务结果而编写的一组指令的集合,正所谓条条大路通罗马,实现 ...

  8. Java反射源码学习之旅

    1 背景 前段时间组内针对"拷贝实例属性是应该用BeanUtils.copyProperties()还是MapStruct"这个问题进行了一次激烈的battle.支持MapStru ...

  9. 关于 axios 是什么?以及怎么用?

    〇.前言 Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 Node.js 中.简单的讲就是可以发送 Get.Post 请求. 诸如 Vue.React.Angular 等前 ...

  10. 如何将Maven项目快速改造成一个java web项目(方式二)

    原始的maven项目,使用IDEA打开后,目录结构如下所示 删除pom.xml文件,删除resource目录,将java目录下的代码放到项目根目录下, 将webapp目录放到项目根目录下.如下图所示 ...