====================================
第四章 并行程序设计
====================================
三种并行划分方式:任务划分、数据划分、数据流划分。
并行程序设计的挑战:同步、通信、负载平衡、可扩展性。
三种死锁类型:自死锁、递归死锁、错序死锁。
常见并行程序设计模式:任务级并行、分治、几何分解、流水线、波峰。
同步的三种原语:信号量、锁、条件变量。
三种原语的使用依赖应用程序的需求。这些同步原语可以通过原子操作和使用
适当的存储栅栏指令实现。存储栅栏(memory fence),是一种依赖于处理器的
操作,它可以将存储器操作维持合理的顺序以确保所有的线程都看到其他线程
的存储器操作。为了隐藏这些同步原语的粒度,可采用更高级别的同步操作。
P(s): atomic{sem = sem-1; temp = sem;}
if (temp < 0)
{线程T被阻塞并插入到信号量s的等待队列中}
V(s): atomic{sem = sem+1; temp = sem;}
if (temp <= 0)
{从信号量s的等待队列中移出一个线程}
生产者消费者问题的伪代码表示
semaphore s
void producer(){
while(1){
<产生下一个数据>
s->release()
}
}
void consumer(){
while(1){
s->wait()
<消费下一个数据>
}
}
互斥量是最简单的锁类型。在使用互斥量时,可以加上超时释放。使用
try-finally确保互斥量被释放。
递归锁是指可以被当前持有该锁的线程重复获取,而不会导致死锁。
递归锁要有足够的释放操作来平衡获取锁操作,主要用在递归函数。
读写锁被称为共享独占锁、多读单写锁或者非互斥信号量。要注意锁的
粒度过大会影响性能。
自旋锁使用在任何锁持有时间少于将一个线程阻塞和唤醒所需的时间的
场合。自旋锁的持有时间应该限制在线程上下文切换时间的50%到100%之间。
Condition C;
Lock L;
Bool LC = false;
void producer(){
while(1){
L->acquire();
while (LC == true){
C->wait(L);
}
<产生下一个数据>
LC = true;
C->signal(L);
L->release();
}
}
void consumer(){
while(1){
L->acquire();
while (LC == false){
C->wait(L);
}
<消费下一个数据>
LC = false;
L->release();
}
}
同步通信和异步通信的区别?是否是确认返回。
什么是栅栏指令?会造成明显性能损失,最好是编译自动产生。
POSIX使用条件变量来达到Windows中事件模型的概念。
同是在Windows中mutex对象是内核机制,开销大,但是可以跨越进程。
而CriticalSection提供用户级的互斥量。
三种消息传递方式:进程内、进程间、进程对方式。
====================================
第五章 线程API
====================================
CreateThread之后ThreadFunc()中返回就隐含对ExitThread()的调用。
_beginthreadex()在调用CreateThread()之前完成数据的每线程初始化。
AfxBeginThread返回一个指向CWinThread的指针,调用_beginthreadex()。
DWORD SuspendThread( HANDLE hThread );
DWORD ResumeThread( HANDLE hThread );
DWORD TerminateThread( HANDLE hThread, DWORD dwExitCode );
注意SuspendThread和TerminateThread都不会释放同步对象。
#include "WindowsEvent.cpp"
Win32API中定义了一些类型不同的同步对象,其中包括事件、信号量、互斥量、
以及临界段。此外程序员可以通过Wait方法在线程或进程句柄上等待,用于等待
线程或进程终止。微软还提供了支持对变量和链表进行原子访问和互锁函数。
Win32使用信号量要使用CreateSemaphore() OpenSemaphore() ReleaseSemaphore()
等待使用WaitForSingleObject()。互斥量和信号量类似,但没有count参数,包括
CreateMutex() OpenMutex() ReleaseMutex()等待使用WaitForSingleObject()。
注意两者都是内核对象,各词内核调用会损失性能。
CriticalSection是用户级的API:
void InitializeCriticalSection( LPCRITICAL_SECTION lpCS);
void InitializeCriticalSectionAndSpinCount(
PCRITICAL_SECTION lpCS
DWORD dwSpinCount );
void EnterCriticalSection( LPCRITICAL_SECTION lpCS );
void TryEnterCriticalSection( LPCRITICAL_SECTION lpCS );
void LeaveCriticalSection( LPCRITICAL_SECTION lpCS );
void SetCriticalSectionSpinCount( PCRITICAL_SECTION lpCS
DWORD dwSpinCount );
void DeleteCriticalSection( LPCRITICAL_SECTION lpCS );
Win32提供低开销的原子操作互锁函数,有相应64位版本:
InterLockedIncrement(); 和InterLockedDecrement();
InterLockedExchange(); 和InterLockedExchangeAdd();
InterLockedCompareExchange();
InterLockedCompareExchangePointer();
互锁函数对单链表进行原子访问支持。
InitializeSListHead()
InterlockedPushEntrySList()
InterlockedPopEntrySList()
InterlockedFlushSList()
Win2k开始提供线程池API QueueUserWorkItem(Func,Contex,Flag)
Flag设置为WT_EXECUTELONGFUNCTION将在线程不足时创建新线程。
Flag设置为WT_EXECUTIONDEFAULT告知线程池该线程不执行异步I/O。
执行异步I/O的线程Flag应设置WT_EXECUTRINIOTHREAD。
Win32线程优先级相关函数,注意不可滥用优先级:
SetThreadPriority(threadhandle, newPriority);
GetThreadPriority(threadhandle);
SetProcessPriorityBoost(hProc, is_dyn_disable);
SetThreadPriorityBoost(hThread, is_dyn_disable);
GetProcessPriorityBoost(hProc, is_dyn_disable);
GetThreadPriorityBoost(hThread, is_dyn_disable);
线程优先级符号常量:
THREAD_PRIORITY_TIME_CRITICAL
THREAD_PRIORITY_HIGHEST
THREAD_PRIORITY_ABOVE_NORMAL
THREAD_PRIORITY_NORMAL
THREAD_PRIORITY_BELOW_NORNAL
THREAD_PRIORITY_LOWEST
THREAD_PRIORITY_IDLE
注意设置处理器亲和要进行完全的测试以证明性能改:
SetThreadAffinityMask(threadhandle, mask);
SetProcessAffinityMask(processhandle, mask);
线程亲和掩码对应位为1说明可以在这个处理器上执行,
注意线程亲和掩码必须是所在进程的亲和掩码子集。
GetProcessAffinityMask(hProc, pProcMask, pSysMask)
设置选择策略而非强制的线程亲和:
SetThreadIdealProcessor(threadhandle, idealprocessor);
获取系统处理器信息:GetSystemInfo(SYSTEM_INFO &info);
将线程转换为windows纤程之后就可以增加另外的纤程:
PVOID ConvertThreadToFiber(parameters);
PVOID GetCurrentFiber();
GetFiberData(); 返回纤程参数
CreateFiber( stackSize, Func, parameters);
纤程函数的最重要特征是纤程函数不能退出。
保存CreateFiber()函数的返回地址,用来进行纤程切换:
VOID SwitchToFiber(PVOID addrOfFiberEnv)
该函数是激活纤程的唯一方法,开发人员对纤程的调动有完全控制。
VOID DeleteFiber(PVOID addrOfFiberEnv);
如果采用这种方式,当前的线程和相关的纤程也会终止。
Window编译多线程开关/MT /MTd /MD /MDd /D"_MT"
使用.NET线程的时候注意Join() Suspend() Resume() Interrupt()
.NET线程池可以管理工作队列、唤醒线程、为线程分派工作等。
线程池是一个有价值的资源,当需要大量线程的时候优先使用。
因为任务可以分派给任何线程池里的线程,因此主线程没有办法合并线程池
中的任何线程,也不存在等待线程池里的线程退出的机制。
除了处理排队任务的工作引擎之外,线程池也是一种分配线程等待某些特殊
事件的有效手段,例如等待网络传输或其他异步事件。.NET提供的几种方法
都是要注册一个回调函数,等到事件发生或者超时的时候调用。
.NET设置多个事件启动线程。WaitHandle.WaitAll()/WaitAny()。
ManualResetEvent()方法可以人工完成激发,人工复位前一直处于激发态。
lock(this){...} Monitor.Enter(this) try{...}finally{Monitor.Exit(this)}
=====================================
第六章 OpenMP解决方案
=====================================
OpenMP 2.5规范中可以多线程执行的循环有如下五点约束:
循环变量必须是有符号整数、比较语句必须每次递增/递减到循环不变的整数,
循环必须是单出口、单入口的,不要外部调入循环内或循环呢跳出循环外。
存在相关事循环之间不能使用OpenMP并行: flow dependence, ouput dependence,
anti-dependence. 可以使用private(x)子句消除数据竞争。使用OpenMP容易忽视
数据竞争的存在,可以使用Intel线程检测器避免。
注意使用private,firstprivate,lastprivate或reduction子句消除数据竞争。
可以使用reduction子句,用来有效地合并一个循环中一个或多个变量满足结合律
的算术规约,但要注意无法保证得到完全相同的结果。例如:
sum = 0;
#pragma omp parallel for reduction(+:sum)
for (k = 0; k <100; k++)
sum += func(k);
注意不同的调度方式开销不同,要测试,如schedule(dynamic)。
尽量减小并行区个数,将#pragma omp parallel{}提到外面来。使用nowait去除
隐式屏障,使用barrier添加显示屏障,single/master指定单线程执行部分。例如:
#pragma omp parallel
{
int tid = omp_get_thread_num();
#pragma omp for nowait
for ( k = 0; k <100; k++ )
x[k] = fn1(tid);
#pragma omp master
y = fn_input_only();
#pragma omp barrier
#pragma omp for nowait
for ( k = 0; k <100; k++ )
x[k] = y + fn2(tid);
#pragma omp single
fn_single_print(y);
#pragma omp master
fn_print_array(x);
}
#pragma omp parallel for private (row, col) firstprivate(doInit, pGray, pRGB)
for ( row = 0; row < height; row++ ){
if (doInit == TRUE){
doInit = FALSE;
pRGB += ( row * RGBStride );
pGray += ( row * GrayStride );
}
for ( col = 0; col < width; col++ ){
pGray[col] = (BYTE)(pRGB[row].red * 0.299 +
pRGB[row].green * 0.587 +
pRGB[row].blue * 0.114 );
}
pGray += GrayStride;
pRGB += RGBStride;
}
区分#pragma omp critical{} 和#pragma omp critical(name){}两种临界段,
但是要记住进入嵌套的同名临界段要死锁。使用#pragma omp atomic的表达式
必须自增减或类似 x += exp。
*Intel OpenMP任务队列扩展
常用的OpenMP库函数和环境变量:
int omp_get_num_threads(void);
int omp_set_num_threads(int NumThreads);
int omp_get_thread_num(void);
int omp_get_num_procs(void);
set OMP_SCHEDULE = "guided, 2"
set OMP_NUM_THREADS = 4
Intel OpenMP编译选项:Windows/Linux
-Qopenmp/-openmp (编译器定义_OPENMP)
-Qopenmp-profile/-openmp-profile 生成调试VTune参数
-Qopenmp-stubs/-openmp-stubs 链接stub库,生成顺序代码
-Qopenmp-report 0|1|2/-openmp-report 0|1|2
OpenMP程序调试的指导性步骤:
1.通过启动和禁止编译制导,二分法找出引发故障的并行结构。
2.关闭-Qopenmp,使用-Qopenmp-stubs查看错误是否在串行代码中。
3.打开-Qopenmp,设置OMP_NUM_THREADS=1,查看单线程是否有错。
4.使用-Qopenmp以及/Od /O1 /O2 /O2或/Qipo找到最低错误优化等级。
5.检查引起错误的代码段,检查并行化之后数据相关性被破坏、竞争、死锁、
缺少栅障和变量未初始化之类的问题。
6.使用/Qtcheck进行代码插桩,使用Intel线程检查器检查。
问题一般是由数据竞争引起的。因为某些共享变量本来应该声明为私有变量、
规约变量或者线程私有变量,或者缺少必要的原子或临界区保护。一种技巧是:
使用default(none) 强制指明变量属性。
另一种常见的问题是变量没有初始化,注意变量在进入或者退出并行结构时
是没有初值的。可以使用firstprivate或lastprivate初始化或复制变量。
可以加上if(exp)子句来设定并行化的条件,规模小则不适合并行化,比如:
#pragma omp parallel for if(n>16)
OpenMP多线程程序的性能在很大程度上依赖:单线程代码固有性能;并行程序
执行的比例及其可扩展性;CPU利用率、有效的数据共享、数据局部性和负载平衡;
线程间通信和同步量;线程创建、合并、管理、挂起、销毁和同步的开销;串行-
并行、并行-串行转换开销;共享内存或伪共享内存引起的存储器访问冲突;诸如
内存、写合并缓冲、总线带宽和CPU执行部件这样的共享资源的性能局限。
多核多线程程序设计笔记(上)
最新推荐文章于 2022-05-27 17:44:52 发布