๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
์ปดํ“จํ„ฐ ๊ตฌ์กฐ & ์šด์˜์ฒด์ œ

[๊ณ ์„ฑ๋Šฅ] CAS(Compare and Swap)๊ณผ Lock-Free Queue

by ์„œ์•„๋ž‘๐Ÿ˜ƒ 2025. 10. 12.

 

๋“ค์–ด๊ฐ€๋ฉฐ

๋™์‹œ์„ฑ ํ”„๋กœ๊ทธ๋ž˜๋ฐ์„ ์ง„ํ–‰ํ•˜๋ฉด์„œ ๊ฐ€์žฅ ํฐ ๋ฌธ์ œ๋Š” ๋ฐ์ดํ„ฐ ๋™๊ธฐํ™”์ž…๋‹ˆ๋‹ค. ์Šค๋ ˆ๋“œ๊ฐ„ ๊ฒฝํ•ฉ(race-condition)์ด ๋ฒŒ์–ด์กŒ์„ ๋•Œ, ๊ณต์œ  ์ž์›์— ๋Œ€ํ•œ ๋™๊ธฐํ™”๋ฅผ ์ง„ํ–‰ํ•˜๊ธฐ ์œ„ํ•ด ๋Œ€ํ‘œ์ ์œผ๋กœ Mutex Lock์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ๊ณ ์„ฑ๋Šฅ ํ”„๋กœ๊ทธ๋ž˜๋ฐ์—์„œ Lock์€ ์ ˆ๋Œ€์•…์ด๋ผ๊ณ  ์ƒ๊ฐํ•˜์‹œ๋ฉด ๋  ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๋ฐ์ดํ„ฐ ๋™๊ธฐํ™”์— ๋Œ€ํ•œ ๋ฌธ์ œ๊ฐ€ ์•„์ง ๋‚จ์•„์žˆ๊ธฐ ๋•Œ๋ฌธ์— Lock์„ ์ตœ์†Œํ™”ํ•˜๋ฉด์„œ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์˜ค๋Š˜์€ Lock์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š์€ ๋ฝํ”„๋ฆฌํ(Lock-Free Queue)์— ๋Œ€ํ•ด ์•Œ์•„๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

 

Producer and Consumer

์ถœ์ฒ˜: https://jenkov.com/tutorials/java-concurrency/producer-consumer.html

๋™์‹œ์„ฑ์„ ์ฒ˜๋ฆฌํ•˜๋Š” ์ „ํ†ต์ ์ธ ๋ฐฉ์‹์ธ ์ƒ์‚ฐ์ž-์†Œ๋น„์ž ํŒจํ„ด(Producer Consumer Pattern)์„ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. ์ด๋ฒคํŠธ/์ด์Šˆ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ์ƒ์‚ฐ์ž ์Šค๋ ˆ๋“œ์—์„œ Queue์— Job์„ Pushํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  Consumer ์Šค๋ ˆ๋“œ์—์„œ Job์„ Popํ•ด์„œ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ์ƒ์‚ฐ์ž๋Š” ๋Š์ž„์—†์ด Job์„ Queue์— ๋„ฃ๊ณ  ์†Œ๋น„์ž๋Š” ๋Š์ž„์—†์ด Job์„ Queue์—์„œ ๋บ๋‹ˆ๋‹ค. ๋งŒ์•ฝ ์†Œ๋น„์ž๊ฐ€ Job์„ ์ฒ˜๋ฆฌํ•˜๋Š” ์†๋„๋ณด๋‹ค ์ƒ์‚ฐ์ž๊ฐ€ Job์„ Pushํ•˜๋Š” ์†๋„๊ฐ€ ๋” ๋น ๋ฅธ ๊ฒฝ์šฐ์—๋Š” ๋ณ‘๋ชฉ์ด ์ผ์–ด๋‚  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ ์—ฌ๋Ÿฌ ์†Œ๋น„์ž ์Šค๋ ˆ๋“œ๋ฅผ ์Šค๋ ˆ๋“œ ํ’€๋กœ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์Šต๋‹ˆ๋‹ค.

 

