什么是原子性
從一個例子說起, x++ ,讀和寫 ,
如圖假設多線程,線程1和線程2同時操作變量x,進行x++的操作,那么由于寫的過程中,都會先讀一份x數據到cpu的寄存器中,所以這個時候cpu1 和 cpu2 拿到了相同的變量x,假設初始x值為1,則cpu1拿到的x為1,cpu2拿到的x為1,都操作并寫回給x后,x的值為2。
預期加兩次,結果為3,但是實際由于多線程同時操作同一個變量了 ,可能產生寫覆蓋。進一步看,這其中還要再提起一個詞,中斷。
中斷
多線程 - cpu中斷
多線程下,常見一個或者多個操作在 CPU 執行時候,中斷,切出再切回。
對于多線程來說,程序在運行一段代碼的時候,可能會中途切出,這種來回切出和切回,就出現了上面x++的情況。產生了寫覆蓋的問題。
那么不用多線程,只用單線程,是不是就不會存在中斷的問題,是不是就安全了,其實也不安全。因為線程下面還有協程(如python Coroutine),或如nodejs中 event loop,其雖然不會在cpu運算的時候切出,但是會在等待io的時候切出。
單線程 - io中斷
單線程下,一個或者多個IO操作執行的過程中,中斷,切出再切回。
一個單線程切出的例子,拿nodejs中event loop舉例,worker1 和 worker2分別產生event,去累加result,但是在累加的過程中會await sleep 模擬等待io,這會導致由于等待io而引起的中斷,切出。
非原子性示例
function sleep(ms: number) {return new Promise(resolve => setTimeout(resolve, ms));
}let result = 0;async function worker1() {let maxtime1 = 1;while(maxtime1 <= 100) {let name = 'worker1';// 執行100次)console.log(`${name} calculate current time ${maxtime1}`)// 開始工作let resultCopy = result;// 讓出await sleep(10);resultCopy += 1;result = resultCopy;maxtime1 += 1;}
}async function worker2() {let maxtime2 = 1;while(maxtime2 <= 100) {let name = 'worker2';// 執行100次console.log(`${name} calculate current time ${maxtime2}`)// 開始工作let resultCopy = result;// 讓出await sleep(10);resultCopy += 1;result = resultCopy;maxtime2 += 1;}
}(async () => {console.log('start calculate')const startTime = Date.now();Promise.all([worker1(), worker2()]).then(() => {const endTime = Date.now();// 預期是200 ,但是由于會寫覆蓋,所以最終小于200.console.log(`耗時: ${endTime - startTime}ms`);console.log('result:', result);}).catch((error) => {console.error('A worker failed with error:', error);});
})()
運行結果,通過結果 ,甚至輸出結果直接就是100,因為worker1 和 worker2的并行執行,導致每次累加計算前,worker1 和 worker2 都拿到相同的值
那么如何避免這種情況,讓worker1的代碼片段執行完,再執行的worker2的代碼片段,不切出,達到原子性,一種方法就是加鎖,下面繼續看如何加鎖達到原子性,
原子性示例
通過加鎖,可以實現代碼片段的原子性 ,如下
import { Mutex } from 'async-mutex';
const mutex = new Mutex();function sleep(ms: number) {return new Promise(resolve => setTimeout(resolve, ms));
}let result = 0;async function worker1() {let maxtime1 = 1;// 執行100次while(maxtime1 <= 100) {let name = 'worker1';// 開始工作// 鎖住,const release = await mutex.acquire();console.log(`${name} calculate current time ${maxtime1}, before start calulate result: ${result}`)// rlet resultCopy = result;// 讓出cpu,這里即使讓出,其它worker由于無法獲取鎖,所以會一直等待await sleep(10);resultCopy += 1;// w result = resultCopy;console.log(`${name} calculate current time ${maxtime1}, after calulate result: ${result}`)release();maxtime1 += 1;}
}async function worker2() {let maxtime2 = 1;// 執行100次while(maxtime2 <= 100) {let name = 'worker2';// 開始工作// 鎖住,const release = await mutex.acquire();console.log(`${name} calculate current time ${maxtime2}, before start calulate result: ${result}`)// rlet resultCopy = result;// 讓出cpuawait sleep(10);resultCopy += 1;// w result = resultCopy;console.log(`${name} calculate current time ${maxtime2}, after calulate result: ${result}`)release();maxtime2 += 1;}
}(async () => {console.log('start calculate')const startTime = Date.now();Promise.all([worker1(), worker2()]).then(() => {const endTime = Date.now();// 預期是200 ,但是由于會寫覆蓋,所以最終小于200.console.log(`耗時: ${endTime - startTime}ms`);console.log('result:', result);}).catch((error) => {console.error('A worker failed with error:', error);});
})()
此時,在看輸出結果,可以發現由于有鎖,worker1 和 worker2是串行累加的,不會在執行累加的過程中切出,所以最終累加的結果是200,符合預期。
同時可以發現,由于加鎖,整體串行,會導致整體運行時間增加。這里就不得不多提下,Event Loop 是一種異步編程模型,io切出本身屬于提高效率的設計,所以如果不是需要原子性,不是同時操作同一個變量,則沒必要加鎖降低效率。
結語
總結 ,對于編程中的原子性,如果說一段代碼是原子性的,則這段代碼無論是cpu 還是 io等待 都不能被切出。這段代碼需要完整的執行,這才是我們預期的一段代碼的原子性。