在多核平台上开发程序,我们主张把子任务并行化。这样需要创建多个进程。问题是,是不是线程越多越好呢?请看下面的示例。
/*计算集合粒子通过成对相互作用的势能*/
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <windows.h>
#define NPARTS 1000
#define NITER 21
#define DIMS 3
#define NUM_THREADS 2
//int rand( void );
DWORD WINAPI computePot(LPVOID);
void initPositions(void);
void updatePositions(void);
double r[DIMS][NPARTS];
int bounds[2][NUM_THREADS];
double pot;
double gPot[NUM_THREADS];
int main() {
int i, j;
HANDLE tHandle[NUM_THREADS];
int tNum[NUM_THREADS];
for (i=0; i<NUM_THREADS; i++) {
bounds[0][i] = i * (NPARTS/NUM_THREADS);
bounds[1][i] = (i+1) * (NPARTS/NUM_THREADS);
}
bounds[1][NUM_THREADS-1] = NPARTS;
initPositions();
updatePositions();
for( i=0; i<NITER; i++ ) {
pot = 0.0;
for (j=0; j<NUM_THREADS; j++) {
tNum[j] = j;
tHandle[j] = CreateThread(NULL, 0, computePot, &tNum[j], 0, NULL);
}
WaitForMultipleObjects(NUM_THREADS, tHandle, TRUE, INFINITE);
for (j=0; j<NUM_THREADS; j++) {
pot += gPot[j];
}
if (i%10 == 0) printf("%5d: Potential: %10.3f\n", i, pot);
updatePositions();
}
}
void initPositions() {
int i, j;
for( i=0; i<DIMS; i++ )
for( j=0; j<NPARTS; j++ )
r[i][j] = 0.5 + ( (double) rand() / (double) RAND_MAX );
}
void updatePositions() {
int i, j;
for( i=0; i<DIMS; i++ )
for( j=0; j<NPARTS; j++ )
r[i][j] -= 0.5 + ( (double) rand() / (double) RAND_MAX );
}
DWORD WINAPI computePot(LPVOID pArg) {
int i, j, start, end, tid;
double lPot = 0.0;
double distx, disty, distz, dist;
tid = *(int *)pArg;
start = bounds[0][tid];
end = bounds[1][tid];
for( i=start; i<end; i++ ) {
for( j=0; j<i-1; j++ ) {
distx = pow( (r[0][j] - r[0][i]), 2 );
disty = pow( (r[1][j] - r[1][i]), 2 );
distz = pow( (r[2][j] - r[2][i]), 2 );
dist = sqrt( distx + disty + distz );
lPot += 1.0 / dist;
}
}
gPot[tid] = lPot;
return 0;
}
我们创建了42个子任务(进程)加上一个主线程,但是并行的效果并不理想10.82秒(我是运行在双核的机器上,同一时刻最多二个任务并行)。从中可以看出,我们虽然试图创建多个线程并试图并行,但是由于资源的有限性,只能有二个子任务同时并行。而这么多的线程创建及终止,毕竟也消耗了额外的系统资源。
下面是使用
Intel(R) Thread Profiler 看到的结果。
Figure-1
那么如何在现有的基础上对代码进行改进呢?
DWORD WINAPI tPoolComputePot(LPVOID); // 增加线程池的控制程序
int done = 0;
HANDLE bSignal[NUM_THREADS]; // 信号量用作计算开始
HANDLE eSignal[NUM_THREADS]; // 信号量用作计算结束
改写main函数:
int main() {
int i, j;
HANDLE tHandle[NUM_THREADS];
int tNum[NUM_THREADS];
for (i=0; i<NUM_THREADS; i++) {
bounds[0][i] = i * (NPARTS/NUM_THREADS);
bounds[1][i] = (i+1) * (NPARTS/NUM_THREADS);
bSignal[i] = CreateEvent(NULL, FALSE, FALSE, NULL); // auto-reset
eSignal[i] = CreateEvent(NULL, FALSE, FALSE, NULL); // auto-reset
}
bounds[1][NUM_THREADS-1] = NPARTS;
for (j=0; j<NUM_THREADS; j++) {
tNum[j] = j;
tHandle[j] = CreateThread(NULL, 0, tPoolComputePot, &tNum[j], 0, NULL);
}
initPositions();
updatePositions();
for( i=0; i<NITER; i++ ) {
WaitForMultipleObjects(NUM_THREADS, eSignal, TRUE, INFINITE); //上次已结束?
pot = 0.0;
for (j=0; j<NUM_THREADS; j++) {
pot += gPot[j];
}
if (i%10 == 0) printf("%5d: Potential: %10.3f\n", i, pot);
updatePositions();
}
done = 1; //全部处理完
for (j=0; j<NUM_THREADS; j++)
SetEvent(bSignal[i]);
WaitForMultipleObjects(NUM_THREADS, tHandle, TRUE, INFINITE);
}
// 修改updatePositions 函数
void updatePositions() {
// 保持原来工作,增加下面内容-发出”可以”工作信号
for (j=0; j<NUM_THREADS; j++)
SetEvent(bSignal[j]);
}
// 增加新的计算控制函数
DWORD WINAPI tPoolComputePot(LPVOID pArg) {
int tid = *(int *)pArg;
while (!done) {
WaitForSingleObject(bSignal[tid], INFINITE); //等待开始信号
computePot(tid);
SetEvent(eSignal[tid]); //发出结束信号
}
return 0;
}
从上面的修改可以看到,子任务的线程从42个变成了2个。原来的任务还是在这二个线程中反复运行,这样,我们我们省去了线程的创建和终止所占用的系统时间。注意,我们用四个信号量去管理二个线程的任务调度。二种方法,那一种更好些呢?可以用
Intel® Thread Profiler 去做一下测量,见下图。
一个很重要的指标:Total Critical Path Time.
还可以深入观察到每一个线程和对象的数据。
Figure-2