多线程详解 - 图文

更新时间:2024-01-06 11:54:02 阅读量: 教育文库 文档下载

说明:文章内容仅供预览,部分内容可能不全。下载后的文档,内容与下面显示的完全一致。下载之前请确认下面内容是否您想要的,是否完整无缺。

第13章 多线程与多核编程

多任务的并发执行会用到多线程(multithreading),而CPU的多核(mult-core)化又将原来只在巨型机中才使用的并行计算(parallel computing)带入普通PC应用的多核程序设计(multi-core programming)中。

13.1 进程与线程

进程(process)是执行中的程序,线程(thread)是一种轻量级的进程。

13.1.1 进程与多任务

现代的操作系统都是多任务(multitask)的,即可同时运行多个程序。进程(process)是位于内存中正被CPU运行的可执行程序。参见图15-1。

进程(内存中)

程序 = 运行

可执行文件(磁/U/光盘上) 图15-1 程序与进程

目前的主流计算机采用的都是冯·诺依曼(John von Neumann)体系结构——存储程序

计算模型,程序(program)就是在内存中顺序存储并以线性模式在CPU中串行执行的指令序列。对于传统的单核CPU计算机,多任务操作系统的实现是通过CPU分时(time-sharing)和程序并发(concurrency)完成的。即在一个时间段内,操作系统将CPU分配给不同的程序,虽然每一时刻只有一个程序在CPU中运行,但是由于CPU的速度非常快,在很短的时间段中可在多个进程间进行多次切换,所以用户的感觉就像多个程序在同时执行,我们称之为多任务的并发。

13.1.2 进程与线程

程序一般包括代码段、数据段和堆栈,对具有GUI(Graphical User Interfaces,图形用户界面)的程序还包含资源段。进程(process)是应用程序的执行实例,即正在被执行的程序。每个进程都有自己的虚拟地址空间,并拥有操作系统分配给它的一组资源,包括堆栈、寄存器状态等。

线程(thread)是CPU的调度单位,是进程中的一个可执行单元,是一条独立的指令执行路径。线程只有一组CPU指令、一组寄存器和一个堆栈,它本身没有其他任何资源,而是与拥有它的进程共享几乎一切,包括进程的数据、资源和环境变量等。线程的创建、维护和管理给操作系统的负担比进程要轻得多,所以才叫轻量级的进程(lightweight process)。

一个进程可以拥有多个线程,而一个线程只能属于一个进程。每个进程至少包含一个线程——主线程,它负责程序的初始化工作,并执行程序的起始指令。随后,主线程可为执行各种不同的任务而分别创建多个子线程。

一个程序的多个运行,可以通过启动该程序的多个实例(即多个进程)来完成,也可以

1

只运行该程序的一个实例(一个进程),而由该进程创建多个线程来做到。显然后者要比前者更高效,更能节约系统的有限资源。这对需要在同一时刻响应成千上万个用户请求的Web服务器程序和网络数据库管理程序等来说是至关重要的。

多线程图示

其中:A为主线程,B、C、D皆为A的子线程 不同并行任务中的同名子线程可以互不相同

有关进程和线程的进一步内容,大家会在将来的操作系统课程中学到。

13.1.3 多线程编程的困难

因为同一程序(进程)的多个线程共享同样的数据和资源,所以会出现同步、排队和竞争等问题,可能导致死锁、无限延迟和数据竞争等现象的发生,这些都需要我们在程序中加以解决。

MFC虽然提供了一个线程类和若干同步类,但是仍然属于线程的低级编程,既困难又繁琐。利用.NET框架类库中的线程命名空间下的线程类,则可以简化线程编程。

13.2 MFC的进程和线程编程

使用传统的MFC/C++直接进行进程和线程编程异常复杂和繁琐,需要程序员自己处理线程间的同步、互斥、死锁等具体问题。

13.2.1 创建、管理和终止进程

MFC中并没有提供处理进程的类,我们需要直接使用Windows的API函数来创建、管理和终止进程。

1.创建进程

下面的CreateProcess函数用于在当前进程中创建一个新进程(和其主线程),以运行指定(路径/文件名或命令行)的应用程序:

2

BOOL CreateProcess( // 成功返回非0,失败返回0(可用GetLastError函数返回出错代码) LPCTSTR lpApplicationName, // 可执行文件的全路径或文件名,有命令行时可为NULL LPTSTR lpCommandLine, // 命令行参数字符串,有可应用名时可为NULL

LPSECURITY_ATTRIBUTES lpProcessAttributes, // 进程的安全属性,NULL表默认安全 LPSECURITY_ATTRIBUTES lpThreadAttributes, // 主线程的安全属性,NULL表默认安全 BOOL bInheritHandles, // 子进程是否继承新进程的句柄

DWORD dwCreationFlags, // 创建标志,用于设置进程的创建状态和优先级别,可为0 LPVOID lpEnvironment, // 环境变量,为NULL时同当前进程的

LPCTSTR lpCurrentDirectory, // 进程运行的当前目录,为NULL时同当前进程的

LPSTARTUPINFO lpStartupInfo, // 指向设置进程主窗口或控制条的各种属性的结构指针 LPPROCESS_INFORMATION lpProcessInformation // 指向返回进程信息的结构指针 );

其中,结构STARTUPINFO和PROCESS_INFORMATION的定义分别为:

typedef struct _STARTUPINFO {

DWORD cb; // 结构的长度(字节数)

LPTSTR lpReserved; // 保留,必须为NULL LPTSTR lpDesktop; // 桌面-窗口站的名称 LPTSTR lpTitle; // 控制台进程的标题 DWORD dwX; // 窗口位置的横坐标 DWORD dwY; // 窗口位置的纵坐标 DWORD dwXSize; // 窗口的水平尺寸 DWORD dwYSize; // 窗口的垂直尺寸

DWORD dwXCountChars; // 控制台窗口的屏幕缓冲区宽度(字符数) DWORD dwYCountChars; // 控制台窗口的屏幕缓冲区高度(字符数) DWORD dwFillAttribute; // 控制台窗口的初始文本和背景色 DWORD dwFlags; // 窗口的创建标志

WORD wShowWindow; // 用作窗口显示函数ShowWindow的缺省参数 WORD cbReserved2; // 保留,必须为0

LPBYTE lpReserved2; // 保留,必须为NULL HANDLE hStdInput; // 标准输入的句柄 HANDLE hStdOutput; // 标准输出的句柄 HANDLE hStdError; // 标准错误的句柄 } STARTUPINFO, *LPSTARTUPINFO;

typedef struct _PROCESS_INFORMATION { HANDLE hProcess; // 返回的进程句柄 HANDLE hThread; // 返回的主线程句柄 DWORD dwProcessId; // 返回的进程ID DWORD dwThreadId; // 返回的主线程ID

} PROCESS_INFORMATION, *LPPROCESS_INFORMATION;

2.管理进程

1)获取进程的句柄和ID

除了可从创建进程函数CreateProcess的最后一个(返回)参数——进程信息结构

3

PROCESS_INFORMATION变量来获取所创建的新进程及其主线程的句柄和ID外,还可利用API函数GetCurrentProcess和GetCurrentProcessId来获取当前进程的句柄和ID:

HANDLE GetCurrentProcess(void); DWORD GetCurrentProcessId(void);

不过用GetCurrentProcess返回的是一个伪句柄,只能在当前进程中使用。可以调用API函数DuplicateHandle将此伪句柄转换为一个真正的句柄。

2)获取和设置进程的优先级

在Windows操作系统中,进程有6种优先级别(priority level/class),从低到高分别为:空闲(Idel)、低普通(Below normal)、普通(Normal)、高普通(Above normal)、高(High)和实时(Real time),对应的符号常量为:

表15-1 进程优先级符号常量 符号常量 IDLE_PRIORITY_CLASS BELOW_NORMAL_PRIORITY_CLASS NORMAL_PRIORITY_CLASS ABOVE_NORMAL_PRIORITY_CLASS HIGH_PRIORITY_CLASS REALTIME_PRIORITY_CLASS 对应数值 0x00000040 0x00004000 0x00000020 0x00008000 0x00000080 0x00000100

可以在创建新进程时,利用其创建标志参数dwCreationFlags来设置。也可以用API函数GetPriorityClass和SetPriorityClass来获取和设置指定进程的优先级:

DWORD GetPriorityClass( HANDLE hProcess );

BOOL SetPriorityClass( HANDLE hProcess, DWORD dwPriorityClass );

例如:

DWORD p = GetPriorityClass( GetCurrentProcess() );

SetPriorityClass( GetCurrentProcess(), IDLE_PRIORITY_CLASS ); 3)等待进程返回

可以调用API函数WaitForSingleObject来等待指定进程(或线程)结束后返回: DWORD WINAPI WaitForSingleObject( HANDLE hHandle, DWORD dwMilliseconds ); 例如:WaitForSingleObject( pi.hProcess, INFINITE );

3.结束进程

结束进程的方法有多种,可以调用API函数ExitProcessk来结束当前进程(及其所有线程):(对控制台程序,在接收到CTRL+C或CTRL+BREAK信号后会调用此函数)

VOID ExitProcess( UINT uExitCode );

或调用API函数ExitProcess来结束指定进程(及其所有线程):

BOOL TerminateProcess(HANDLE hProcess, UINT uExitCode );

进程的所有线程终止后进程也会自动终止。在用户关闭系统或注销推出系统时,也会导致进程终止。

在结束进程后,还需要调用API的CloseHandle函数来删除进程和线程对象:

BOOL CloseHandle(HANDLE hObject );

例如:CloseHandle( pi.hProcess ); CloseHandle( pi.hThread );

4.例子

4

下面是一个控制台程序,在该程序中,创建一个新进程来运行指定的另一个可执行程序。为此,需要创建一个名为Process的“Visual C++/常规/空项目”,并将如下代码文件添加到此项目中: // Process.cpp

#include #include #include

void _tmain( ) {

STARTUPINFO si;

PROCESS_INFORMATION pi;

ZeroMemory( &si, sizeof(si) ); si.cb = sizeof(si);

ZeroMemory( &pi, sizeof(pi) );

// Start the child process. if( !CreateProcess(

\ // Module name(须换成你自己磁盘上的某个可执行文件的路径) NULL, // No Command line (use module name) NULL, // Process handle not inheritable NULL, // Thread handle not inheritable FALSE, // Set handle inheritance to FALSE 0, // No creation flags

NULL, // Use parent's environment block NULL, // Use parent's starting directory &si, // Pointer to STARTUPINFO structure

&pi ) // Pointer to PROCESS_INFORMATION structure ) {

