I. 务虚
1.1 总体介绍
在Windows平台下可以通过Windows的线程库来实现多核编程,可以利用Win32API或MFC以及.Net Framework提供的接口来实现。实现方式的多样化给Windows编程带来了很大的灵活性,但也使得多线程编程变得复杂。对于多线程的程序可以使用Visual Studio调试工具进行调试,也可以使用多核芯片厂家的线程分析与调试工具进行调试以及优化。
1.2 Windows线程库介绍
Win32 API是Windows操作系统为内核以及应用程序之间提供的借口,将内核提供的功能进行函数封装,应用程序通过调用相关的函数获得相应的系统功能。Win32 API提供了一些列处理线程的函数接口,来向应用程序提供多线程的功能。用Win32 API直接编写应用程序要求程序员对Windows操作系统具有一定了解,否则会占用程序员很多时间来对系统的资源进行管理,降低程序员的工作效率。但直接用Win32 API编写的应用程序,程序的执行代码小,运行效率高。
MFC是微软提供的,称为“微软基础函数类库”(Microsoft Foundation Classes),即用类库的方式将Win32 API进行封装,以类的方式提供给开发者。在MFC类库中,提供了对多线程的支持。由于MFC是在Win32 API基础上进行封装,其基本原理与Win32 API的基本实现原理很类似,且MFC对同步对象进行了封装,因此对用户编程实现来说更加方便。由于MFC具有快速、简介、功能强大等特点,因此深受广大用户的喜爱。
1.3 并行环境、编程语言与编译器
在当前并行计算机上,比较流行的并行环境主要有3类:消息传递、共享存储和数据并行。
3种并行编程环境主要特征一览表:
特征 | 消息传递 | 共享存储 | 数据并行 |
典型代表 | MPI, PVM | OpenMP | HPF |
可移植性 | 所有主流并行计算机 | SMP, DSM | SMP, DSM, MPP |
并行粒度 | 进程级大粒度 | 进程极细粒度 | 进程极细粒度 |
并行操作方式 | 异步 | 异步 | 松散同步 |
数据存储模式 | 分布式存储 | 共享存储 | 共享存储 |
数据分配方式 | 显示 | 隐式 | 半隐式 |
学习入门难度 | 较难 | 容易 | 偏易 |
可扩展性 | 好 | 较差 | 一般 |
1)由上表看出,共享存储并行编程基于线程极细粒度并行,仅被SMP何DSM并行计算机所支持,可移植性不如消息传递并行编程。但是,由于他们支持数据的共享存储,所以并行编程的难度较小,但一般情况下,当处理机个数较多时,其并行性能明显不如消息传递编程。
2)消息传递并行编程基于大粒度的进程级并行,具有最好的可扩展性,几乎被所有当前的各类并行计算机所支持,且具有较好的可扩展性,但是,消息传递并行编程只能支持进程间的分布式存储模式,即各个进程只能直接访问其局部内存空间,而对其他进程的局部内存空间的访问只能通过消息传递来实现。因此,学习和使用消息传递并行编程的难度均大于共享存储和数据并行着两种模式。
在科学计算领域对并行编程支持已取得的相当成功的三项技术:自动并行化、数据并行语言(HPF)、共享存储并行编程接口(OpenMP)。
OpenMP:1997年,有Silicon Graphics领导的工业协会推出了penMP,这是一个与Fortran77和C语言绑定的非正式并行编程接口。协会后来扩展了这些绑定,以引入对Fortran95的支持,目前正在研究用于C++语言的绑定。如同HPF一样,在符合标准的程序中,OpenMP指令在单机编译器上被当做注释而忽略,并且对最终结果没有影响。OpenMP是非常适合于具有一致性访问的共享存储计算机的编程接口,然而它没有向用户提供如何实现对共享存储计算机的非一致性访问,或开发分布存储计算机中局部性的方法。在多处理机工作站机群上,OpenMP通常和MPI同时使用,OpenMP用于节点内,MPI用于节点间的消息传递。当代计算机结合了共享存储并行和分布存储并行两种特征,混合使用OpenMP和MPI指令似乎是支持该类计算机最有前途方式。
1)自动并行化。使用该技术,编译器把串行程序翻译为并行程序。尽管从最终用户的角度来看,这是一种理想的策略,但它尚未获得广泛的接受。不过自动并行化技术是支持其他很多高级策略的基础。
2)数据并行语言。以HPF为例,数据并行语言支持一种从分布存储计算机系统上跨处理器分解数组数据结构而派生来的并行风格。HPF提供一个数据分解指令集,用于提示编译器如何在这样的系统中获得较高的局并行和隐式并行。
3)共享存储并行编程接口。以OpenMP为例,共享存储并行最初关注任务的分解,因为这些接口所应用的理想目标平台是具有一致性访问的全局共享存储。OpenMP是共享存储并行编程接口中最卓越的实力。它使用指令系统说明经在哪里需要使用多线程以及如何给这些多线程分派任务。
1.4 使用Win32 线程API
Win32函数库中提供了操作系统多线程的函数,包括创建线程、管理线程、终止线程、线程同步等接口。
II. 务实
2.1 线程创建
1)线程必须从一个指定的函数开始执行,该函数称为“线程函数”,具有如下原型:
DWORD WINAPI ThreadFunc(LPVOID lpvThreadParm);
该函数的输入参数是一个LPVOID型的参数。该函数返回了一个DWORD型的值。通过这种定义方式,可以定义一个线程函数,还可以将一个线程的运行入口指向这个线程函数。
2) 所有的进程开始时都是只有一个线程,这个线程称为主线程。可以用微软提供的API来创建更多新的线程:
(部分摘自:MSDN)
(部分摘自:《多核程序设计》))
法一:CreateThread
Syntax
C++
HANDLE WINAPI CreateThread( __in_opt LPSECURITY_ATTRIBUTES lpThreadAttributes, __in SIZE_T dwStackSize, __in LPTHREAD_START_ROUTINE lpStartAddress, __in_opt LPVOID lpParameter, __in DWORD dwCreationFlags, __out_opt LPDWORD lpThreadId);
参数:
lpThreadAttributes:指向SECURITY_ATTRIBUTES结构的指针,该结构指定了线程的安全属性,默认为NULL;
dwStackSize:是栈的大小,一般设置为0;
lpStartAddress:是新线程开始执行时,线程函数的入口地址。它必须是将要被新线程执行的函数地址,不能为NULL;
lpParameter:是线程函数定义的参数。指向一个变量的指针。可以通过这个参数传送值,包括指针或者NULL;
dwCreationFlags:控制线程创建的附加标志,可以设置两种值。如果该参数为0.则表示线程在被创建后就会立即开始执行;如果该参数为CREATE_SUSPENDED,则系统产生线程后,该线程处于挂起状态,并不马上执行,直至函数ResumeThread被调用;
lpThreadId:为指向32位变量的指针,该参数接收所创建的线程ID号。如果创建成功则返回线程的句柄,否则返回NULL。
法二:_beginthread
使用process.h头文件中声明的C执行时期连接库函数_beginthread
原型:
uintptr_t _beginthread( void( *start_address )( void * ), unsigned stack_size, void *arglist );
参数解释:
start_address:起止地址的一个例行程序,执行一个新的线程,调用约定是_cdecl或者_clrcall,其实就是一个线程函数的地址;相应的线程函数是: void _cdecl ThreadProc(void* pParam);
stack_size:一个新的线程的堆的大小或者0;
arglist:传给新线程的参数列表,或者是NULL。
返回值:
如果创建成功,每个函数返回一个新