人的大腦並不善於處理並發的邏輯。如果把源代碼寫成並發的樣子,對人的大腦是非常不友好的。所以就需要各種語法糖來把「優雅」 串列的代碼,翻譯成並發形式來執行。這裡有四種主要的方式。

1、投機執行

不僅僅CPU可以做speculative execution。我們自己也可以做

這個類型的經典代表是 Haxl:facebook/Haxl

let fetchSingleElement = function (arg) {
console.log(fetch single element, arg);
return arg;
};

let fetchBatchElements = function (args) {
console.log(fetch batch elements, args);
return args;
};

let fakeResult = {};

function speculativeExecute(f) {
let cache = {};
let collectedArgs = [];
fetchSingleElement = function (arg) {
if (cache[arg] !== undefined) {
return cache[arg];
}
collectedArgs.push(arg);
return fakeResult
};
while(true) {
let result = f();
if (collectedArgs.length === 0) {
return result;
}
for (const [arg, fetched] of fetchBatchElements(collectedArgs).entries()) {
cache[arg] = fetched;
}
collectedArgs = [];
}
}

function businessLogic() {
let result = 0;
for (let i = 0; i < 10; i++) {
result += fetchSingleElement(i);
}
return result;
}

// console.log(result, businessLogic());
console.log(result, speculativeExecute(businessLogic));

執行的結果是

fetch batch elements [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
result 45

如果直接執行businessLogic,效果是

fetch single element 0
fetch single element 1
fetch single element 2
fetch single element 3
fetch single element 4
fetch single element 5
fetch single element 6
fetch single element 7
fetch single element 8
fetch single element 9
result 45

本質上這種數據流依賴的分析也可以用編譯器來做。

2、協程和同步調用

const verifyUser = function(username, password, callback){
dataBase.verifyUser(username, password, (error, userInfo) => {
if (error) {
callback(error)
}else{
dataBase.getRoles(username, (error, roles) => {
if (error){
callback(error)
}else {
dataBase.logAccess(username, (error) => {
if (error){
callback(error);
}else{
callback(null, userInfo, roles);
}
})
}
})
}
})
};

相比

const verifyUser = async function(username, password){
try {
const userInfo = await dataBase.verifyUser(username, password);
const rolesInfo = await dataBase.getRoles(userInfo);
const logStatus = await dataBase.logAccess(userInfo);
return userInfo;
}catch (e){
//handle errors as needed
}
};

協程主要解決了線程數量不夠,怕阻塞的問題。讓多個協程共享有限的線程。

3、編譯器翻譯for循環

經典代表是intel的ispc Intel? SPMD Program Compiler

編寫SIMD代碼的時候,我們需要同時考慮多條」數據管線「。下面的%ymm0就是一個256位的寄存器,按32位來算就是8條數據管線。

LBB0_3:
vpaddd %ymm5, %ymm1, %ymm8
vblendvps %ymm7, %ymm8, %ymm1, %ymm1
vmulps %ymm0, %ymm3, %ymm7
vblendvps %ymm6, %ymm7, %ymm3, %ymm3
vpcmpeqd %ymm4, %ymm1, %ymm8
vmovaps %ymm6, %ymm7
vpandn %ymm6, %ymm8, %ymm6
vpand %ymm2, %ymm6, %ymm8
vmovmskps %ymm8, %eax
testl %eax, %eax
jne LBB0_3

如果不用一條指令同時操縱多條數據管線,而是寫那種獨立處理單條數據管線的邏輯就要簡單得多:

float powi(float a, int b) {
float r = 1;
while (b--)
r *= a;
return r;
}

用 ispc.github.io/ispc.html 編譯之後,上面的代碼就可以從SPMD的風格,轉換成SIMD的風格,其實他們是等價的代碼。

4、硬體直接支持海量多線程

經典代表是nvidia的CUDA

__global__ void parallel_shared_reduce_kernel(float *d_out, float* d_in){
int myID = threadIdx.x + blockIdx.x * blockDim.x;
int tid = threadIdx.x;
extern __shared__ float sdata[];
sdata[tid] = d_in[myID];
__syncthreads();
//divide threads into two parts according to threadID, and add the right part to the left one,
//lead to reducing half elements, called an iteration; iterate until left only one element
for(unsigned int s = blockDim.x / 2 ; s>0; s>>=1){
if(tid<s){
sdata[tid] += sdata[tid + s];
}
__syncthreads(); //ensure all adds at one iteration are done
}
if (tid == 0){
d_out[blockIdx.x] = sdata[myId];
}
}

編寫的代碼是逐個像素處理的。但是硬體直接支持很多個線程。

推薦閱讀:

相关文章