printf( \ return; }

// Wait until child process exits.

WaitForSingleObject( pi.hProcess, INFINITE );

// Close process and thread handles. CloseHandle( pi.hProcess ); CloseHandle( pi.hThread ); }

13.2.2 创建、管理和终止线程

MFC中提供了线程类CWinThread,参见图15-2。在MFC

5

图15-2 CWinThread类

及其派生类

中区分两种类型的线程:用户界面线程(user-interface thread)和辅助线程(worker thread)。用户界面线程通常用于处理用户输入及响应用户生成的事件和消息。辅助线程通常用于完成不需要用户输入的任务(如重新计算)。Win32 API则不区分线程类型;它只需要了解线程的起始地址以开始执行线程。MFC为用户界面中的事件提供消息泵(message pump),从而对用户界面线程进行专门处理。CWinApp是用户界面线程对象的一个示例,因为它从CWinThread派生并对用户生成的事件和消息进行处理。

1.创建线程

MFC应用程序中的所有线程都由CWinThread对象表示。大多数情况下,甚至不必显式创建这些对象,而只需调用MFC框架的助手型全局函数AfxBeginThread,该函数将自动创建CWinThread对象。也可以先创建自己的线程(C++)对象,再利用线程类的成员函数CreateThread来创建Windows线程。

1)利用全局函数AfxBeginThread创建线程

AfxBeginThread函数有两个版本,分别用于创建用户界面线程和辅助线程: CWinThread* AfxBeginThread( // 创建用户界面线程 CRuntimeClass* pThreadClass,

int nPriority = THREAD_PRIORITY_NORMAL, UINT nStackSize = 0,

DWORD dwCreateFlags = 0,

LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL );

CWinThread* AfxBeginThread( // 创建辅助线程 AFX_THREADPROC pfnThreadProc, LPVOID pParam,

int nPriority = THREAD_PRIORITY_NORMAL, UINT nStackSize = 0,

DWORD dwCreateFlags = 0,

LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL ); 例如:

class CMyThread : public CWinThread {??} ??

CMyThread *pMyThread = (CMyThread*)AfxBeginThread(RUNTIME_CLASS(CSockThread)); if (pMyThread != NULL) { ??

pMyThread->ResumeThread(); } 及

UINT WorkerThread(LPVOID pParam) {CWnd *pWin = (CWnd*) pParam; ??} ??

AfxBeginThread(WorkerThread, this);

2)利用CWinThread类的CreateThread函数创建线程

可以先从CWinThread类派生自己的线程类,再利用CWinThread的成员函数

6

CreateThread来创建线程:

BOOL CreateThread(

DWORD dwCreateFlags = 0, UINT nStackSize = 0,

LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL ); 例如:

class CMyThread : public CWinThread {??} ??

CMyThread *pMyThread = new CMyThread; pMyThread->CreateThread(); ??

实际上,1)中的AfxBeginThread函数也是通过先创建一个CWinThread对象,再调用其CreateThread函数来完成线程创建的。

2.管理线程 1)获取线程对象

可以利用MFC的全局函数AfxGetThread来获取当前的线程对象:

CWinThread* AfxGetThread( );

2)获取和设置线程优先级

利用CWinThread类的成员函数GetThreadPriority和SetThreadPriority可以获取和设置线程的优先级:

int GetThreadPriority( );

BOOL SetThreadPriority( int nPriority );

线程的优先级是在其所属进程优先级的基础上的一种相对优先级,nPriority的取值可为如下符号常量:

表15-2 线程优先级符号常量

符号常量 THREAD_PRIORITY_IDLE THREAD_PRIORITY_LOWEST THREAD_PRIORITY_BELOW_NORMAL THREAD_PRIORITY_NORMAL THREAD_PRIORITY_ABOVE_NORMAL THREAD_PRIORITY_HIGHEST THREAD_PRIORITY_TIME_CRITICAL 相对数值 -15 -2 -1 0 1 2 15

3)检查线程是否活动

可以利用API函数GetExitCodeThread返回的退出代码是否为STILL_ACTIVE (259)来判断指定线程是否仍然在运行:

BOOL GetExitCodeThread( HANDLE hThread, LPDWORD lpExitCode ); 例如:

DWORD ec;

GetExitCodeThread(pMyThread->m_hThread, &ec);

7

if (ec == STILL_ACTIVE) {??}

4)挂起和恢复线程

可利用CWinThread类的成员函数SuspendThread和ResumeThread来挂起下车和恢复线程的运行:

DWORD SuspendThread( ); DWORD ResumeThread( );

3.终止线程

可以调用MFC的全局函数AfxEndThread来终止当前线程:

void AFXAPI AfxEndThread( UINT nExitCode, BOOL bDelete = TRUE ); 为了正常终止一个辅助线程,还可以使用return语句;为了正常终止一个用户界面线程,还可以在线程内调用Windows SDK中的PostQuitMessage函数:

void PostQuitMessage( int nExitCode );

还可以利用API函数TerminateThread强制终止指定线程:

BOOL WINAPI TerminateThread( HANDLE hThread, DWORD dwExitCode ); 不过这样做是危险的。

13.2.3 线程的同步*

由于同一进程的多个线程共享同样的数据段,具有同一地址空间。为了解决并行或并发的多个线程同时访问同一数据或资源所造成的冲突,需要解决线程的同步(synchronization)问题。

Windows操作系统提供了多种同步对象,并可替我们管理同步对象的加锁和解锁操作。我们的任务只是对每个需要同步使用的数据和资源产生一个同步对象,并在使用这些数据和资源前申请加锁,且在使用完成后申请解锁。

Windows设置了4种同步对象——临界区(critical section)、互斥量(mutex)、信号量(semaphore)和事件(event),其中,除了临界区外,其余三种同步对象都是操作系统的内核对象。MFC封装了这4种同步对象,对应的类分别为CCriticalSection、CMutex、CSemaphore和CEvent,它们都是同步对象类CSyncObject的派生类。另外,MFC还提供了两

图15-3 MFC同步类

个同步访问对象类CMultiLock和CSingleLock,它们俩都是没有基类的独立类,参见图15-3。

表15-3 MFC中的同步类

对象名 临界区 互斥量 类名 描述 用于 CCriticalSection 只允许当前进程中的一个线程访问某个对象 只有一个应用程序使用此资源时 CMutex 只允许系统中的一个进程内的一个线程访问某个对象 只允许知道数目的线程同时访问某个对象 可有多个应用程序使用此资源时 同一应用程序内多个线程可以同时访问此资源时 信号量 事件 CSemaphore CEvent 当某个事件发生时通知一个程序(的线程) 必须等到发生某事才能访问资源时 8

多锁 单锁 CMultiLock CSingleLock 为多个访问对象加锁 为单个访问对象加锁 在一特定时间需使用多个对象时 一次等待一个对象时 1.临界区

临界区(critical section)是一个进程中的所有线程共享的某个受保护的资源或代码段,被锁定后每次只能被一个线程所使用。临界区也是最容易使用的同步对象,但是只能用于单个进程中的线程同步,而不能被其他进程共享。由于临界区对象不是Windows的内核对象,所以它存在于进程的内存空间中。

为了在MFC中使用临界区,必须先创建一个CCriticalSection对象;在线程进入临界区之前,调用该对象的成员函数Lock来锁定临界区;在线程离开临界区之后,调用该对象的成员函数Unlock来解锁临界区。如果在调用Lock函数时,没有其他线程锁定临界区,则Lock函数对临界区加锁后立即返回,使调用它的线程继续运行;如果在调用Lock函数时,已经有其他线程锁定了临界区,则Lock函数被挂起,直到其他线程解锁临界区后才能返回。

例子:

CCriticalSection g_cs; // 创建临界区对象

int g_iNum = 0; // 需锁定的全局变量(受保护的同步数据资源)