Lcok-based ๊ตฌ์กฐ(์ถœ์ฒ˜: https://onseok.blog/lock-free/)

์ด ๊ตฌ์กฐ์—์„œ ๊ณต์œ  ์ž์›(Shared Resource)์€ Queue์ž…๋‹ˆ๋‹ค. ์ƒ์‚ฐ์ž๊ฐ€ pushํ•˜๋Š” ๋™์•ˆ ์†Œ๋น„์ž๋Š” pop์„ ๋ชป ํ•˜๊ณ , ์†Œ๋น„์ž๊ฐ€ popํ•˜๋Š” ๋™์•ˆ ์ƒ์‚ฐ์ž๋Š” push๋ฅผ ๋ชป ํ•ฉ๋‹ˆ๋‹ค. ์—ฌ๋Ÿฌ ์Šค๋ ˆ๋“œ๋“ค์ด ๊ณต์œ  ์ž์›์„ ๋†“๊ณ  ๊ฒฝํ•ฉ์„ ๋ฒŒ์ด๋Š” ๋™์•ˆ Busy-waiting์ด ๋ฐœ์ƒํ•˜๊ณ  Context Switching์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ๋ฌผ๋ก  ์ •๋ง ๋ฆฌ์–ผํƒ€์ž„์ด ์•„๋‹ˆ๋ผ 100ms~1000ms ์ •๋„์˜ ๋”œ๋ ˆ์ด๊ฐ€ ํ—ˆ์šฉ๋œ๋‹ค๋ฉด ์ƒ์‚ฐ์ž-์†Œ๋น„์ž ํŒจํ„ด๋„ ๋‚˜์˜์ง€ ์•Š์€ ์„ ํƒ์ž…๋‹ˆ๋‹ค. ์ฝ”๋“œ ๋ณต์žก์„ฑ๊ณผ CPU ๊ฐ€์šฉ๋ฅ  ์ธก๋ฉด์—์„œ ์œ ๋ฆฌํ•œ ๋ถ€๋ถ„์ด ์žˆ์Šต๋‹ˆ๋‹ค. SPSC/MPSC๊ตฌ์กฐ(Single Consumer)์—์„œ๋Š” Lock์ด ํ•„์š”ํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ์ด ํŒจํ„ด์„ ์œ ์šฉํ•˜๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ณ ์„ฑ๋Šฅ ํ”„๋กœ๊ทธ๋ž˜๋ฐ์—์„œ๋Š” Lock Free๋ฅผ ์ง€ํ–ฅํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์—ฌ๊ธฐ์„œ ๋”์šฑ ๋ฐœ์ „๋œ ํ˜•ํƒœ์˜ ๊ตฌ์กฐ๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

 

CAS(Compare and Swap)

Compare and Swap(CAS, ๋น„๊ต ํ›„ ๊ตํ™˜)์€ ๋™์‹œ์„ฑ ์ œ์–ด๋ฅผ ์œ„ํ•œ ํ•ต์‹ฌ ์›์ž์  ์—ฐ์‚ฐ์ž…๋‹ˆ๋‹ค. ๋ฝ์„ ์“ฐ์ง€ ์•Š๊ณ  ๋ฉ€ํ‹ฐ์Šค๋ ˆ๋“œ ํ™˜๊ฒฝ์—์„œ ์•ˆ์ „ํ•˜๊ฒŒ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐฑ์‹ ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ๊ธฐ๋ณธ ๋นŒ๋”ฉ ๋ธ”๋ก์ž…๋‹ˆ๋‹ค. ๋ฉ€ํ‹ฐ ์Šค๋ ˆ๋“œ, ๋ฉ€ํ‹ฐ ์ฝ”์–ด ํ™˜๊ฒฝ์—์„œ ๊ฐ ๋ณ€์ˆ˜๋Š” ์Šค๋ ˆ๋“œ ๋‚ด์˜ ์Šคํƒ(์บ์‹œ)์— ์ €์žฅ๋ฉ๋‹ˆ๋‹ค. ์ด ๋ณ€์ˆ˜์— ๋Œ€ํ•ด์„œ ์Šค๋ ˆ๋“œ์— ์ €์žฅ๋œ ๊ฐ’๊ณผ ๋ฉ”์ธ ๋ฉ”๋ชจ๋ฆฌ์— ์ €์žฅ๋œ ๊ฐ’์„ ๋น„๊ตํ•˜์—ฌ ๊ฐ’์ด ๊ฐ™๋‹ค๋ฉด ์ƒˆ๋กœ์šด ๊ฐ’์œผ๋กœ ์น˜ํ™˜ํ•ด์ค๋‹ˆ๋‹ค. ๊ฐ’์ด ๋‹ค๋ฅด๋ฉด ๊ณ„์† ์žฌ์‹œ๋„๋ฅผ ํ•ฉ๋‹ˆ๋‹ค.

๋ฉ”์ธ ๋ฉ”๋ชจ๋ฆฌ์— ์ €์žฅ๋œ ๊ฐ’๊ณผ CPU ์บ์‹œ์— ์ €์žฅ๋œ ๊ฐ’์ด ๋‹ค๋ฅธ ๊ฒฝ์šฐ๊ฐ€ ์žˆ๋Š”๋ฐ ์ด๋ฅผ ๊ฐ€์‹œ์„ฑ ๋ฌธ์ œ๋ผ๊ณ  ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ๋˜๋Š” ๊ฒƒ์ด CAS ์•Œ๊ณ ๋ฆฌ์ฆ˜์ž…๋‹ˆ๋‹ค. ํ•˜๋“œ์›จ์–ด ์ž์› ์ธก๋ฉด์—์„œ ์Šค๋ ˆ๋“œ๊ฐ„ ์ถฉ๋Œ์ด ๋ฐœ์ƒํ–ˆ์„ ๋•Œ ์ด๋ฅผ ์›์ž์„ฑ์œผ๋กœ ๋ณด์žฅํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค. ์Šค๋ ˆ๋“œ ๊ฒฝํ•ฉ์‹œ ์ตœ์‹ ๊ฐ’์„ ์œ ์ง€ํ•˜๋Š” ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค.

CAS๋Š” ์„ธ ๊ฐœ์˜ ๊ฐ’์„ ๊ฐ€์ง€๊ณ  ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.
1. ๋ฉ”๋ชจ๋ฆฌ ์ฃผ์†Œ (addr): ๋ฐ”๊ฟ€ ๊ฐ’์ด ์ €์žฅ๋œ ์œ„์น˜
2. ์˜ˆ์ƒ ๊ฐ’ (expected): ์šฐ๋ฆฌ๊ฐ€ ์˜ˆ์ƒํ•˜๋Š” ํ˜„์žฌ ๊ฐ’
3. ์ƒˆ ๊ฐ’ (desired): ๋ฐ”๊พธ๊ณ  ์‹ถ์€ ๊ฐ’

if (*addr == expected) {
    *addr = desired;
    return true;   // ์„ฑ๊ณต
} else {
    expected = *addr;  // ์‹คํŒจ ์‹œ ์˜ˆ์ƒ ๊ฐ’ ๊ฐฑ์‹ 
    return false;
}

์ฆ‰, ๋‚ด๊ฐ€ ๋ฐ”๊พธ๋ ค๊ณ  ํ•˜๋Š” ๊ฐ’์ด ์˜ˆ์ƒํ•œ ๊ทธ๋Œ€๋กœ๋ผ๋ฉด ๋ฐ”๊ฟ”์น˜๊ธฐ(์„ฑ๊ณต, ์ง„ํ–‰)ํ•˜๊ณ , ์•„๋‹ˆ๋ฉด ์˜ˆ์ƒ ๊ฐ’์„ ๊ฐฑ์‹ (์‹คํŒจ, ์žฌ์‹œ๋„)ํ•˜๋Š” ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค. 

std::atomic<int> x{10};
int expected = 10;
int desired  = 20;

if (x.compare_exchange_weak(expected, desired)) {
    // x == 20 ์œผ๋กœ ์„ฑ๊ณต์ ์œผ๋กœ ๊ต์ฒด๋จ
} else {
    // ์‹คํŒจ, expected์—๋Š” x์˜ ์‹ค์ œ ๊ฐ’์ด ๋“ค์–ด์žˆ์Œ
}

์ถœ์ฒ˜: https://www.semanticscholar.org/paper/Lock-free-linked-lists-using-compare-and-swap-Valois/c3c80b53e71dd65b66e712b99e19b21434cd43d3

CAS ์•Œ๊ณ ๋ฆฌ์ฆ˜์˜ ์žฅ์ ์€ ์„ฑ๋Šฅ ๊ทธ์ž์ฒด์ž…๋‹ˆ๋‹ค. ๋†’์€ ์„ฑ๋Šฅ์˜ ๋™์‹œ์„ฑ ์ž๋ฃŒ๊ตฌ์กฐ๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์Šค๋ ˆ๋“œ ๊ฐ„ ์ถฉ๋Œ์ด ์žˆ์–ด๋„ ์ „์ฒด ์‹œ์Šคํ…œ์ด ๋ฉˆ์ถ”์ง€ ์•Š๊ณ  ๊ณ„์† ์ง„ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

CAS ์—ฐ์‚ฐ(์ถœ์ฒ˜: https://onseok.blog/lock-free/)

๋‘ ์Šค๋ ˆ๋“œ ๋ชจ๋‘ ๋ฉ”์ธ ๋ฉ”๋ชจ๋ฆฌ์—์„œ ๊ฐ’ 3์„ ์ฝ์Šต๋‹ˆ๋‹ค. Thread 2๊ฐ€ ์„ฑ๊ณต(๋…น์ƒ‰)ํ•˜์—ฌ ๋ณ€์ˆ˜๋ฅผ 8๋กœ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค. Thread 1์˜ ์ฒซ ๋ฒˆ์งธ CAS๋Š” ๊ฐ’์ด ์—ฌ์ „ํžˆ 3์ด๊ธฐ๋ฅผ ๊ธฐ๋Œ€ํ•˜๋ฏ€๋กœ, CAS๋Š” ์‹คํŒจ(๋นจ๊ฐ„์ƒ‰)ํ•ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ Thread 1์€ ๊ฐ’์„ ๋‹ค์‹œ ์ฝ๊ณ , ๋‘ ๋ฒˆ์งธ CAS๋Š” ์„ฑ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์„œ ์ค‘์š”ํ•œ ์ ์€ CAS๊ฐ€ ์ž๋ฃŒ๊ตฌ์กฐ์— ๋ฝ์„ ํš๋“ํ•˜์ง€ ์•Š๊ณ , ์—…๋ฐ์ดํŠธ๊ฐ€ ์„ฑ๊ณตํ•˜๋ฉด 'true'๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ณ , ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด 'false'๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

๋‹จ์ ์€ ABA๋ฌธ์ œ์™€ Busy-wait์ด ์žˆ์Šต๋‹ˆ๋‹ค. ABA๋Š” ๊ฐ’์ด A -> B -> A๋กœ ๋ณ€ํ•˜๋ฉด CAS๋Š” "์•ˆ ๋ฐ”๋€Œ์—ˆ๋‹ค"๊ณ  ์ฐฉ๊ฐํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ฒ„์ „ ๋ฒˆํ˜ธ๋ฅผ ๋ถ™์ด๊ฑฐ๋‚˜ ๋ณ„๋„์˜ Pointer๋ฅผ ๋‹ฌ์•„์ฃผ๊ธฐ๋„ ํ•ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ ์Šค๋ ˆ๋“œ๊ฐ„ ๊ฒฝํ•ฉ์ด ์‹ฌํ•ด์งˆ ๊ฒฝ์šฐ CAS๊ฐ€ ๊ณ„์† ์‹คํŒจํ•˜๋Š”๋ฐ ์ด๋Š” CPU๋‚ญ๋น„๋กœ ์ด์–ด์ง‘๋‹ˆ๋‹ค. ๊ฒฝ์Ÿ ์ƒํƒœ์—์„œ ๋ฝํ”„๋ฆฌ๋Š” CPU๋ฅผ ํƒœ์›Œ์„œ ๊ธฐ๋‹ค๋ฆฌ๊ณ  ๋ฎคํ…์Šค๋Š” OSํ•œํ…Œ ๋งก๊ธฐ๊ณ  ์ž ๋“ญ๋‹ˆ๋‹ค. "CPU ๋ถ€ํ•˜๊ฐ€ ๋†’๋”๋ผ๋„ Sleep๋ณด๋‹ค๋Š” ์ข‹์ง€ ์•Š์„๊นŒ"๋ผ๊ณ  ์ƒ๊ฐ๋˜๋Š” ๋ถ€๋ถ„์ž…๋‹ˆ๋‹ค.

 

Lock-Free queue ์˜ˆ์ œ

๋ณดํŽธ์ ์œผ๋กœ ์‚ฌ์šฉ๋˜๋Š” Michael-Scott lock-free queue๋ฅผ ์‚ฌ์šฉํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. AI๊ฐ€ ์ •๋ฆฌํ•œ ๋‚ด์šฉ์„ ํ† ๋Œ€๋กœ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

// lock_free_queue_ms.hpp
#pragma once

#include <atomic>
#include <memory>
#include <optional>

// Michael-Scott lock-free queue (๊ต์œก/์‹ค์Šต์šฉ ๊ตฌํ˜„)
// ์ฃผ์˜: ์ƒ์‚ฐ ํ™˜๊ฒฝ์—์„œ๋Š” ๋ฉ”๋ชจ๋ฆฌ ์žฌ์‚ฌ์šฉ/ํšŒ์ˆ˜(ABA ๋ฐฉ์ง€)๋ฅผ ์œ„ํ•œ ์ถ”๊ฐ€ ๊ธฐ๋ฒ• ํ•„์š”.
// - ๊ถŒ์žฅ: Hazard pointers, Epoch-based reclamation, or use a well-tested library (boost::lockfree, folly).

template<typename T>
class LockFreeQueue {
private:
    struct Node {
        std::shared_ptr<T> data;       // shared_ptr๋กœ ๋ฐ์ดํ„ฐ ์ˆ˜๋ช… ๊ด€๋ฆฌ
        std::atomic<Node*> next;

        Node() : data(nullptr), next(nullptr) {}
        explicit Node(T const& value) : data(std::make_shared<T>(value)), next(nullptr) {}
    };

    std::atomic<Node*> head;
    std::atomic<Node*> tail;

public:
    LockFreeQueue() {
        // ๋”๋ฏธ ๋…ธ๋“œ๋กœ ์ดˆ๊ธฐํ™” (MS ์•Œ๊ณ ๋ฆฌ์ฆ˜์˜ ํ‘œ์ค€)
        Node* dummy = new Node();
        head.store(dummy, std::memory_order_relaxed);
        tail.store(dummy, std::memory_order_relaxed);
    }

    ~LockFreeQueue() {
        // ๋‚จ์•„ ์žˆ๋Š” ๋…ธ๋“œ๋“ค ํ•ด์ œ
        while (Node* old = head.load(std::memory_order_relaxed)) {
            head.store(old->next.load(std::memory_order_relaxed), std::memory_order_relaxed);
            delete old;
        }
    }

    // enqueue: ๋์— ๋…ธ๋“œ ์ถ”๊ฐ€
    void enqueue(T const& value) {
        Node* newNode = new Node(value);
        while (true) {
            Node* last = tail.load(std::memory_order_acquire);
            Node* next = last->next.load(std::memory_order_acquire);

            if (last == tail.load(std::memory_order_acquire)) {
                if (next == nullptr) {
                    // last->next๊ฐ€ ๋น„์–ด์žˆ์œผ๋ฉด ์ƒˆ ๋…ธ๋“œ๋ฅผ ์—ฐ๊ฒฐ ์‹œ๋„
                    if (std::atomic_compare_exchange_weak_explicit(
                            &last->next,
                            &next,
                            newNode,
                            std::memory_order_release,
                            std::memory_order_relaxed)) {
                        // ์„ฑ๊ณต์ ์œผ๋กœ ์—ฐ๊ฒฐํ–ˆ์œผ๋ฉด tail์„ ์—…๋ฐ์ดํŠธ ์‹œ๋„
                        std::atomic_compare_exchange_weak_explicit(
                            &tail,
                            &last,
                            newNode,
                            std::memory_order_release,
                            std::memory_order_relaxed);
                        return;
                    }
                } else {
                    // ๋‹ค๋ฅธ ์Šค๋ ˆ๋“œ๊ฐ€ ์ด๋ฏธ ์—ฐ๊ฒฐํ–ˆ์œผ๋ฏ€๋กœ tail์„ ์•ž์œผ๋กœ ๋‹น๊ธด๋‹ค
                    std::atomic_compare_exchange_weak_explicit(
                        &tail,
                        &last,
                        next,
                        std::memory_order_release,
                        std::memory_order_relaxed);
                }
            }
            // CAS ์‹คํŒจ ์‹œ ๋ฃจํ”„ ๋ฐ˜๋ณต
        }
    }

    // try_pop: ์„ฑ๊ณตํ•˜๋ฉด optional<T> ๋ฐ˜ํ™˜, ์‹คํŒจํ•˜๋ฉด nullopt
    std::optional<T> try_pop() {
        while (true) {
            Node* first = head.load(std::memory_order_acquire);
            Node* last  = tail.load(std::memory_order_acquire);
            Node* next  = first->next.load(std::memory_order_acquire);

            if (first == head.load(std::memory_order_acquire)) {
                if (first == last) {
                    if (next == nullptr) {
                        // ํ๊ฐ€ ๋น„์–ด์žˆ์Œ
                        return std::nullopt;
                    }
                    // tail์ด ๋’ค์ณ์ ธ ์žˆ์œผ๋ฏ€๋กœ ๋‹น๊ฒจ์ค€๋‹ค
                    std::atomic_compare_exchange_weak_explicit(
                        &tail,
                        &last,
                        next,
                        std::memory_order_release,
                        std::memory_order_relaxed);
                } else {
                    // ์‹ค์ œ๋กœ pop ํ•  ์ˆ˜ ์žˆ๋Š” ๋…ธ๋“œ(next)๊ฐ€ ์กด์žฌ
                    std::shared_ptr<T> result = next->data;
                    // head๋ฅผ next๋กœ ๊ต์ฒด (CAS)
                    if (std::atomic_compare_exchange_weak_explicit(
                            &head,
                            &first,
                            next,
                            std::memory_order_release,
                            std::memory_order_relaxed)) {
                        // ์•ˆ์ „ํ•˜๊ฒŒ ์ฒซ ๋…ธ๋“œ ์‚ญ์ œ
                        // <-- ์ฃผ์˜: ์ด ๋‹จ๊ณ„์—์„œ ๋ฉ”๋ชจ๋ฆฌ ์žฌ์‚ฌ์šฉ(ABA) ๋ฌธ์ œ๊ฐ€ ์žˆ์„ ์ˆ˜ ์žˆ์Œ.
                        delete first;
                        if (result) return *result;
                        else return std::nullopt;
                    }
                }
            }
            // CAS ์‹คํŒจ ์‹œ ๋ฐ˜๋ณต
        }
    }

    bool empty() const {
        Node* h = head.load(std::memory_order_acquire);
        Node* n = h->next.load(std::memory_order_acquire);
        return n == nullptr;
    }
};

 

  1. ๋”๋ฏธ(dummy) ๋…ธ๋“œ: MS ํ๋Š” ํ•ญ์ƒ ํ•˜๋‚˜์˜ ๋”๋ฏธ ๋…ธ๋“œ๋ฅผ ์œ ์ง€ํ•˜์—ฌ head์™€ tail ์—…๋ฐ์ดํŠธ๋ฅผ ๋‹จ์ˆœํ™”ํ•ฉ๋‹ˆ๋‹ค. ์‹ค์ œ ์ฒซ ์œ ํšจ ๋ฐ์ดํ„ฐ๋Š” head->next์—์„œ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค.
  2. ์›์ž์„ฑ(atomic): head, tail, ๊ทธ๋ฆฌ๊ณ  ๊ฐ ๋…ธ๋“œ์˜ next๋Š” std::atomic<Node*>๋กœ ์„ ์–ธ๋˜์–ด CAS๋กœ ๊ฐฑ์‹ ํ•ฉ๋‹ˆ๋‹ค.
  3. enqueue ํ๋ฆ„:
    • tail๊ณผ tail->next๋ฅผ ์ฝ๊ณ , tail->next๊ฐ€ nullptr์ด๋ฉด ์—ฌ๊ธฐ์— ์ƒˆ ๋…ธ๋“œ๋ฅผ CAS๋กœ ์—ฐ๊ฒฐ.
    • ์—ฐ๊ฒฐ ์„ฑ๊ณตํ•˜๋ฉด tail์„ ์ƒˆ ๋…ธ๋“œ๋กœ CAS๋กœ ์ด๋™(์‹คํŒจํ•ด๋„ ๋ฌดํ•ด).
  4. dequeue ํ๋ฆ„:
    • head, tail, head->next๋ฅผ ์ฝ๋Š”๋‹ค.
    • head == tail ์ด๊ณ  head->next == nullptr์ด๋ฉด ํ๋Š” ๋น„์–ด์žˆ์Œ.
    • head != tail์ด๋ฉด head๋ฅผ head->next๋กœ CAS๋กœ ์˜ฎ๊ธฐ๊ณ , ์„ฑ๊ณต ์‹œ ์ด์ „ head๋Š” ์‚ญ์ œ.

 

์ถ”๊ฐ€ ์ฒดํฌ ํฌ์ธํŠธ

  1. ABA ๋ฌธ์ œ: ํฌ์ธํ„ฐ๊ฐ€ A -> B -> A์ฒ˜๋Ÿผ ์žฌ์‚ฌ์šฉ๋˜๋ฉด CAS๊ฐ€ ์„ฑ๊ณตํ•ด๋„ ์ž˜๋ชป๋œ ์ƒํƒœ๊ฐ€ ๋  ์ˆ˜ ์žˆ์Œ. ์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋ ค๋ฉด ํฌ์ธํ„ฐ์— ํƒœ๊ทธ(์นด์šดํ„ฐ)๋ฅผ ๋ถ™์ด๊ฑฐ๋‚˜ Hazard Pointers / epoch-based reclamation ๋“ฑ์„ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  2. ๋ฉ”๋ชจ๋ฆฌ ์žฌ์‚ฌ์šฉ ๋ฐ ์•ˆ์ „ํ•œ ์‚ญ์ œ: ์œ„ ๊ตฌํ˜„์€ ๊ฐ„๋‹จํ™” ์œ„ํ•ด delete first;๋ฅผ ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๋‹ค๋ฅธ ์Šค๋ ˆ๋“œ๊ฐ€ ์—ฌ์ „ํžˆ ํ•ด๋‹น ๋…ธ๋“œ ์ฃผ์†Œ๋ฅผ ์ฐธ์กฐํ•˜๊ณ  ์žˆ์„ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ์•ˆ์ „ํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์•ˆ์ „ํ•œ ๋ฐฉ๋ฒ•:
    • Hazard pointers (๊ถŒ์žฅ) — ๋…ธ๋“œ๋ฅผ ์ ‘๊ทผ ์ค‘์ธ ์Šค๋ ˆ๋“œ๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ํ‘œ์‹œํ•ด์„œ ์•ˆ์ „ํ•  ๋•Œ๋งŒ ์‚ญ์ œ.
    • Epoch-based reclamation (๋” ๋น ๋ฆ„, ๋ณต์žก์„ฑ ์žˆ์Œ).
    • ํ˜น์€ ์ด๋ฏธ ๊ฒ€์ฆ๋œ ๊ตฌํ˜„ ์‚ฌ์šฉ: boost::lockfree::queue, Folly, concurrency kit ๋“ฑ.
  3. ๋ฉ”๋ชจ๋ฆฌ ์˜ค๋ฒ„ํ—ค๋“œ vs ์„ฑ๋Šฅ: shared_ptr๋ฅผ ๋…ธ๋“œ ๋ฐ์ดํ„ฐ์— ์‚ฌ์šฉํ•˜๋ฉด ์•ˆ์ „ํ•˜๋‚˜ ํ• ๋‹น/์ฐธ์กฐ ์นด์šดํŠธ ๋น„์šฉ์ด ์ƒ๊น๋‹ˆ๋‹ค. ์ดˆ๊ณ ์„ฑ๋Šฅ(low-latency) ํ™˜๊ฒฝ์—์„œ๋Š” hazard/epoch ๋ฐฉ์‹์œผ๋กœ raw pointer์™€ ๋น ๋ฅธ reclamation ์กฐํ•ฉ์ด ์„ ํ˜ธ๋ฉ๋‹ˆ๋‹ค.

 

์กฐ๊ธˆ ๋” ์‰ฌ์šด ๋ฒ„์ „์ธ CAS stack ์˜ˆ์ œ๋„ ํ•จ๊ป˜ ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

#include <atomic>
#include <iostream>
#include <thread>

template <typename T>
struct Node {
    T value;
    Node* next;
    Node(T v) : value(v), next(nullptr) {}
};

template <typename T>
class LockFreeStack {
private:
    std::atomic<Node<T>*> head;

public:
    LockFreeStack() : head(nullptr) {}

    void push(T value) {
        Node<T>* newNode = new Node<T>(value);
        newNode->next = head.load(std::memory_order_relaxed);

        // head๋ฅผ newNode๋กœ ๊ต์ฒด ์‹œ๋„ (CAS ๋ฃจํ”„)
        while (!head.compare_exchange_weak(
            newNode->next, newNode,
            std::memory_order_release,
            std::memory_order_relaxed)) {
            // ์‹คํŒจํ•˜๋ฉด newNode->next๋ฅผ ์ตœ์‹  head๋กœ ๊ฐฑ์‹ ํ•˜๊ณ  ์žฌ์‹œ๋„
        }
    }

    bool pop(T& result) {
        Node<T>* oldHead = head.load(std::memory_order_relaxed);

        // head๋ฅผ oldHead->next๋กœ ๊ต์ฒด ์‹œ๋„ (CAS ๋ฃจํ”„)
        while (oldHead != nullptr &&
               !head.compare_exchange_weak(
                   oldHead, oldHead->next,
                   std::memory_order_acquire,
                   std::memory_order_relaxed)) {
            // ์‹คํŒจ ์‹œ oldHead๊ฐ€ ์ตœ์‹  ๊ฐ’์œผ๋กœ ์ž๋™ ๊ฐฑ์‹ ๋จ
        }

        if (oldHead == nullptr) return false;

        result = oldHead->value;
        delete oldHead;
        return true;
    }
};

int main() {
    LockFreeStack<int> stack;

    // ์—ฌ๋Ÿฌ ์Šค๋ ˆ๋“œ์—์„œ ๋™์‹œ์— push/pop
    std::thread t1([&]() {
        for (int i = 0; i < 5; i++) stack.push(i);
    });

    std::thread t2([&]() {
        for (int i = 5; i < 10; i++) stack.push(i);
    });

    t1.join();
    t2.join();

    int value;
    while (stack.pop(value)) {
        std::cout << value << " ";
    }
    std::cout << std::endl;

    return 0;
}

CAS ๊ธฐ๋ฐ˜์˜ Lock-Free Stack ๊ธฐ๋ณธ ํ˜•ํƒœ์ž…๋‹ˆ๋‹ค. ๋ฝ ์—†์ด ์—ฌ๋Ÿฌ ์Šค๋ ˆ๋“œ๊ฐ€ ๋™์‹œ์— `push`์™€ `pop`์„ ์•ˆ์ „ํ•˜๊ฒŒ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‘ ์˜ˆ์ œ ๋ชจ๋‘ ABA ํ•ด๊ฒฐ ์ „๋žต๊ณผ ๋ฉ”๋ชจ๋ฆฌ ํšŒ์ˆ˜ ์ „๋žต์ด ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค. ๋ณด์—ฌ๋“œ๋ฆฐ ์˜ˆ์ œ์—์„œ๋Š” ๊ฐ„๋‹จํ•œ ์˜ˆ์ œ์—์„œ๋Š” ๋ฌธ์ œ๊ฐ€ ์—†์ง€๋งŒ ๋ณต์žกํ•œ ์‹ค๋ฌด์—์„œ๋Š” ์ถ”๊ฐ€์ ์ธ ์˜ˆ์™ธ ์žฅ์น˜๊ฐ€ ๋ฐ˜๋“œ์‹œ ํ•„์š”ํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  C++์—์„œ์˜ compare_exchange_weak, memory_order๋Š” ๋”ฐ๋กœ ํฌ์ŠคํŒ…์„ ์ง„ํ–‰ํ•  ๊ณ„ํš์ž…๋‹ˆ๋‹ค. 

 

Lock-free๊ตฌ์กฐ(์ถœ์ฒ˜: https://onseok.blog/lock-free/)



๋งˆ์น˜๋ฉฐ

CAS๋ฅผ ํ†ตํ•œ ๊ฒฝ์Ÿ์ƒํƒœ ํ•ด๊ฒฐ๋ฐฉ์‹์€ ๊ธฐ์กด Lock๊ณผ๋Š” ์กฐ๊ธˆ ๋‹ค๋ฆ…๋‹ˆ๋‹ค. while๋ฌธ์„ ๋Œ€๊ธฐ์—†์ด ๋ฌดํ•œ๋ฃจํ”„์ฒ˜๋Ÿผ ๋ฐ˜๋ณตํ•˜๋ฉด์„œ waiting์„ ํ•˜๋Š” ๋ชจ์Šต์ž…๋‹ˆ๋‹ค. ๋ˆ„๊ตฐ๊ฐ€๋Š” backoff ์ „๋žต์œผ๋กœ 1~10ns sleep์„ ์ ์šฉํ•œ๋‹ค๊ณ  ํ•˜์ง€๋งŒ, ์ด๋Š” ๋ฌธ๋งฅ ๊ตํ™˜์ด ๋ฐœ์ƒํ•  ์—ฌ์ง€๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ๋Œ€๊ธฐ ์—†๋Š” polling์ด ์–ด์ฉŒ๋ฉด ์„ฑ๋Šฅ์—๋Š” ๊ฐ€์žฅ ์ข‹์€ ์ „๋žต์ด ์•„๋‹๊นŒ ์‹ถ์Šต๋‹ˆ๋‹ค. Lock Free์˜ ๊ธฐ์ดˆ๊ฐœ๋…์ธ CAS ์•Œ๊ณ ๋ฆฌ์ฆ˜์€, CPU๊ฐ€ ์ง„ํ–‰ํ•˜๋ ค๋Š” ํ๋ฆ„์„ OS ๊ฐ„์„ญ(Context Switching) ์—†์ด ์บ์‹œ ์ง€์—ญ์„ฑ์„ ๋†’์ผ ์ˆ˜ ์žˆ๋Š” ์ค‘์š” ๊ฐœ๋…์ด๋ผ๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค.

 

 

์ฐธ๊ณ 

https://onseok.blog/lock-free/

https://en.wikipedia.org/wiki/Compare-and-swap

๋Œ“๊ธ€