← 返回首页目录
# Excel 多线程重新计算(MTR)全面解析
## 引言
Microsoft Office Excel 2007 是首个采用多线程重新计算(Multithreaded Recalculation,简称 MTR)技术的 Excel 版本。这一革命性的技术进步允许 Excel 在重新计算公式时,根据计算机硬件配置和用户设置,同时使用多达 1024 个线程进行并行计算,极大提升了复杂工作簿的计算效率。值得注意的是,实际可用的线程数量并不受限于计算机的物理处理器或核心数量,系统会根据需要灵活调度。然而,每个线程都会带来相应的操作系统开销,因此合理配置线程数量至关重要——盲目追求最大线程数可能适得其反,导致性能下降而非提升。在拥有多个处理器或多核心处理器的计算机上,操作系统会自动以最高效的方式将线程分配到各个处理器上执行,实现负载均衡。
## Excel MTR 的核心机制
Excel 的多线程重新计算机制建立在对计算链(calculation chain)进行依赖分析的基礎之上。系统会深入解析工作簿中公式单元格之间的依赖关系,识别可并行计算的部分,并将这些独立计算任务分配到不同线程同时执行,从而最大化利用系统资源。
### 基础示例
为理解 MTR 的基本原理,我们可以考察一个简化的计算树模型。在此模型中,`x ← y` 表示 `y` 的计算依赖于 `x` 的值。例如,假设 A1 是基础单元格,A2、A3 依赖于 A1,而 B1、C1 之间相互独立,那么一旦 A1 的计算完成,A2、A3 就可在同一线程上并行计算,同时 B1 和 C1 可在另一线程上独立计算。这种并行计算的前提是所涉及的单元格必须满足线程安全性(thread-safe)要求。
> **线程安全单元格定义**:仅包含线程安全函数(thread-safe functions)的单元格。关于线程安全的具体判定标准,将在后续章节详细阐述。
### 复杂工作簿的优化策略
实际应用中的大多数工作簿,其依赖树远比上述示例复杂。单元格的计算顺序在计算完成前难以确定,且受函数参数的影响变化极大。为此,Excel 迭代采用优化算法,在每次计算中持续改进计算顺序,直至达到无法进一步优化的程度。这种动态优化策略确保了系统能够在复杂的依赖关系中快速识别并行计算机会。
### 主线程的专属职责
虽然 MTR 技术支持多线程并行计算,但 Excel 始终保留一个专门的“主线程”(main thread),负责执行以下任务:
- 处理内部命令和 XLL(Excel Add-In)命令
- 运行 XLL 加载项管理器接口函数(如 xlAutoOpen 等)
- 执行 VBA(Visual Basic for Applications)用户定义命令(通常称为宏)
- 运行 VBA 用户定义函数(User-Defined Functions,UDF)
- 调用内部非线程安全的电子表格函数
- 执行 XLM 宏表用户自定义函数
- 运行 COM 加载项函数与命令
- 处理条件格式表达式中的函数和运算符
- 评估公式中使用的已定义名称表达式
- 处理用户在编辑栏中按 F9 强制计算表达式
在 Excel 默认使用单线程配置时,所有电子表格公式均在此主线程上求值,无论其函数是否线程安全。当用户将配置改为多线程后,额外创建的线程将专门用于处理线程安全的单元格,而主线程仍可根据负载均衡需要参与线程安全单元格的计算。
## Excel 线程安全的界定
明确哪些函数和操作被视为线程安全,是有效利用 MTR 功能的基础。
### 被认为线程安全的内容
根据微软官方文档,Excel 将以下项目归类为线程安全:
1. **所有 Excel 一元和二元运算符**(如加减乘除、取反、连接等)
2. **Excel 2007 及以后版本中几乎所有内置工作表函数**(具体例外见下文)
3. **明确注册为线程安全的 XLL 插件函数**
### 非线程安全的内置函数
以下内置工作表函数因各种原因被列为非线程安全例外:
- **FONÉTICA(PHONETIC)**:处理日文假名注音,其实现涉及全局状态
- **CÉL(CELL)**:当参数为“format”或“address”时,依赖工作表上下文信息
- **INDIRETO(INDIRECT)**:间接引用函数,其求值依赖目标单元格的状态
- **INFODADOSTABELADINÂMICA**:获取数据透视表相关信息
- **MEMBROCUBO、VALORCUBO、PROPRIEDADEMEMBROCUBO、CONJUNTOCUBO、MEMBROCLASSIFICADOCUBO、MEMBROKPICUBO、CONTAGEMCONJUNTOCUBO**:这些多维数据集(OLAP)函数需要访问外部数据源和全局缓存
- **ENDEREÇO(ADDRESS)**:当提供第五个参数(工作表名)时,涉及跨工作表引用
- **所有引用数据透视表的数据库函数**(如 BDSOMA(DSUM)、BDMÉDIA(DAVERAGE)等)——需实时访问数据透视表的缓存结构
- **TIPO.ERRO(ERROR.TYPE)**:其实现依赖全局错误状态
- **HIPERLINK(HYPERLINK)**:涉及外部资源访问和界面交互
### 其他被明确视为非线程安全的内容
以下用户定义的类型均被归类为非线程安全,Excel 不允许它们参与多线程并行计算:
- VBA(Visual Basic for Applications)用户定义函数
- COM(Component Object Model)加载项用户定义函数
- XLM 宏表用户自定义函数
- 未明确注册为线程安全的 XLL 插件函数
## XLL 函数与线程安全实现
XLL 插件是 Excel 中利用 MTR 功能的最佳途径之一,因为只有 XLL 函数才能被显式地注册为线程安全函数。然而,将函数注册为线程安全仅表示开发者向 Excel 承诺该函数能在多线程环境下安全运行,实际确保安全性是开发者的责任,不满足安全性要求的函数可能导致 Excel 崩溃。
### 线程安全函数不能调用的操作
当从已被注册为线程安全的 XLL 函数中调用其他 API 时,以下操作是不允许的,若强行执行则会失败:
1. **调用 XLM 信息函数**(如 xlfGetCell(OBTER.CÉLULA)),失败错误为 `xlretFailed`
2. **调用 xlfSetName(DEF.NOME)** 设置或删除 XLL 内部名称——不允许修改全局名称空间
3. **调用用户定义的非线程安全函数**(通过 `xlUDF` 接口),失败错误为 `xlretNotThreadSafe`
4. **调用 `xlfEvaluate`** 函数计算包含非线程安全函数或包含其定义引用了非线程安全函数的已定义名称
5. **调用 `xlAbort`** 清除中断条件(仅对无参数调用允许)
6. **调用 `xlCoerce`** 获取未计算的单元格引用值——此操作会失败并返回 `xlretUncalced` 错误
此外,XLL 工作表函数不能调用 C API 命令函数(如 xlcSave),无论是否注册为线程安全。由于这些限制,Excel 不允许将宏表等效函数注册为线程安全。
### XLL 开发者的线程安全规则
编写线程安全的 XLL 函数时,开发者必须遵守以下关键规则:
1. **不得调用其他 DLL 中可能非线程安全的资源**
2. **不得通过 C API 或 COM 进行非线程安全的调用**
3. **必须使用临界区(Critical Section)** 保护可能被多个线程同时访问的资源
4. **必须使用线程本地存储(Thread Local Storage,TLS)** 来替代函数中的静态变量——C/C++ 编译器为标准静态变量仅创建一个副本,当多个线程同时访问时会产生数据竞争
### C API 回调函数的线程安全性
幸运的是,以下仅限 C API 的回调函数天然具备线程安全性:
- `xlCoerce`(获取非计算单元格的引用值时除外)
- `xlFree`
- `xlStack`
- `xlSheetId`
- `xlSheetNm`
- `xlAbort`(用于清除中断条件时除外)
- `xlGetInst`
- `xlGetHwnd`
- `xlGetBinaryName`
- `xlDefineBinaryName`
唯一的例外是 `xlSet` 函数,它本质上是命令等效函数,因此不能从任何工作表函数中调用。
## 内存竞争与解决方案
多线程系统必须解决两个基本的内存问题:
1. **如何保护必须由多个线程读写的共享内存**
2. **如何创建和访问线程特有的私有内存**
Windows 操作系统和 Windows SDK 提供了两个核心工具来解决这两个问题:**临界区(Critical Sections)** 和 **线程本地存储(TLS)API**。
- **临界区**可确保多个线程安全地访问共享资源,防止数据竞争
- **TLS 机制**允许每个线程拥有独立的变量副本,从根本上避免了静态变量带来的线程安全问题
一个典型的应用场景是:多个线程需要同时访问项目中的全局变量(如隐藏在对象类中的全局实例)。此时需使用临界区在读写操作前后进行加锁/解锁,确保在同一时间只有单个线程可操作该变量。
另一个常见场景是函数内部声明了静态变量或对象。C/C++ 编译器仅创建单个副本供所有线程使用,这意味着一个线程实例可能修改该值,而另一线程中的实例可能基于已过期的旧值进行计算。解决方案是将此类变量替换为 TLS 变量,使每个线程拥有自己的独立副本。
## MTR 的实际应用与性能优化
### 典型应用场景
任何导出工作表函数的 XLL 都可通过 MTR 功能受益,前提是这些函数不需要执行非线程安全操作。MTR 最具显著性能提升的场景是:工作簿大量调用用户定义函数,而这些 UDF 又调用外部进程获取结果。
### 典型案例分析
考虑一个调用远程服务器的 UDF。如果该服务器能够同时处理多个请求,且工作簿包含对该函数的许多调用,则:
- **单线程重算模式下**:每次 UDF 调用必须先完成,才能发起下一次调用,这严重浪费了服务器并行处理能力
- **多线程重算模式下**:Excel 可同时或快速连续发起多个调用
假设 Excel 配置的线程数等于服务器可处理的并发请求数(设为 N),且工作簿依赖树允许,则总重新计算时间可接近单线程计算时间的 1/N。上述收益即使在客户端计算机只有单处理器的情况下也能实现,尤其是单次调用耗时小于服务器处理时间时。
### 线程数的配置考量
由于每个额外线程都会引入系统开销,因此需针对特定组合(工作簿、服务器和客户端计算机)进行适当测试,以找到最优线程数。
以单处理器计算机运行含 1000 个相互独立的调用远程服务器 UDF 的单元格为例:
- 若服务器可处理 100 个并发请求,Excel 设置为 100 线程,则总运行时间可降至单线程的约 1/100
- 实际优化比例低于理论值,因为线程调度和资源竞争会产生额外开销
- 隐含假设:服务器须具备良好的扩展性,100 个并发任务不会显著影响各任务的完成时间
### 应用领域:蒙特卡洛方法等
在金融建模、风险评估等依赖蒙特卡洛(Monte Carlo)模拟的场景中,MTR 可发挥巨大作用。此类任务通常计算量大且包含大量可拆分的独立子任务,非常适合多线程并行处理,且可进一步扩展到服务器集群中进行大规模并行计算。
## Excel Services 中的相关考虑
Excel Services(Excel 服务)支持在服务器上加载、计算和呈现 Excel 工作簿,用户可通过标准浏览器工具访问和交互。Excel Services 的用户定义函数基于 .NET 托管代码创建,通过 .NET 程序集部署;而 XLL 本身不受支持。
不过,在服务器上托管代码的 UDF 项目可以调用 XLL 来获得其功能。具体方法包括:
1. **创建 .NET 包装程序集**,将参数和返回值从 .NET 托管类型转换为原生数据类型(或反向转换),并调用 XLL 函数
2. **包装器为所需 XLL 函数导出对应的服务器 UDF**
3. **XLL 函数必须为线程安全**——这与客户端 Excel 不同,服务器上无注册机制可验证其安全性,因此确保安全性完全属于 XLL 开发者的责任
当服务器端缺少注册检查手段时,包装器无法强制要求安全性,开发者的合规性就显得更加重要,否则可能导致服务器不稳定或计算错误。
## 结论与展望
Excel 的多线程重算功能是电子表格软件发展史上的重要里程碑,从根本上改变了大型复杂工作簿的计算方式。充分利用 MTR 功能需要理解以下核心要点:
1. **线程配置需平衡性能与开销**——并非线程数越多越好,应根据实际环境和需求测试确定
2. **线程安全是并行计算的前提**——函数必须满足特定安全标准才能参与多线程计算
3. **XLL 插件是发挥 MTR 全部潜力的重要媒介**——它们是唯一能显式注册为线程安全的扩展机制
4. **开发者须严格遵守线程安全规则**——使用临界区和线程本地存储,避免非安全 API 调用
随着数据规模和计算复杂度的不断提升,Excel MTR 技术为处理大规模数据表、复杂金融模型和高频计算任务提供了切实可行的优化方案。理解并善用这一技术,能让 Excel 在实际应用中发挥出数倍的性能潜力,特别是在与外部计算服务配合使用的场景下。
---
**作者:吉祥法师**