轻松进行多核处理器编程





0/5 (0投票)
多核处理器正变得无处不在,但由于多线程编程的复杂性,很少有程序员能够充分利用它们。Jibu 是一个适用于 .NET、C++、Java 和 Delphi 的库,可以使专家和初学者都能轻松进行并发和并行编程。
这是我们对 The Code Project 赞助商的展示性评测。这些评测旨在为您提供我们认为对开发人员有用且有价值的产品和服务信息。
多核计算机正变得无处不在,几乎所有新的笔记本电脑、台式机或服务器都配备了多个核心。从单核到多核的转变主要是由于处理器难以扩展到更高的时钟速度,但这对编程界的影响是巨大的。新一代处理器自动加速现有应用程序以更快运行的日子已经一去不复返了——免费午餐已经结束。
另一方面,通过在芯片上增加额外的核心所带来的前所未有的计算能力也为应用程序开发人员提供了新的机会,如果他们能够在其应用程序中驾驭多个核心的威力。有许多原因促使开发人员考虑编写能够利用多核的应用程序
- 更快或更好地完成工作。在多媒体、人工智能、优化、科学计算、游戏编程、数据挖掘等计算密集型领域,充分利用所有可用电源的应用程序将更快地完成工作,或在相同时间内提供更好的结果。
- 在嵌入式系统、机器人、GUI 应用程序等需要同时运行大量任务的领域,利用多核将产生更具响应性和更有效的系统。
- 在目前不需要额外电源的应用程序中,使用多核机器上可用的额外处理能力,可能会实现新功能。
- 竞争优势。当您的客户知道他们购买的应用程序不仅能够很好地运行在当今的双核或四核机器上,而且还能很好地扩展到未来的 16 核机器时,他们会感到安心。
多线程的障碍
编写能够利用多核的程序的标准方法是多线程。多线程以其复杂且容易出错的声誉而闻名,与普通的单线程编程相比,这种声誉是名副其实的。死锁、活锁和数据争用等问题仅在多线程程序中发生,并且可能极难识别、重现和修复。
同时,多线程编程通常使用一套非常底层的构造来管理线程、确保对数据的独占访问以及同步不同线程。这些构造甚至没有标准化,这意味着多线程代码很难移植到不同的平台。
介绍 Jibu
Jibu 是一个适用于 .NET、Java、C++ 和 Delphi 的库,可以使多线程编程变得简单,无论您是专家还是初学者。
它同样适用于机器人和嵌入式系统等并发应用程序,以及对于必须充分利用计算机全部处理能力的传统并行程序。
Jibu 让程序员专注于想做什么,而不是怎么做。在大多数情况下,程序员无需考虑线程、监视器、锁、互斥锁、信号量、条件变量和信号等概念,而是可以使用少数几个高级 Jibu 构造来完成相同的任务,编写的代码更少,错误也更少。
Jibu 的核心是一个先进的工作窃取调度程序,它能在运行时确保可用核心之间的动态负载均衡。程序员永远不会直接与 Jibu 调度程序打交道,而是可以专注于指定要执行的任务。任务可以通过通道或邮箱交换数据并进行同步。在最高层,Jibu 提供了易于实现的并行 for 循环和归约操作的构造。
使程序并行化
让应用程序利用并发的最佳方法是完全重写它。这需要大量的时间和精力,许多公司选择逐步迁移代码库以支持多核。Jibu 中的高级构造使得通过使用 For、ForEach 和 Reduce 构造将并发性引入现有顺序程序变得非常容易。为了说明并行 for 循环,请看下面的 C# 代码片段
// Multiply square matrices
public static double[,] SeqMult(double[,] m1, double[,] m2)
{
int size = m1.GetLength(0);
double[,] m3 = new double[size, size];
for (int i = 0; i < size; i++)
for (int j = 0; j < size; j++)
for (int k = 0; k < size; k++)
m3[i,j] += m1[i, k] * m2[k, j];
return m3;
}
该代码使用三个嵌套的 for 循环来计算两个方阵的乘积。在四核机器上,这段代码只会利用一个核心,导致性能低下。使用 Jibu 的 for 循环,相应的代码如下所示:
// Multiply square matrices
public static double[,] ParMult(double[,] m1, double[,] m2)
{
int size = m1.GetLength(0);
double[,] m3 = new double[size, size];
Parallel.For(0, size, delegate(int i)
{
for (int j = 0; j < size; j++)
for (int k = 0; k < size; k++)
m3[i,j] += m1[i, k] * m2[k, j];
});
return m3;
}
请注意,最外层的 for 循环被替换为 Jibu 的 Parallel.For,它接受一个委托——其余代码与顺序版本相同。那么性能如何呢?下图显示了矩阵乘法顺序版本和并行版本的执行时间。测试是在四核机器上进行的。随着矩阵尺寸的增加,使用 Jibu 获得的加速也随之增加。通过替换一行代码获得超过 3.6 的加速并不算差。
基于任务的并行处理和 Jibu 调度程序
任务是使用 Jibu 编写程序的核心概念。Jibu 中有两种类型的任务,分别称为 Async
和 Future
。Async
和 Future
之间唯一的区别是 Future
完成时会返回值,而 Async
则不会。Async
看起来是这样的:
class DemoTask : Async
{
public override void Run()
{
for (int i = 0; i < 100000; i++) ;
//Do something
}
}
static void Main(string[] args)
{
DemoTask[]tasks = new DemoTask[1000000];
for (int i = 0; i < tasks.Length; i++)
tasks[i] = new DemoTask();
Parallel.Run(tasks);
}
这个小示例创建了一个名为 DemoTask
的 Async
任务,该任务执行一个 for 循环。Main
方法创建了数百万个新的 DemoTask
,并使用 Jibu 的 Parallel.Run
构造来执行它们。请注意,代码中没有使用线程和同步构造,但这段代码的执行速度与更复杂的基于线程的程序一样快,甚至更快。
任务调度程序会持续监控待执行任务的数量、当前正在执行任务的线程数、阻塞的任务数以及系统中可用的核心数。
所有信息都会被分析,并在必要时启动调度程序,以确保负载均衡、线程数量最佳以及程序响应迅速。Jibu 调度程序与 Jibu 线程池协同工作,以确保在需要时启动线程,并在不再需要时停止线程。
如果在双核机器上运行包含 100 万个 DemoTask
的示例程序,则只会创建两个线程来执行任务;而在八核机器上运行该程序,则会创建 8 个线程。
任务通信
Jibu 相较于标准线程和其他高级库的一个最大优势是,它能够以极高的简洁性实现不同任务之间的数据交换、事件协调和相互同步。
只需三种不同的 Jibu 构造(Channel、MailBox 和 Choice)即可编写具有任意复杂通信和同步模式的并发程序。
每个任务都有一个集成的邮箱,其他任务可以将任何类型的数据放入其中,而无需担心同步或锁定。邮箱是缓冲的,可以轻松实现任务之间的一对一通信。
Channel 抽象了低级同步机制,并为多个任务之间的数据交换提供了一种极其简单的方式。无需关心传统的锁、互斥锁、信号量、临界区或条件变量等构造。
Choice 构造可以轻松地协调多个任务之间的通信。Choice 既支持公平通信,也支持优先级通信,消除了饥饿的风险。它还确保 CPU 周期不会在等待特定事件发生时被浪费。
下面展示了使用任务、通道、选择和邮箱的 Jibu 程序的结构
有关邮箱、通道和选择的更详细描述,请访问 Jibu 文档中心。
Jibu 支持多平台
如果您曾不幸地需要将并发或并行应用程序从一种编程语言移植到另一种语言,或者从一个操作系统移植到另一个操作系统,您就会知道,尽管线程和同步构造在大多数平台和大多数编程语言中都可用,但它们在语法和语义方面却大相径庭。
Jibu 目前支持 .NET 2.0 或更高版本、Java 5.0 或更高版本、Windows 版 C++ 和 Windows 版 Delphi 2007。Linux 和 Solaris 版 C++ 支持正在开发中,并且很可能支持其他语言。
Jibu API 在支持的语言和平台之间尽可能统一,这意味着 Jibu 应用程序的并发和并行部分可以轻松地移植到其他 Jibu 支持的语言和平台。
统一 API 的另一个优点是,开发人员可以在一种语言中进行原型开发,然后轻松地移植到其他语言。例如,精通 C# 的开发人员可以对 Jibu 应用程序进行原型开发,然后将其移植到 C++、Delphi 和 Java,而无需了解这些语言的原生线程构造。
下载 Jibu
Jibu 的免费试用版下载仅供非商业用途,可在下载中心获取。