DWORD ThreadProc(LPVOID pParam) { // 自定义的线程过程处理函数 g_cs.Lock(); // 加锁

g_iNum++; // 临界区(受保护的代码段) g_cs.Unlock(); // 解锁 return 0; }

2.互斥量

互斥量(mutex)的用途与临界区类似,但是它可以跨进程使用,能用来同步多个进程的数据共享访问,不过互斥量的操作速度比临界区的要慢近百倍。由于互斥量是Windows的内核对象,所以它存在于系统内存空间中,并具有引用计数。

MFC的CMutex类封装了互斥量对象,其使用方法类似于临界区类的——先构造一个CMutex对象,再调用函数Lock来锁定互斥量;在线程离开互斥量之后,调用Unlock来解锁互斥量。

下面是CMutex类的构造函数及从其基类继承下来的加锁与解锁函数: CMutex(

BOOL bInitiallyOwn = FALSE, // 指定创建线程是否初始时有权访问被保护的资源 LPCTSTR lpszName = NULL, // 跨进程使用时需指定相同的互斥量名

LPSECURITY_ATTRIBUTES lpsaAttribute = NULL // 指向安全属性结构的指针 );

virtual BOOL Lock( DWORD dwTimeout = INFINITE ); virtual BOOL Unlock( ) = 0;

其中,构造函数中的bInitiallyOwn参数为TRUE时,则会直到(指定名称的)互斥量可用时,才会从对此构造函数的调用中返回,可用于在希望等待的时刻来创建一个CMutex对象。加锁函数中的dwTimeout参数,用于指定等待时间的毫秒数,缺省为无限长(INFINITE)。如果指定了该参数的有限值,则超时时会放弃加锁并返回FALSE。

例子:

CMutex g_mx; // 创建互斥量对象

int g_iNum = 0; // 需锁定的全局变量(受保护的同步数据资源)

9

DWORD ThreadProc(LPVOID pParam) { // 自定义的线程过程处理函数 if (g_mx.Lock(100) ) { // 加锁

g_iNum++; // 互斥量(受保护的代码段) g_mx.Unlock(); // 解锁 }

return 0; }

3.信号量

信号量(semaphore,旗语)也是一种内核对象,用于资源的计数(即可使用该资源的线程个数)。当线程用信号量对象的句柄作参数来调用等待函数WaitForSingleObject时,系统会检查该信号量所对应的资源数是否大于0(即资源是否可用),若大于0(称为有信号signaled)则减少资源计数并唤醒线程,若等于0(称为无信号nonsignaled)则让线程进入睡眠状态直到(超出指定时间或当指定时间为无限时)占用该资源的其他线程释放资源并增加资源的计数(使信号量大于0,即有信号或有资源可用)为止。

信号量与前面讲的临界区和互斥量不同,它不属于某个线程,而且它允许(不同进程中的)多个线程同时访问一个受保护的资源(资源的可访问线程数由信号量的最大计数值决定)。信号量对象可用来限制对共享资源(如串口)进行访问的线程数目(如须≤计算机的串口总数)。

MFC中的信号量类为CSemaphore,其构造函数为 CSemaphore(

LONG lInitialCount = 1, // 初始计数值,须≥0且≤lMaxCount LONG lMaxCount = 1, // 最大计数值,须≥lInitialCount

LPCTSTR pstrName = NULL, // 信号量名串,用于多个进程中的线程

LPSECURITY_ATTRIBUTES lpsaAttributes = NULL // 安全属性,NULL表缺省 );

为了访问用信号量计数的资源,还需要创建一个CSingleLock(或CMultiLock)对象,来锁定和释放受保护的资源。CSingleLock类的成员函数有:

explicit CSingleLock( // 构造函数

CSyncObject* pObject, // 同步对象(如CSemaphore对象),不能为NULL BOOL bInitialLock = FALSE // 初始时是否锁定资源 );

BOOL IsLocked( ); // 判断是否锁定

BOOL Lock( DWORD dwTimeOut = INFINITE ); // 锁定资源 BOOL Unlock( ); // 释放资源 BOOL Unlock(// 释放资源

LONG lCount, // 释放的访问计数,须≥0

LPLONG lPrevCount = NULL // (返回)原计数的指针,NULL表不返回 ); 例子:

CSemaphore m_smph(5, 5); ??

WaitForSingleObject(m_smph.m_hObject, INFINITE ); ??

10

CSingleLock singleLock(&m_smph);

singleLock.Lock(); // Attempt to lock the shared resource if (singleLock.IsLocked()) { // Resource has been locked // Use the shared resource ??

// Now that we are finished, // unlock the resource for others. singleLock.Unlock(); }

4.事件

有时线程并不是要访问某个受保护的数据或资源,而是需要等待某一事件(event)的发生,这在GUI编程中十分常见。事件对象用于一个线程通知另一线程某一事件的发生(发信号表示某一操作已经完成),它是同步对象中形式最简单的一种,而且其同步的机制也是最具有弹性的。事件是一种内核对象,具有激发(有信号signaled)和非激发(无信号nonsignaled)两种状态,状态完全由程序来控制。

有两类事件对象——手工的(manual)和自动的(automatic)。手工事件对象会保持(由函数SetEvent或ResetEvent所设置的)状态不变,直到调用其他函数;而自动事件对象则在(至少一个)线程被释放后会自动返回无信号(不可用)状态。

MFC的CEvent类封装了Windows的事件对象,其成员函数有:

CEvent( // 构造函数

BOOL bInitiallyOwn = FALSE, // TRUE:线程对单/多锁对象可用; // FALSE:想访问资源的所有线程都必须等待

BOOL bManualReset = FALSE, // TRUE:手工事件对象;FALSE:自动事件对象 LPCTSTR lpszName = NULL, // 事件名串,用于跨进程的线程

LPSECURITY_ATTRIBUTES lpsaAttribute = NULL // 安全属性,NULL表缺省 );

BOOL SetEvent( ); // 设置事件为有信号(激发),释放任意在等待的线程 BOOL ResetEvent( ); // 设置事件为无信号(未激发),直到调用SetEvent才能激发 BOOL PulseEvent( ); // 先激发事件,在释放等待的线程后,再自动非激发事件 BOOL Unlock( ); // 释放事件对象 使用CEvent对象的一般方法是,在适当需要的时候构造CEvent对象,在适当的时候调用其SetEvent函数激发事件(使事件对象处于有信号状态),在完成对所控资源的访问后,再调用其Unlock函数释放事件对象。

例子:

UINT __cdecl MyThreadProc(LPVOID lpParameter) { CEvent* pEvent = (CEvent*)(lpParameter); VERIFY(pEvent != NULL);

// Wait for the event to be signaled

::WaitForSingleObject(pEvent->m_hObject, INFINITE);

// Terminate the thread

::AfxEndThread(0, FALSE);

11

return 0L; }

void CEvent_Test() {

// Create the CEvent object that will be passed to the thread routine CEvent* pEvent = new CEvent(FALSE, FALSE);

// Create a thread that will wait on the event CWinThread* pThread;

pThread = ::AfxBeginThread(&MyThreadProc, pEvent, 0, 0, CREATE_SUSPENDED, NULL); pThread->m_bAutoDelete = FALSE; pThread->ResumeThread();

// Signal the thread to do the next work item pEvent->SetEvent();

// Wait for the thread to consume the event and return

::WaitForSingleObject(pThread->m_hThread, INFINITE); delete pThread; delete pEvent; }

Windows API还提供了多个互锁函数Interlocked*,用于多个线程对一个共享变量的同步,能绝对保证改变变量的线程独占对该变量的访问。Windows API还提供了一组使线程阻塞自身执行的等待函数,包括等待单个对象的SignalObjectAndWait、WaitForSingleObject和WaitForSingleObjectEx;等待多个对象的WaitForMultipleObjects、WaitForMultipleObjectsEx、MsgWaitForMultipleObjects和MsgWaitForMultipleObjectsEx;发出提示的MsgWaitForMultipleObjectsEx、SignalObjectAndWait、WaitForMultipleObjectsEx和WaitForSingleObjectEx;注册登记的RegisterWaitForSingleObject和UnregisterWaitEx。由于时间和篇幅的限制,这里就不详细介绍了。

13.3 .NET下的进程和线程编程

13.3.1 进程编程

在.NET的框架类库中,与进程编程相关的类有Process(进程)、ProcessStartInfo(进程启动信息)、ProcessModule(进程模块)等,它们都位于System.Diagnostics命名空间中(程序集为在System.dll中的System)。ProcessStartInfo为一个独立的类,Process和ProcessModule的基类都为(System.ComponentModel命名空间中的)Component。

Process 类(组件)提供对正在计算机上运行的进程的访问,可用来启动、停止、控制和监视应用程序等任务。使用 Process 组件,可以获取正在运行的进程的列表,也可以启动新的进程。

12

Process 类的ProcessorAffinity 属性可用于获取或设置一些处理器,此进程中的线程可以按计划在这些处理器上运行。其属性值为System..IntPtr类型的位掩码,表示关联进程内的线程可以在其上运行的处理器。默认值为2 n -1,其中n是计算机上的处理器数。这一点可用于多核编程。

可使用ProcessStartInfo类来更好地控制启动的进程。至少必须以手动方式或使用构造函数来设置(应用程序或文档的)文件名属性FileName。此处,将文档定义为具有与其关联的打开或默认操作的任何文件类型。使用操作系统提供的“文件夹选项”对话框,可以查看计算机中已注册的文件类型及其相关应用程序。单击“高级”按钮可打开一个对话框,其中显示了是否存在与特定注册文件类型相关联的打开操作。

另外,还可使用ProcessStartInfo类来设置定义要对该文件执行的操作的其他属性。可以为Verb属性指定特定于FileName属性的类型的值。例如,可以为文档类型指定“print”。另外,还可以指定 Arguments属性值,这些值将成为传递给文件的打开过程的命令行参数。例如,如果在FileName属性中指定一个文本编辑器应用程序,则可以使用Arguments属性指定将用该编辑器打开的一个文本文件。

在进程启动前,可更改任何ProcessStartInfo属性的值。而启动进程后,更改这些值是没有效果的。

ProcessModule类表示加载到特定进程中的.dll或.exe文件。每个进程包含一个或多个模块,可用该类来获取进程中模块的信息。

下面是几个相关的C# 例子: 例1:(Process类) using System;

using System.Diagnostics;

using System.ComponentModel;

namespace MyProcessSample { ///

/// Shell for the sample. /// class MyProcess { // These are the Win32 error code for file not found or access denied. const int ERROR_FILE_NOT_FOUND =2; const int ERROR_ACCESS_DENIED = 5; /// /// Prints a file with a .doc extension. /// void PrintDoc() { Process myProcess = new Process(); try { // Get the path that stores user documents. string myDocumentsPath = Environment.GetFolderPath(Environment.SpecialFolder.Personal);

13

}

}

}

myProcess.StartInfo.FileName = myDocumentsPath + \ myProcess.StartInfo.Verb = \ myProcess.StartInfo.CreateNoWindow = true; myProcess.Start();

} catch (Win32Exception e) { if(e.NativeErrorCode == ERROR_FILE_NOT_FOUND) { Console.WriteLine(e.Message + \ } else if (e.NativeErrorCode == ERROR_ACCESS_DENIED) { // Note that if your word processor might generate exceptions // such as this, which are handled first. Console.WriteLine(e.Message + \ } }

public static void Main() { MyProcess myProcess = new MyProcess(); myProcess.PrintDoc(); }

例2(ProcessStartInfo类) using System;

using System.Diagnostics;

using System.ComponentModel;

namespace MyProcessSample { ///

/// Shell for the sample. /// class MyProcess { /// /// Opens the Internet Explorer application. /// void OpenApplication(string myFavoritesPath) { // Start Internet Explorer. Defaults to the home page. Process.Start(\ // Display the contents of the favorites folder in the browser. Process.Start(myFavoritesPath);

14

} ///

/// Opens urls and .html documents using Internet Explorer. /// void OpenWithArguments() { // url's are not considered documents. They can only be opened // by passing them as arguments. Process.Start(\ // Start a Web page using a browser associated with .html and .asp files. Process.Start(\ Process.Start(\ } /// /// Uses the ProcessStartInfo class to start new processes, both in a minimized /// mode. /// void OpenWithStartInfo() { ProcessStartInfo startInfo = new ProcessStartInfo(\ startInfo.WindowStyle = ProcessWindowStyle.Minimized; Process.Start(startInfo); startInfo.Arguments = \ Process.Start(startInfo); } static void Main() { // Get the path that stores favorite links. string myFavoritesPath = Environment.GetFolderPath( Environment.SpecialFolder.Favorites); MyProcess myProcess = new MyProcess(); myProcess.OpenApplication(myFavoritesPath); myProcess.OpenWithArguments(); myProcess.OpenWithStartInfo(); }

15

}

}

例3(ProcessModule类) Process myProcess = new Process();

// Get the process start information of notepad.

ProcessStartInfo myProcessStartInfo = new ProcessStartInfo(\// Assign 'StartInfo' of notepad to 'StartInfo' of 'myProcess' object. myProcess.StartInfo = myProcessStartInfo; // Create a notepad.

myProcess.Start();

System.Threading.Thread.Sleep(1000); ProcessModule myProcessModule;

// Get all the modules associated with 'myProcess'.

ProcessModuleCollection myProcessModuleCollection = myProcess.Modules; Console.WriteLine(\ associated \ +\

// Display the properties of each of the modules.

for( int i=0;i

myProcessModule = myProcessModuleCollection[i];

Console.WriteLine(\ Console.WriteLine(\ + myProcessModule.BaseAddress);

Console.WriteLine(\

+ \

Console.WriteLine(\ + myProcessModule.FileName); }

// Get the main module associated with 'myProcess'. myProcessModule = myProcess.MainModule; // Display the properties of the main module.

Console.WriteLine(\ + myProcessModule.ModuleName);

Console.WriteLine(\ + myProcessModule.BaseAddress);

Console.WriteLine(\ + myProcessModule.EntryPointAddress);

Console.WriteLine(\ + myProcessModule.FileName); myProcess.CloseMainWindow();

16

13.3.2 线程编程

.NET框架类库的(位于mscorlib.dll的程序集mscorlib中的)System.Threading命名空间中的Thread 类(基类为CriticalFinalizerObject),用于创建并控制线程、设置其优先级并获取其状态。

一个进程可以创建一个或多个线程以执行与该进程关联的部分程序代码。使用ThreadStart委托或ParameterizedThreadStart委托指定由线程执行的程序代码。使用ParameterizedThreadStart委托可以将数据传递到线程过程。

在线程存在期间,它总是处于由ThreadState定义的一个或多个状态中。可以为线程请求由ThreadPriority定义的调度优先级,但不能保证操作系统会接受该优先级。

从Object类继承的GetHashCode方法,提供托管线程的标识。在线程的生存期内,无论获取该值的应用程序域如何,它都不会和任何来自其他线程的值冲突。

需要的说明是,操作系统ThreadId和托管线程没有固定关系,这是因为非托管宿主能控制托管与非托管线程之间的关系。特别是,复杂的宿主可以使用CLR Hosting API针对相同的操作系统线程调度很多托管线程,或者在不同的操作系统线程之间移动托管线程。

一旦启动线程,便不必保留对Thread对象的引用。线程将继续执行,直到该线程过程完成。

例子(C#): using System;

using System.Threading;

// Simple threading scenario: Start a static method running // on a second thread.

public class ThreadExample {

// The ThreadProc method is called when the thread starts. // It loops ten times, writing to the console and yielding // the rest of its time slice each time, and then ends. public static void ThreadProc() { for (int i = 0; i < 10; i++) {

Console.WriteLine(\ // Yield the rest of the time slice. Thread.Sleep(0); } }

public static void Main() {

Console.WriteLine(\ // The constructor for the Thread class requires a ThreadStart // delegate that represents the method to be executed on the // thread. C# simplifies the creation of this delegate. Thread t = new Thread(new ThreadStart(ThreadProc));

// Start ThreadProc. Note that on a uniprocessor, the new // thread does not get any processor time until the main thread

17

// is preempted or yields. Uncomment the Thread.Sleep that // follows t.Start() to see the difference. t.Start();

//Thread.Sleep(0);

for (int i = 0; i < 4; i++) {

Console.WriteLine(\ Thread.Sleep(0); }

Console.WriteLine(\ t.Join();

Console.WriteLine(\thread: ThreadProc.Join has returned. Press Enter to end program.\

Console.ReadLine(); } }

13.4 Java的进程和线程编程

13.5 超线程与多核处理器

Intel公司的超线程技术将一个物理处理器核模拟成两个逻辑核,可并行执行两个线程,从而能有效提高处理器的运行效率。

一直以来,处理器芯片厂商都是通过不断提高主频来提高处理器的性能(例如Intel于 2004年12月12日推出的Pentium 4 HT 570J处理器的主频就达到了3.8GHz)。但是随着芯片制程工艺的不断进步,单个芯片上集成的晶体管数已超过数亿,传统处理器体系结构技术面临瓶颈,很难单纯通过提高主频来提升性能。而且在主频的提高同时,带来功耗的迅速提高以及散热等问题非常严重,这些也是直接促使单核转向多核的深层次原因。从应用需求来看,日益复杂的多媒体、科学计算、虚拟化等多个应用领域都呼唤更为强大的计算能力。在这样的背景下,各主流处理器厂商将产品战略从提高芯片的时钟频率转向多线程和多内核方面。

总之,不能永远靠加快频率的方法来改善性能。频率高到一定程度以后,必然要转向多核技术。这是由芯片的先天性质决定的。

13.5.1 SMT与超线程

SMT(Simultaneous MultiThreading,同时多线程)使用硬件多线程来改善超标量CPU的整体性能,它允许执行多个(如2~16个或更多)独立的线程来更好地利用现代处理器架构所提供的资源。超线程(HT,Hyper-Threading,早期曾叫Super-Threading)是Intel公司

18

研发的一种在一个实体处理器核中提供两个逻辑线程的技术,是SMT技术的特例。

1.超标量与流水线

超标量(superscalar)CPU架构,使用单颗核心来实现一种被称为指令集并行(instruction-level parallelism)的并行运算形式,它能够在相同的时钟频率下增加CPU的吞吐量。超标量处理器,通过同时分派多条指令给处理器上的冗余功能单元,可以在一个时钟周期内执行一条以上的指令。每个功能单元(functional unit)不是单独的CPU核,而是在单个CPU内的一个执行资源,例如一个ALU(Arithmetic Logic Unit,算术逻辑单元)、一个位移器(bit shifter)或一个乘法器(multiplier)。

在典型情况下,超标量CPU同时也是流水线的,它们是两种不同的性能增强技术。非流水线的超标量CPU或流水线的非超标量CPU在理论上是可能的。

流水线(pipeline,管道/管线),是一个串连在一起的数据处理元素的集合,其中的一个元素的输出是下一个元素的输入。流水线中的元素常常以并行或时间片方式执行,在这种情况下,在元素之间常常插入一些数量的缓冲存储器。

指令流水线(instruction pipelines)是与计算机相关的流水线(其他流水线有图形流水线和软件流水线等)中的一种,它被用于处理器中,允许在同一时钟周期(circuitry)内重叠执行多个指令。时钟周期通常分割成阶段,包括指令解码、算术和寄存器读取阶段,其中每个阶段每次处理一条指令。

简单的超标量流水线

通过每次读取和分派两个指令,在每个指令周期可以完成最多两个指令

2.超线程

Intel的超线程技术利用特殊的硬件指令,把两个逻辑内核模拟成两个物理芯片,让单个处理器核都能使用线程级并行计算,进而兼容多线程操作系统和软件,减少了CPU的闲置时间,提高的CPU的运行效率。参加下图:

19

采用超线程及时可在同一时间里,应用程序可以使用芯片的不同部分。虽然单线程芯片每秒钟能够处理成千上万条指令,但是在任一时刻只能够对一条指令进行操作。而超线程技术可以使芯片同时进行多线程处理,使芯片性能得到提升。

Intel表示,超线程技术让(P4)处理器在只增加5%的芯片面积的情况下,就可以换来15%~30%的效能提升。但实际上,在某些程序或未对多线程编译的程序而言,超线程反而会降低效能。除此之外,超线程CPU技术也需要主板芯片组和操作系统的配合,才能充分发挥超线程的效能。Intel公司的i865PE和i875P及更新的芯片组。微软公司的Windows XP、Windows Vista和Windows 7等操作系统都能较好地支持Intel的超线程CPU。

2002年2月Intel公司在其推出的代号为Prestonia的130nm新款至强(Xeon)处理器中首次采用超线程技术。Intel在其2003年5月21日推出的代号为Northwood的新款Intel Pentium 4 HT CPU(及绝大多数更新推出的P4处理器)中也内含超线程技术。Intel公司在

其2005年5月1日推出的代号为Smithfield的EE(Extreme Edition,极致版)系列和2006年1月16日代号为Presler的双核Pentium D处理器中也支持超线程技术。但是,Intel公司于2006年7月27日开始推出的Core 2(酷睿2)处理器,包括Solo(单核,只限笔记本电脑)、Duo(双核)、Quad(四核)及Extreme(极致)等型号,都不支持超线程。不过,Intel公司在其于2008年4月2日发布的用于笔记本和上网本的Atom(凌动)单核处理器和2008年11月17日推出的Core i7(酷睿 i7)桌面四核处理器又开始重新支持超线程技术。

13.5.2 多核处理器

多核,即多微处理器核心,是将两个或更多的独立处理器核封装在一个集成电路(IC)芯片中的一种方案。一般说来,多核心微处理器允许一个计算设备,在不需要将多个处理器核心分别进行独立的物理封装情况下,可以执行某些形式的线程级并行处理(Thread-Level Parallelism,TLP)。这种形式的TLP,通常被认为是芯片级别的多处理(Chip-level MultiProcessing,CMP)。

1.多核构架

按硬件层次划分,多核的种类有: ? 芯片级(多核芯片):片上多核处理器(Chip Multi--Processor,CMP)就是将多个计算内核集成在一个处理器芯片中,从而提高计算能力。按计算内核的对等与否,CMP可分为同构多核(如Intel和Sun)和异构多核(如IBM)。CPU核心数据共享与同步,包括总线共享Cache结构(每个CPU内核拥有共享的二级或三级Cache,用于保存比较常

20

用的数据,并通过连接核心的总线进行通信。例如Intel的Core 2 Due和Core i7)和基于片上互连的结构(每个CPU核心具有独立的处理单元和Cache,各个CPU核心通过交叉开关或片上网络等方式连接在一起。例如Intel的Pentium D和Core 2 Quad)。参见下图:

浮点处理单元 浮点处理单元 执行核 执行核 一级高速缓存 一级高速缓存 二级高速缓存 系统总线 Intel Core 2 Due的平面和逻辑结构图

? 板级:在一块主板上集成多个(多核)芯片。参见下图:

? 机架级:将多个含(多核)处理器的主板置于同一机箱内,主板之间利用专用芯片和线路进行通信。 ? 网络级(网格):将多个(多核)主机用(局域或互联)网连接在一起,构成分布式多核系统。

我们下面只讨论CMP级的多核,并且以Intel公司的Core系列微处理器为主。

2.体系结构

下面简单介绍一般的超线程与多核的体系结构,以及主流的单核和多核处理器——Intel公司的Pentium、Pentium D、Core 2和Core i7的逻辑结构及其基础微架构。

? 多核和超线程

下面是单核、多核和超线程处理器的体系结构(architecture)示意图:

21

CPU状态 中断逻辑 执行单元 Cache 单核单线程CPU

CPU状态 中断逻辑 CPU状态 中断逻辑 执行单元 Cache 单核双线程CPU CPU状态 中断逻辑 执行单元 Cache 共享cache的双核双线程CPU CPU状态 中断逻辑 执行单元 CPU状态 中断逻辑 执行单元 Cache CPU状态 中断逻辑 执行单元 Cache 独立cache的双核双线程CPU

CPU状态 中断逻辑 CPU状态 中断逻辑 CPU状态 中断逻辑 CPU状态 中断逻辑 执行单元 L1 Cache L2 Cache 双核四线程CPU

执行单元 L1 Cache

下面是分别单核与多核处理器的芯片结构图:

? 奔腾处理器

下面是Intel Pentium(奔腾)微处理器的功能结构图:

22

? 奔腾D处理器

下面是Intel Pentium D微处理器及其配套芯片组的功能结构图:

? Intel Core 2与Intel Core微架构

下面是酷睿2双核处理器的逻辑结构图:

23

Core 2 Due处理器逻辑结构图

其中:ROM = Read Only Memory 只读存贮器、ROB = 、

FPU = Float Point Unit 浮点运算单元、ALU= Arithmetic Logical Unit 算术逻辑部件、 TLB = Translation Lookaside Buffer转译后备缓冲器(转址旁路缓存/页表缓存)、

LD = 、ST = 、D-TLB = Data-TLB 数据TLB

酷睿2处理器是基于Intel Core 微架构(microarchitecture)的,下面是其逻辑结构图:

Intel Core微架构逻辑结构图

24

? Intel Core i7与Intel Nehalem微架构

下面是酷睿i7四核处理器的平面结构图:

Core i7处理器平面结构图

其中:Memory Controller = 内存控制器、Misc = 其他、Core = 核、IO = I/O = 输入/输出

Queue = 队列、QPI = QuickPath Interconnct = 快速通道互连、

Shared L3 Cache = 共享三级高速缓存

可见四核Core i7的基本构成:有超大容量的三级高速缓存、I/O控制单元、内存控制器电路和两条QPI总线连接。不同级别的Nehalem处理器将会有不同条数的QPI连接,普通桌面处理器通常只有一条QPI连接,工作站以上级别的将会有多条QPI连接。

Core i7处理器使用的是Nehalem微架构,而Nehalem采用了可扩展架构。主要是每个处理器单元均采用了组装模块化设计,组件包括:核心数量、SMT功能、L3缓存容量、QPI连接数量、IMC数量、内存类型、内存通道数量、整合GPU、能耗和时钟频率等,这些组件均可自由组合,以满足多种性能需求,比如可以组合成双核心、四核心甚至八核心的处理器,而且组合多个QPI(QuickPath Interconnct,快速通道互连)连接更可以满足多路服务器的需求。整合了GPU的Nehalem架构处理器Havendale可能在今年第四季度生产。

模块化设计的可伸缩Nehalem微架构 其中:IA = Intel Architecture 英特尔架构、

IGP = Integrated Graphics Processor 集成图形处理器、

QPI = QuickPath Interconnct 快速通道互连、

25

IMC = Integrated Memory Controller 集成内存控制

单个执行核心的基本构成

其中:Execution Units = 执行单元、L1 Data Cache = 一级数据高速缓存、

Memory Ordering & Execution = 内存排序与执行、

L2 Cache & Interrupt Servicing = 二级高速缓存与中断维护、Paging = 页面调度、

Out-of-Order Scheduling & Retirement = 乱序调度与退役、 Instruction Decode & Microcode = 指令解码与微码、

Branch Prediction = 分支预测、

Instruction Fetch & L1 Cache = 取指令与一级高速缓存

在每个执行核心中,包括乱序执行单元和完整的逻辑电路,有了这些才算是一个完整的高级处理核心,另外还有L1、L2缓存等电路,L1、L2缓存的面积并不大,大概也就1/4,像解码单元、分支预测逻辑判断单元、内存排序和页处理单元也占了不少面积。

Nehalem的改进是全方位的,比如改善循环监测机制,Nehalem的LSD能够缓冲28个微指令(Core为18),能处理更多的分支指令。Nehalem中进一步添加了指令融合机制,支持目前所有Core中的宏指令技术,更具备有Core不支持的64位宏融合模式,在处理64位代码的时候,将会有明显的性能改善。Nehalem还提升分支预测能力,搭载多级分支预测机制,提供了更高的性能表现。Nehalem同时增强并行计算功能,在Core体系架构上,并行计算可以同时处理96个微指令,Nehalem处理器将乱序窗口尺寸扩大了33%,这样就能同时处理128个微指令。参见下图:

26

Intel Nehalem微架构逻辑结构图

3.发展历史

IBM于2001年10月发布了世界上首款多核处理器——双核RISC处理器Power 4,它将两个64位的Power PC处理器集成在一颗芯片上。Power 4采用180纳米技术,主频为1.1和1.3 GHz,两核心共享1.41 MB二级高速缓存(L2 cache)。

HP于2004年2月也发布了其首款(64位RISC)双核处理器PA-RISC8800,采用130纳米技术,主频为0.8和1GHz,两核心共享32 MB L2 cache。

Sun于2004年3月发布了其首款(64位RISC)双核处理器UltraSPARC Ⅳ,也采用130纳米技术,主频为1.05和1.2GHz,两核心共享16 MB L2 cache,支持超线程。2005年11月14日Sun发布了代号为Niagara(尼亚加拉瀑布)低能耗的8核32线程的UltraSPARC T1处理器,采用90纳米技术,主频为1.2GHz。

Intel于2005年4月18日(5月25日)发布(推出)了世界上

27

首款基于x86架构的(64位CISC)双核处理器——Pentium D(奔腾D)820、830和840,主频分别为2.8、3.0和3.2 GHz。它将两颗Pentium 4 Prescott核心放在同一块芯片上(胶水CPU),采用90纳米技术,两核各自拥有独立的1 MB L2 cache。最初的Pentium D型号不支持超线程,后来推出的EE(Extreme Edition,极致版)系列和Presler型号才支持超线程技术。

AMD于2005年4月21日也发布了其首(64位CISC)多核处理器——双核版的Opteron(皓龙)200和800系列,采用90纳米技术,主频为1.4到2.2GHz,两核各自拥有独立的512 KB ~ 1 MB L2 cache。AMD的Opteron处理器,是针对服务器的高档CPU。

2006年1月5日Intel推出用于笔记本的Core Duo(酷睿双核)(32位CISC)处理器T2300~T2600和L2300~T2500,主频分别为1.666~2.166 GHz和1.5~1.833 GHz,共享2 MB L2 cache。Core Duo是Intel 首次采用65纳米技术的处理器,也是全球首款低能耗的双核处理器。。

2006年7月23日Intel推出用于服务器的代号为Conroe XE 的Core 2 Extreme(酷睿2 极致)(64位CISC)处理器X6800,也采用65纳米技术,主频为2.933 GHz,共享4 MB L2 cache。

2006年7月27日Intel发布Core 2 Duo(酷睿2 双核)(64位CISC)处理器,采用65纳米技术,包括代号为Allendale 的E6300和E6400,主频分别为1.866和2.133 GHz,共享2 MB L2 cache;和代号为Conroe的E6600和E6700,主频分别为2.4和2.666 GHz,共享4 MB L2 cache。

2007年1月7日Intel发布了Core 2 Quad(酷睿2 四核)(64位CISC)处理器Q6600,也采用65纳米技术,它将两颗Core 2 Duo Conroe核心放在同一块芯片上(胶水CPU),主频为2.4 GHz,两核各自拥有独立的4 MB L2 cache。

2007年11月19日AMD推出其代号为Agena的首款(64位CISC)四核(64位CISC)处理器Phenom(羿龙)X4 9500和9600,采用65纳米技术,主频分别为2.2和2.3 GHz,四个核各自拥有独立的512 KB L2 cache,共享2 MB L3 cache。与Intel的胶水四核CPU Core 2 Quad不同,AMD的Agena为首款真正四核的x86处理器。

2008年3月27日AMD推出代号为Toliman的三核处理器AMD Phenom X3 8400和8600,采用65纳米技术,主频分别为2.1和2.3 GHz,三个核各自拥有独立的512 KB L2 cache,共享2 MB L3 cache。Toliman是屏蔽掉AMD四核处理器Agena的一个核心后的产物(废品再用)。

2008年9月15日Intel推出了代号为Dunnington的基于Intel Core微架构的64位6核至强(Six-Core Xeon)处理器L7455、X7460和E7450,采用45纳米技术,主频分别为2.133、2.4和2.667 GHz,3组双核每组各自拥有独立的3MB L2 cache,共享12或16 MB L3 cache。

2008年11月17日Intel推出了面向高端应用的代号为Bloomfield的64位四核处理器Core i7(酷睿 i7)920和965 Extreme Edition,基于Intel Nehalem微架构,采用45纳米技术,主频分别为2.66和3.20 GHz,四个核各自拥有独

立的256 KB L2 cache,共享8 MB L3 cache,支持超线程技术。Intel计划于2010年推出代号为Gulftown的32纳米Core i7,将拥有六个核。

28

Core i7-940及其LGA 1366触点

Intel计划于2009年9月推出代号为Lynnfield的2~4核的64位处理器Core i5,也基于Intel Nehalem微架构,可视为Core i7的简化版,面向主流应用。

Intel于2009年6月18日又宣布了Core i3处理器,也基于Intel Nehalem微架构,低能耗,面向笔记本和上网本等移动应用。

在2009年2月的ISSCC 2009国际固态电路会议上,Intel首次宣布了8核心服务器处理器“Nehalem-EX”。2009年6月1日,Intel则正式公布了该处理器的详细资料,并将于2009年6月26日向公众详细介绍Nehalem-EX。8核16线程Nehalem-EX基于45nm工艺Nehalem架构,支持QPI总线互联,集成双芯片、四通道内存控制器,三级缓存容量24MB,晶体管数量也达到了惊人的23亿个,热设计功耗130W,接口为新的LGA1567。该系列处理器预计将于今年年底或明年年初发布,针对多路服务器市场,替代Penryn微架构的六核“Dunnington”,成为Intel多路服务器领域的旗舰产品,目标甚至是成为RISC超级计算机的低成本替代者。

4.并行性

多核中的并行性可以分成指令级并行和线程级并行两种: ? 指令级并行(Instruction-Level Parallelism, ILP) 当指令之间不存在相关时,它们在流水线中是可以重叠起来并行执行的。这种指令序列中存在的潜在并行性称为指令级并行,是在机器指令级并行。通过指令级并行,处理器可以调整流水线指令重执行顺序,并将它们分解成微指令,能够处理某些在编译阶段无法知道的相关关系(如涉及内存引用时),并简化编译设计;能够允许一个流水线机器上编译的指令,在另一个流水线上也能有效运行。指令级并行能使处理器速度迅速提高。

? 线程级并行(Thread Level Parallelism, TLP)

线程级并行将处理器内部的并行由指令级上升到线程级,旨在通过线程级的并行来增加指令吞吐量,提高处理器的资源利用率。TLP处理器的中心思想是:当某一个线程由于等待内存访问结构而空闲时,可以立刻导入其他的就绪线程来运行。处理器流水线就能够始终处于忙碌的状态,系统的处理能力提高了,吞吐量也相应提升。

服务器可以通过每个单独的线程为某个客户服务(Web服服务器,数据库服务器)。单核超标量体系结构处理器不能完全实现TLP,而多核架构则可以完全实现TLP,解决了以上问题。现在业界普遍认为,TLP将是下一代高性能处理器的主流体系结构技术。

29

5.特点

? MIMD架构——多核处理器是一种特殊的多处理器,所有的处理器都在同一块芯片上,属于MIMD架构:不同的核执行不同的线程(多指令),在内存的不同部分操作(多数据)。多核是一个共享内存的多处理器:所有的核共享同一个内存。但可以有各自的一、二级高速缓存。 ? 同步多线程(Simultaneously Multithreading,SMT)——容许多个独立的线程在同一个核上同步执行,可以将多个线程组合到同一个核上。例如:如果一个线程正在等待一个浮点操作的结束,其他的线程可以使用整数单元。 ? 实现多核架构难点——内存共享(同步访问)、独立缓存(缓存一致性)、核之间的通信、与系统其他部分的通信。

6.多核与超线程的比较

? 超线程技术与多核体系结构的区别:

? 超线程技术是通过延迟隐藏的方法提高了处理器的性能,本质上就是多个线程共

享一个处理核。因此,采用超线程技术所获得的性能并不是真正意义上的并行,因此采用超线程技术多获得的性能提升,将会随着应用程序以及硬件平台的不同而参差不齐。

? 多核处理器是将两个甚至更多的独立执行核嵌入到一个处理器内部。每个指令序

列(线程),都具有一个完整的硬件执行环境,所以,各线程之间就实现了真正意义上的并行。

两个线程在支持超线程技术的单个处理器核上执行

两个线程在双核处理器上并行执行

? 超线程技术与多核体系结构的联系:

? 超线程技术:充分利用空闲CPU资源,在相同时间内完成更多工作。

30

3.并行化方法——分而治之

并行化的主要方法是分而治之: ? 任务并行——根据问题的求解过程,把任务分成若干子任务(任务级并行或功能并行)。 ? 数据并行——根据处理数据的方式,形成多个相对独立的数据区,由不同的处理器分别处理。

13.6.3 并行计算机

并行计算机(parallel computer)由一组处理单元组成,这组处理单元通过相互之间的通信与协作,以更快的速度共同完成一项大规模的计算任务。并行计算机的两个最主要的组成部分是计算节点和节点间的通信与协作机制。

1.出现背景

1960年代初期,晶体管以及磁芯存储器的出现,处理单元变得越来越小,存储器也更加小巧和廉价。出现规模不大的共享存储多处理器系统,即大型主机(Mainframe)。

1960 年代末期,同一个处理器开始设置多个功能相同的功能单元,流水线技术也出现了,在处理器内部的应用大大提高了并行计算机系统的性能。

2.弗林分类(Flynn's taxonomy)

1966年Michael J. Flynn根据指令流和数据流的不同组织方式,把计算机系统的结构分为以下四类:

? SISD(Single Instruction stream Single Data stream,单指令流单数据流) ? SIMD(Single Instruction stream Multiple Data stream,单指令流多数据流) ? MISD(Multiple Instruction stream Single Data stream,多指令流单数据流) ? MIMD(Multiple Instruction stream Multiple Data stream,多指令流多数据流)

36

弗林分类

单指令 多指令 MISD MIMD 单数据 SISD 多数据 SIMD SISD MISD SIMD MIMD 弗林分类的图示

其中:PU = Processing Unit 处理单元

SISD是普通的顺序处理串行机(如Intel 486);SIMD是一种特殊的并行机制(如专用的向量机和Intel的MMX[MultiMedia eXtension,多媒体扩展]和SSE[Streaming SIMD Extensions,流SIMD扩展]指令集);MISD型的计算机是根本不可能存在的,但也有人认为流水线可以视为MISD结构;而MIMD则是典型的并行计算机(如Intel Croel 2和Corel i7、AMD的Opteron和Phenom等多核处理器,各种巨型机,包括中国的巨型机:国防科技大学的银河、国家智能计算机研究开发中心的曙光、国家并行计算机工程技术研究中心等的神威、联想集团的深腾等)。

13.6.4 并行计算机体系结构

并行计算机与超级计算机技术,为多核计算机的出现奠定了基础,而集成电路技术是多核芯片得以实现的物理条件。

37

1.分类

并行计算机系统结构可以分成如下五类:

1. 分布式存储器的SIMD处理机——含有多个同样结构的处理单元(PE,Processing

Element),通过寻径网络以一定方式互相连接。每个PE有各自的本地存储器(LM,Local Memory)。在阵列控制部件的统一指挥下,实现并行操作。 程序和数据通过主机装入控制存储器。由于通过控制部件的是单指令流,所以指令的执行顺序还是和单处理机一样,基本上是串行处理。指令送到控制部件进行译码。如果是标量指令,则直接由标量处理机执行。如果是向量指令,则阵列控制部件通过广播总线将它广播到所有PE并行执行。划分后的数据集合通过向量数据总线分布到所有PE的本地存储器LM。

PE通过数据寻径网络互连。数据寻径网络执行PE间的通信。控制部件通过执行程序来控制数据寻径网络。PE的同步由控制部件的硬件实现。也就是说,所有PE在同一个周期执行同一条指令。但是可以用屏蔽逻辑来决定任何一个PE在给定的指令周期执行或不执行指令。

2. 向量超级计算机(共享式存储器SIMD)——集中设置存储器,共享的多个并行存

储器通过对准网络与各处理单元PE相连。在处理单元数目不太大的情况下很理想。 这是集中设置存储器的一种方案。共享的多个并行存储器通过对准网络与各处理单元PE相连。存储模块的数目等于或略大于处理单元的数目。为了减少存储器访问冲突,存储器模块之间必须合理分配数据。通过灵活高速的对准网络,使存储器与处理单元之间的数据传送在大多数向量运算中都能以存储器的最高频率进行。这种共享存储器模型在处理单元数目不太大的情况下是很理想的。存储器模块数与PE数互质可以实现无冲突并行访问存储器。

3. 对称多处理器(SMP,Symmetric Multiple Processor)——一个计算机上汇集了一

组处理器,各处理器之间共享内存子系统以及总线结构。它是相对非对称多处理技术而言的、应用十分广泛的并行技术。

在这种架构中,一台电脑不再由单个CPU组成,而同时由多个处理器运行操作系统的单一复本,并共享内存和一台计算机的其他资源。虽然同时使用多个CPU,但是从管理的角度来看,它们的表现就像一台单机一样。系统将任务队列对称地分布于多个CPU之上,从而极大地提高了整个系统的数据处理能力。所有的处理器都可以平等地访问内存、I/O和外部中断。在对称多处理系统中,系统资源被系统中所有CPU共享,工作负载能够均匀地分配到所有可用处理器之上。

4. 并行向量处理机(PVP,Parallel Vector Processor)——在并行向量处理机中有少量

专门定制的向量处理器,每个向量处理器有很高的处理能力。并行向量处理机,通过向量处理和多个向量处理器并行处理,两条途径来提高处理能力。并行向量处理机通常使用定制的高带宽网络将向量处理器连向共享存储器模块。存储器可以以很高的速度向处理器提供数据。这种机器通常不使用高速缓存,而是使用大量的向量寄存器和指令缓冲器。

5. 集群计算机(computers cluster)——集群计算机是随着微处理器和网络技术的进

步而逐渐发展起来的,它主要用来解决大型计算问题。集群计算机是一种并行或分布式处理系统,由很多连接在一起的独立计算机组成,像一个单独集成的计算机资源一样协同工作。计算机节点可以是一个单处理器或多处理器的系统,拥有内存、IO设备和操作系统。一个集群一般是指连接在一起的两个或多个计算机(节点)。

38

节点可以是在一起的,也可以是物理上分散而通过网络连结在一起的(参见下图)。一个连接在一起的计算机集群对于用户和应用程序来说像一个单一的系统,这样的系统可以提供一种价格合理的且可获得所需性能和快速而可靠的服务的解决方案,而在以往只能通过更昂贵的专用共享内存系统来达到。集群计算机实际上就是典型的“云计算”设备。

集群系统的体系结构

2.并行计算机组成

组成并行计算机主要由节点(node)、互联网络(interconnect network)和内存(memory)等部分组成。参见下图:

内存模块与节点分离

内存模块位于节点内部

3.多级存储体系结构

为了解决内存墙(memory wall)性能瓶颈问题,现代计算机一般采用多级存储体系结构。其中最快的是CPU中的寄存器、其次是位于处理器内部的cache(高速缓存)、然后是本地局部内存、最后是机群内远程内存,单位价格是逐级降低,容量是逐级增加。参见下图:

39

位于多核处理器内的高速缓存,一般分为两级:位于核内的小型一级高速缓存(L1 cache)和位于核外并由所有核共享的二级高速缓存(L2 cache)。在一些4核、8核或更多核的处理器中,将核进行分组,组内共享二级高速缓存,组外共享三级高速缓存(L3 cache)。

L1 cache连接CPU寄存器和L2 cache,负责缓存L2 cache中的数据到寄存器中。类似地,L2 cache连接L1 cache和L3 cache,负责缓存L3 cache中的数据到L2 cache中。

4.高速缓存映射策略

cache映射策略(mapping strategy)是指内存块和cache线之间建立的相互映射关系,包括如下几种:

? 直接映射策略(direct mapping strategy)——每个内存块只能被唯一的映射到一条 cache 线中。 ? K-路组关联映射策略(K-way set association mapping strategy)——Cache被分解为V个组,每个组由 K条cache线组成,内存块按直接映射策略映射到某个组,但在该组中,内存块可以被映射到任意一条cache线。 ? 全关联映射策略(full association mapping strategy)——内存块可以被映射到 cache中的任意一条cache线。

5.并行计算机访存模型

并行计算机访存模型(Memory Access Model)有如下四种: ? UMA(Uniform Memory Access,统一访存)模型:

? 物理存储器被所有节点共享;

? 所有节点访问任意存储单元的时间相同;

? 发生访存竞争时,仲裁策略平等对待每个节点,即每个节点机会均等; ? 各节点的CPU可带有局部私有高速缓存;

? 外围I/O设备也可以共享,且每个节点有平等的访问权利。 ? NUMA(Non-Uniform Memory Access,非统一访存)模型:

? 物理存储器被所有节点共享,任意节点可以直接访问任意内存模块;

? 节点访问内存模块的速度不同,访问本地存储模块的速度一般是访问其他节点内存模块的3倍以上;

? 发生访存竞争时,仲裁策略对节点可能是不等价的; ? 各节点的CPU可带有局部私有高速缓存(cache); ? 外围I/O设备也可以共享,但对各节点是不等价的。

40

? COMA(Cache-Only Memory Access,只高速缓存访存)模型:

? 各处理器节点中没有存储层次结构,全部高速缓存组成了全局地址空间; ? 利用分布的高速缓存目录D进行远程高速缓存的访问; ? COMA中的高速缓存容量一般都大于2级高速缓存容量;

? 使用COMA时,数据开始时可以任意分配,因为在运行时它最终会被迁移到要用到它的地方;

? NORMA(No-Remote Memory Access,非远程访存)模型:

? 所有存储器都是私有的;

? 绝大多数NORMA都不支持远程存储器的访问; ? 在DSM中,NORMA就消失了。

下图是并行计算机系统的不同访存模型分类:(括号中为机器例)

13.6.5 并行计算模型

并行计算模型可以分成SIMD同步并行计算模型和MIMD异步并行计算模型两大类,而且在这两种模型中主要是PRAM(Parallel Random Access Machine,并行随机存储机)模型。

1.SIMD同步并行计算模型

单指令流多数据流的同步并行计算模型可以细分成如下两类: ? 共享存储的SIMD模型(PRAM模型)

又可以分成如下三个子类: ? PRAM-EREW(Exclusive--Read and Exclusive--Write),不允许同时读和同时写; ? PRAM-CREW (Concurrent--Read and Exclusive—Write),允许同时读但不允

许同时写;

? PRAM-CRCW (Concurrent--Read and Concurrent--Write),允许同时读和同时

写。 优点:

41

? 适合于并行算法的表达、分析和比较;

? 使用简单,很多诸如处理器间通信、存储管理和进程同步等并行计算机的低级

细节均隐含于模型中;

? 易于设计算法和稍加修改便可运行在不同的并行计算机上; ? 且有可能加入一些诸如同步和通信等需要考虑的方面。 ? 分布存储的SIMD模型(SIMD互联网络模型)

又可以分成如下九个子类:

? SIMD-LC——采用一维线性连接的SIMD模型; ? SIMD-MC——采用网孔连接的SIMD模型; ? SIMD-TC——采用树形连接的SIMD模型; ? SIMD-MT——采用树网连接的SIMD模型; ? SIMD-CC——用立方连接的SIMDSIMD模型; ? SIMD-CCC——采用立方环连接的SIMD模型; ? SIMD-SE——采用洗牌交换连接的SIMD模型; ? SIMD-BF——采用蝶形连接的SIMD模型;

? SIMD-MIN——采用多级互联网络连接的SIMD模型。

2.MIMD异步并行计算模型

多指令流多数据流的并行计算模型都属于APRAM(Asynchronism PRAM,异步PRAM)模型。

? APRAM特点:

? 每个处理器都有其本地存储器、局部时钟和局部程序; ? 处理器间的通信经过共享全局存储器;

? 无全局时钟,各处理器异步地独立执行各自的指令;

? 处理器任何时间依赖关系需明确地在各处理器的程序中加入同步(路)障(Synchronization Barrier);

? 一条指令可在非确定但有限的时间内完成。 ? APRAM模型的四类指令:

? 全局读——将全局存储单元中的内容读入本地存储器单元中;

? 局部操作——对本地存储器中的数执行操作,其结果存入本地存储器中; ? 全局写——将本地存储器单元中的内容写入全本地存储器单元中; ? 同步——同步是计算中的一个逻辑点,在该点各处理器均需等待别的处理器到达后才能继续执行其局部程序。

异步PRAM模型可以进一步分成如下三种: ? BSP模型

BSP(Bulk Synchronous Parallel,大块同步并行)模型作为计算机语言和体系结构之间的桥梁,由以下述三个参数描述分布存储的并行计算机模型: ? 处理器/存储器模块(PMM);

? PMM模块之间点到点信息传递的路由器; ? 执行以时间间隔L为周期的路障同步器。 BSP的特点:

42

? 将PMM和路由器分开,强调了计算任务和通信任务的分开,而路由器仅施行点到点的消息传递,不提供组合、复制或广播等功能,这样做既掩盖了具体的互联网络拓扑,又简化了通信协议;

? 采用路障方式的以硬件实现的全局同步是在可控的粗粒度级,从而提供了执行紧耦合同步式并行算法的有效方式,而程序员并无过分的负担;

? 在分析BSP模型的性能时,假定局部操作可在一个时间步内完成,而在每一超级步中,一个PMM至多发送或接受h条消息(h-relation)。 ? LogP模型—— 一种分布存储的、点到点通信的多处理机模型,其中通信网络由一组参数来描述,但它并不涉及到具体的网络结构,也不假定算法一定要用显式的消息传递操作进行描述。LogP机由L、o、g和P四个参数描述:(这也是该模型名称的来历) ? L(Latency,等待/潜伏时间)——通信介质的等待时间; ? o(overhead,开销)——发送和接收消息的开销; ? g(gap,间隙)——发送/接收操作之间所需的间隙;

? P(Processing units,处理模块)——处理模块的数量。每次在每个机器上的本地操作花费同样的(单位)时间,该时间被称为处理器周期。

前三个参数都用处理器周期来度量。

? C3(Computation、Communication、Congestion,计算、通信、拥塞)模型—— 一个与体系结构无关的粗粒度的并行计算模型,旨在能反映计算复杂度、通信模式和通信期间潜在的拥挤等因素,对粗粒度网络算法的影响。

13.6.6 并行计算性能评测

并行计算性能一般通过并行程序执行的时间来评测。行程序执行时间等于从并行程序开始执行到所有进程执行完毕,墙上时钟走过的时间,也称为墙上时间(wall clock time)。对各个进程,墙上时间可进一步分解为计算CPU时间、通信CPU时间、同步开销时间、同步导致的进程空闲时间。

并行程序性能评价方法有: ? 浮点峰值性能与实际浮点性能; ? 数值效率和并行效率。

13.6.7 并行计算的挑战

并行计算的挑战面临众多的挑战,包括如何协调、如何控制、如何监视、并行编程、采用多线程解决同一个问题和在并行线程之间的通信与同步机制等。

13.7 并行编程

并行编程涉及并行软件程序员的工作、并行程序设计方法、并行程序设计模型、并行编程标准和并行算法描述等方面。

43

13.7.1 并行编程环境

比较流行的并行编程环境主要有三类:消息传递、共享存储和数据并行,参见下表:

特征 典型代表 可移植性 并行粒度 并行操作方式 数据存储模式 数据分配方式 学习入门难度 可扩展性 消息传递 MPI、PVM 所有主流并行计算机 进程级大粒度 异步 分布式存储 显式 较难 好 共享存储 OpenMP SMP、DSM 线程级细粒度 异步 共享存储 隐式 容易 较差 数据并行 HPF SMP、DSM、MPP 进程级细粒度 松散同步 共享存储 半隐式 较易 一般 其中:MPI = Message Passing Interface 消息传递接口、PVM = Parallel Virtual Machine 并行虚拟机、OpenMP = Open Multi-Processing 开放多处理、HPF = High Performance Fortran 高性能Fortran、SMP = Symmetric Multiple Processor 对称多处理器、DSM = Distributed Shared Memory 分布式共享内存、MPP = Massively Parallel Processing 大规模并行处理。

13.7.2 编程语言与编译器

在科学计算领域已有三项成功的并行编程技术:自动并行化、数据并行语言(HPF)和共享存储并行编程接口(OpenMP)。

1.自动并行化

自动并行化一直是人们的奋斗目标。1980年代中期,基于依赖分析的自动向量化工具已经成熟,可以帮助程序员将Fortran语言代码移植到向量计算机上进行并行计算。后来的研究转向共享存储的MIMD和分布式存储结构的自动并行化,碰到很大的困难。现在,研究重点又逐步转向基于语言的策略研究,即从用户那里获得更多信息,同时利用自动化并行技术来减轻程序设计的负担。

2.HPF:数据并行编程

HPF(High Performance Fortran,高性能Fortran)是Fortran 90的扩展版(在Fortran 95和Fortran-2008包含了对HPF的支持),提供了注释形式的指令来扩展变量类型的说明,能够对数组的数据布局进行相当详细的控制。

HPF由HPFF(HPF Forum,HPF论坛,由美国Rice University的Ken Kennedy领导,网址为:http://hpff.rice.edu/)公布,1993年春夏推出1.0版HPF-1、1997年1月推出2.0版HPF-2。

由于具有HPF功能的Fortran编译器的实现和使用都遇到了不少困难,现在大多数厂商开始转向基于OpenMP的并行处理。

44

3.OpenMP:共享存储并行编程

OpenMP (Open Multi-Processing,开放多处理)是一种支持多平台共享内存多处理编程的C、C++和Fortran语言API。它支持许多体系结构,包括Unix和Microsoft Windows。它包含一组编译器指令、库程序、和影响运行时行为的环境变量。支持OpenMP的编译器包括Sun Compiler、GNU Compiler、Intel Compiler和Microsoft Visual C++等。

OpenMP的API规范由OpenMP ARB(Architecture Review Board, 架构评审委员会,网址为:http://openmp.org/wp/)公布。1997年10月推出OpenMP for Fortran 1.0、1998年10月推出OpenMP for C/C++ 1.0、2000年推出OpenMP for Fortran 2.0、2002年10月推出OpenMP for C/C++ 2.0、2005年推出2.5(for C/C++/Fortran)。2008年5月推出3.0。

OpenMP提供了对并行算法的高层的抽象描述,程序员通过在源代码中加入各种专用的pragma指令(directive,指示/命令)来指明自己的意图,由此编译器可以自动将程序进行并行化,并在必要之处加入同步互斥以及通信。当选择忽略这些pragma,或者编译器不支持OpenMP时,程序又可退化为通常的(串行)程序,代码仍然可以正常运作,只是不能利用多线程来加速程序执行。

OpenMP提供的这种对于并行描述的高层抽象降低了并行编程的难度和复杂度,这样程序员可以把更多的精力投入到并行算法本身,而非其具体实现细节。对基于数据分集的多线程程序设计,OpenMP是一个很好的选择。同时,使用OpenMP也提供了更强的灵活性,可以较容易的适应不同的并行系统配置。线程粒度和负载平衡等是传统多线程程序设计中的难题,但在OpenMP中,OpenMP库从程序员手中接管了部分这两方面的工作。

但是,作为高层抽象,OpenMP并不适合需要复杂的线程间同步和互斥的场合。OpenMP的另一个缺点是不能在非共享内存系统(如计算机集群)上使用。在这样的系统上,MPI使用较多。

13.7.3 并行软件程序员的工作

并行编程涉及不同的层次: ? 指令层:非常细的粒度; ? 数据层:细粒度; ? 控制层:中粒度; ? 任务层:大粒度。

前两层大都由硬件和编译器负责处理,程序员通常处理后两层的并行。

13.7.4 并行程序设计

1.方法

并行程序设计可以分成如下两类: ? 隐式并行程序设计:(即并行自动化)

? 常用传统的语言编程成顺序源编码,把“并行”交给编译器实现自动并行。 ? 程序的自动并行化是一个理想目标,存在难以克服的困难。

45

? 语言容易,编译器难。 ? 显式并行程序设计:

? 在用户程序中出现“并行”的调度语句。

? 显式的并行程序开发则是解决并行程序开发困难的切 实可行的。 ? 语言难,编译器容易。

2.模型

并行程序设计有如下四种模型: ? 隐式并行(Implicit Parallel)——程序员用熟悉的串行语言编程(未作明确的制定并行性),编译器和运行支持系统自动转化为并行代码。具有如下特点:语义简单、可移植性好、单线程(易于调试和验证正确性)、细粒度并行、效率很低。 ? 数据并行(Data Parallel)——是SIMD的自然模型,局部计算和数据选路操作。具有如下特点:单线程、并行操作于聚合数据结构(数组)、松散同步、单一地址空间、隐式交互作用、显式数据分布。数据并行的优点是编程相对简单且串并行程序一致;缺点有程序的性能在很大程度上依赖于所用的编译系统及用户对编译系统的了解、并行粒度局限于数据级并行、粒度较小。 ? 共享变量(Shared Variable)——是PVP、SMP和DSM的自然模型。具有如下特点:多线程(SPMD, MPMD)、异步、单一地址空间、显式同步、隐式数据分布、隐式通信。 ? 消息传递(Message Passing)——是MPP和COW的自然模型。具有如下特点:多线程、异步、多地址空间、显式同步、显式数据映射和负载分配、显式通信。

其中:SPMD (Single Process/ Program, Multiple Data,单进程/程序、多数据)和MPMD (Multiple Process/ Program, Multiple Data,多进程/程序、多数据)都是MIMD的子类。COW (Cluster of Workstations,工作中机群)是集群计算机的一种。

3.并行编程标准

并行编程标准有: ? 数据并行语言标准——HPF、显式数据分布描述、并行DO循环; ? 共享变量编程标准:

? 线程库(Thread Library)——Win32 API、POSIX threads线程模型; ? 编译制导(Compiler Directives)——OpenMP(可移植共享内存并行性); ? 消息传递库 (Message Passing Libraries) 标准——MPI和PVM。

可以将并行编程标准归为如下三类: ? 数据并行——HPF,用于SMP和DSM; ? 共享编程——OpenMP,用于SMP和DSM; ? 消息传递——MPI和PVM,用于所有并行计算机。

三者可混合使用,如对以SMP为节点的Cluster来说,可以在节点间进行消息传递,在节点内进行共享变量编程。

46

4.基本并行化方法

并行化的基本方法有: ? 相并行(Phase Parallel) ? 流水线并行(Pipeline Parallel) ? 主从并行(Master-Slave Parallel) ? 分治并行(Divide and Conquer Parallel) ? 工作池并行(Work Pool Parallel)

5.程序性能优化

? 串行程序性能优化

? 调用高性能库,比如优化的BLAS(Basic Linear Algebra Subprograms,基本线

性代数子程序),FFTW(Fastest Fourier Transform in the West,西方快速傅立叶变换,是最快的FFT自由软件库)等; ? 选择适当的编译器优化选项; ? 合理定义数组维数;

? 注意嵌套循环的顺序,尽量改善数据访问的局部性; ? 循环展开。 ? 并行程序性能优化

? 减少通信量、提高通信粒度;

? 全局通信尽量利用高效集合通信算法; ? 挖掘算法的并行度,减少CPU空闲等待; ? 负载平衡;

? 通信、计算的重叠;

? 通过引入重复计算来减少通信,即以计算换通信。

13.7.5 并行编译器

并行编译过程如下图所示:

47

1.流分析

流分析(flow analysis)主要是相关性分析(dependency analysis),包括流相关、反相关、输出相关和控制相关。还需要进行数据相关性测试,以证明同一数组变量的下标引用对之间的相关性不存在。

2.程序优化

程序优化就是代码优化,主要包括代码向量化和代码并行化:

? 代码向量化(Code Vectorization)——把标量程序中的由一种可向量化循环完

成的操作变换成向量操作。

? 代码并行化(Code Parallelization)——并行代码的优化是将一个程序展开成

多线程以同时供多台处理机并行执行,其目的是要减少总的执行时间。

3.代码生成

并行代码生成(Code Generation)涉及到将优化后的中间形式的代码转换程可执行的具体的机器目标代码。包括执行次序、指令选择、寄存器分配、负载平衡、并行粒度、代码调度以及后优化(Postoptimization)等问题。

13.8 Visual C++本地多核编程

Visual C++从2005版开始支持OpenMP 2.0的多核编程(2008和2010版也只支持2.0版)。Visual C++ 2010 Beta 1版支持本地C++的PPL(Parallel Pattern Library,并行模式库)编程。

13.8.1 OpenMP

本小节介绍OpenMP多核编程,主要内容包括:OpenMP简介、OpenMP编程技术、OpenMP应用程序设计的考虑因素和Visual C++的OpenMP多核编程。

OpenMP的MSDN帮助文档位于:开发工具与语言\\Visual Studio\\Visual C++\\参考信息\\Libraries Reference\\OpenMP\\(为英文版)。

1.OpenMP简介

OpenMP (Open Multi-Processing,开放多处理)是一种面向共享内存以及分布式共享内存的多处理器多线程并行编程语言,是一种能够被用于显示指导多线程、共享内存并行的应用程序编程接口(API),包含一组编译器指令、库程序、和影响运行时行为的环境变量。OpenMP具有良好的可移植性,支持多种编程语言C/C++ 和Fortan等。支持OpenMP的编译器包括Sun Compiler、GNU Compiler、Intel Compiler和Microsoft Visual C++等。OpenMP能够支持

48

多种平台,包括大多数的类UNIX系统以及Windows NT系统(Windows 2000、Windows XP、Windows Vista、Windows 7等)。

1)OpenMP特点

OpenMP的设计目标为:标准性、简洁实用、使用方便、可移植性。

OpenMP API(Application Programming Interface,应用编程接口)由三个基本部分(编译指令、运行部分和环境变量)构成,参见下图。是C/C++ 和Fortan等的标准API,已经被大多数计算机硬件和软件厂家所接受。

OpenMP不包含的性质有:不是建立在分布式存储系统上的、不是在所有的环境下都是一样的、不是能保证让多数共享存储器均能有效的利用。参见下图:

共享内存模式

分布内存模式

2)OpenMP的历史

? 1994年,第一个ANSI X3H5草案提出,被否决。 ? 1997年,OpenMP标准规范代替原先被否决的ANSI X3H5,被人们认可。 ? 1997年10月公布了与Fortran语言捆绑的第一个标准规范FORTRAN 1.0。 ? 1998年11月9日公布了支持C和C++的标准规范C/C++ 1.0。 ? 2000年11月推出FORTRAN 2.0。 ? 2002年3月推出C/C++ 2.0。 ? 2005年5月推出的OpenMP 2.5将原来的Fortran和C/C++标准规范结合在一起。 ? 2008年5月推出OpenMP 3.0。 ? 2008年11月推出OpenMP 3.0的C/C++语法摘要规范。 ? 2009年3月推出OpenMP 3.0的Fortran语法摘要规范的修订版。 相关的规范可在http://openmp.org/wp/openmp-specifications/中下载。

3)OpenMP多线程编程基础

OpenMP的编程模型以线程为基础,通过编译指导语句来显示地指导并行化,为编程人员提供了对并行化的完整的控制。

采用Fork-Join(分叉-接合)的形式,参见下列两图:

49

分叉-接合图示

其中:A为主线程(master thread),B、C、D皆为A的子线程

不同并行任务(task)中的同名子线程可以互不相同

4)编译指导语句

在编译器编译程序的时候,会识别特定的注释,而这些特定的注释就包含着OpenMP程序的一些语义。

#pragma omp [clause[ [,] clause]…] newline

其中,红色部分为关键字,#pragma(编译指示/附注/注记/杂注)为编译指令,omp表示OpenMP;(指导/指令/指示/指向)部分就包含了具体的编译指导语句,包括:parallel、for、parallel for、section、sections、single、master、critical、flush、ordered和atomic;clause(子句)为可选的若干子句,子句间可以用逗号或白空符分隔;newline为换行符,每个OpenMP语句必须以换行符结束。例如:

#pragma omp parallel private(var1, var2) shared(var3) {??}

编译指导语句的功能是将串行的程序逐步地改造成一个并行程序,达到增量更新程序的目的,减少程序编写人员一定的负担。

4)运行时库函数

OpenMP运行时函数库原本用以设置和获取执行环境相关的信息,它们当中也包含一系列用以同步的API。支持运行时对并行环境的改变和优化,给编程人员足够的灵活性来控制运行时的程序运行状况。

参见下图:

50

本文来源:https://www.bwwdw.com/article/degx.html

Top