리눅스 커널에서 널리 쓰이는 READ_ONCE()/WRITE_ONCE() 매크로의 의미와, 커널 Rust 코드에서는 이를 그대로 노출하기보다 Atomic 기반 접근을 선호하게 된 배경 및 그 파급효과를 다룬다.
URL: https://lwn.net/SubscriberLink/1053142/8ec93e58d5d3cc06/
Title: READ_ONCE(), WRITE_ONCE(), but not for Rust
* [LWN FAQ](https://lwn.net/op/FAQ.lwn)
* [원고를 보내주세요](https://lwn.net/op/AuthorGuide.lwn)
사용자: 비밀번호: | |
LWN.net에 오신 것을 환영합니다
다음 구독자 전용 콘텐츠는 LWN 구독자 한 분이 여러분께 제공해 주셨습니다. 수천 명의 구독자가 리눅스 및 자유 소프트웨어 커뮤니티의 최고의 뉴스를 위해 LWN에 의존하고 있습니다. 이 글이 마음에 드셨다면 LWN 구독을 고려해 주세요. LWN.net을 방문해 주셔서 감사합니다!
글: Jonathan Corbet
2026년 1월 9일
READ_ONCE()와 WRITE_ONCE() 매크로는 커널 내부에서 광범위하게 사용되고 있다. READ_ONCE()만 해도 호출 지점이 거의 8,000곳에 이른다. 이 매크로들은 많은 락-프리(잠금 없는) 알고리즘 구현의 핵심이며, 일부 유형의 디바이스 메모리 접근에는 필수일 수 있다. 따라서 커널 내 Rust 코드의 비중이 늘어날수록, 이 매크로의 Rust 버전도 필요해질 것이라 생각하기 쉽다. 하지만 실제로는 Rust 커뮤니티가 동시성 데이터 접근에 대해 다른 접근을 택하려는 것으로 보인다.
READ_ONCE()와 WRITE_ONCE()를 이해하는 것은 어떤 형태로든 데이터에 대한 동시 접근을 다루는 커널 개발자에게 중요하다. 그러니 당연히 커널 문서에는 이 내용이 거의 전혀 없다. (역주: 반어적 표현) 그래도 일종의 설명은 include/asm-generic/rwonce.h 상단에서 찾을 수 있다.
컴파일러가 읽기나 쓰기를 병합하거나 다시 가져오는(refetch) 것을 막는다. 또한 컴파일러는 READ_ONCE 및 WRITE_ONCE의 연속된 인스턴스들을 재정렬(reorder)하는 것도 금지된다. 다만 이는 컴파일러가 어떤 특정한 순서(order)를 인지하고 있을 때에만 해당한다. 컴파일러가 순서를 인지하게 만드는 한 가지 방법은 READ_ONCE 또는 WRITE_ONCE의 두 호출을 서로 다른 C 문(statement)에 배치하는 것이다.
즉, READ_ONCE() 호출은 컴파일러가 지정된 위치에서 정확히 한 번만 읽도록 강제하며, 읽기가 생략되거나 반복되도록 만드는 최적화 기법을 허용하지 않는다. WRITE_ONCE()는 같은 조건으로 쓰기를 강제한다. 또한 이 매크로들은 해당 접근이 원자적(atomic)임을 보장한다. 한 태스크가 READ_ONCE()로 어떤 위치를 읽는 동안 다른 태스크가 그 위치에 쓰기를 수행한다면, 읽기는 쓰기 이전 또는 이후의 값 중 하나를 반환해야 하며, 둘이 섞인 임의의 조합을 반환해서는 안 된다. 위에서 설명된 것 외에는 컴파일러나 CPU에 어떤 순서 제약도 부과하지 않으므로, 더 강한 순서 요구사항이 있는 smp_load_acquire() 같은 매크로와는 다르다.
READ_ONCE()와 WRITE_ONCE() 매크로는 2014년 3.18 릴리스에 추가되었다. WRITE_ONCE()는 처음에는 ASSIGN_ONCE()라는 이름이었지만, 3.19 개발 사이클 동안 이름이 변경되었다.
2025년 마지막 날, Alice Ryhl은 Rust용 READ_ONCE() 및 WRITE_ONCE() 구현을 추가하는 패치 시리즈를 게시했다. 그녀는 해당 호출이 उपलब्ध해지면 코드의 여러 위치에서 volatile read를 이 호출로 대체할 수 있다고 했다. 다른 변경 중 하나로, 시리즈는 struct file의 f_flags 필드 접근을 READ_ONCE()를 사용하도록 바꾸었다. 이 매크로의 구현은 Rust 매크로 마법이 꽤 많이 들어가지만, 결국에는 Rust의 read_volatile() 및 write_volatile() 함수 호출로 귀결된다.
하지만 다른 커널 Rust 개발자들 일부는 이 변경에 반대했다. Gary Guo는 READ_ONCE()와 WRITE_ONCE()를 노출하고 싶지 않다고 말하며, 대신 Rust Atomic 크레이트 커널의 Atomic 모듈에서 제공하는 relaxed 연산을 사용하자고 제안했다. Boqun Feng은 반대 이유를 좀 더 확장해서 설명했다.
READ_ONCE()와 WRITE_ONCE()의 문제는 의미론(semantics)이 복잡하다는 것이다. 때로는 원자성(atomicity)을 위해 쓰이고, 때로는 데이터 레이스를 방지하기 위해 쓰인다. 그렇다, Rust에서도 LKMM(리눅스 커널 메모리 모델)을 사용하지만, 가능하다면 API의 의도를 명확히 해야 한다. 그런 측면에서 Atomic::from_ptr().load(Relaxed)를 쓰는 것이 도움이 된다. IMO(내 의견으로는), READ_ONCE()/WRITE_ONCE()는 몇 가지 문제에 대한 "반창고" 같은 해결책이다. 이것이 존재하면 동시성 프로그래밍에 대해 더 명확한 관점을 발전시키는 것을 방해하게 될 것이다.
즉, Atomic 크레이트(여기서는 커널의 Atomic 모듈)를 사용하면 개발자가 어떤 보장이 필요한지 더 정확히 지정할 수 있어 코드의 기대(및 요구)가 더 명확해진다. 이 관점이 받아들여진 것으로 보이며, Ryhl은 — 적어도 지금은 — 커널 Rust 코드에 이 추가를 더 이상 밀어붙이지 않고 있다.
이 결론이 유지된다면 몇 가지 흥미로운 함의가 있다. 첫째, Rust 코드가 커널의 핵심 영역으로 더 깊이 들어갈수록, 공유 데이터에 대한 동시 접근 코드는 동일한 데이터를 다루더라도 C 코드와 상당히 다른 모습이 될 것이다. 락-프리 데이터 접근을 이해하는 것은 API 하나만 있어도 충분히 어렵다. 이제 개발자들은 서로 다른 두 API를 이해해야 할지도 모르며, 이는 일을 더 쉽게 만들지 않을 것이다.
한편 이 논의는 C 쪽 코드에도 주목을 끌고 있다. Feng이 지적했듯, 커널에는 여전히 많은 상황에서 단순한 plain write가 원자적일 것이라고 가정하는 C 코드가 남아 있지만, C 표준은 명시적으로 그렇지 않다고 말한다. Peter Zijlstra는 그러한 코드는 모두 WRITE_ONCE()를 올바르게 사용하도록 업데이트해야 한다고 답했다. 문제는 그런 코드를 찾는 것 자체가 도전일 수 있다는 점이다(다만 KCSAN이 도움이 될 수 있다). 모두 업데이트하는 데에도 시간이 걸릴 것이다. 또한 이 대화는 (C) 고해상도 타이머 코드에서 필요한 READ_ONCE() 호출이 빠진 지점을 하나 찾아냈다. 이것은 Rust 작업이 C 코드 개선으로 이어진 또 다른 사례다.
과거 Rust 추상화 설계에 대한 논의에서는, C 대응물과 상당히 다른 모습을 가진 Rust 인터페이스를 만드는 것에 대한 저항이 있어 왔다. 예를 들어 2024년 기사를 보라. Rust 개발자들이 더 나은 인터페이스 설계를 만들어낸다면, C 쪽도 그 새로운 설계에 맞춰 개선되어야 한다는 생각이었다. Rust의 READ_ONCE() 및 WRITE_ONCE() 접근이 기존 방식보다 낫다는 생각을 받아들인다면, 여기에서도 유사한 과정이 따라야 한다고 결론 내릴 수도 있다. 다만 수천 개의 저수준 동시성 프리미티브를 더 정확한 의미론을 지정하도록 바꾸는 작업은 결코 만만치 않을 것이다. 결국 두 언어의 코드는 그냥 서로 다르게 동작(또는 표현)하는 사례가 될지도 모른다.
댓글을 게시하려면
2026년 1월 9일 17:19 UTC(금) mb (구독자, #50428) [링크] (응답 9개)
결국 Rust read_volatile() 및 write_volatile() 함수 호출로 귀결된다.
나는 AVR에서 volatile read/write를 실험해 보았는데, 특정한 스레드 간(인터럽트) 통신에서 이를 쓰는 것이 크게 유익할 수 있었다.
마치 모든 AVR C 프로그램이 이를 위해 volatile "원자" 변수를 사용하는 것과 같은 방식이다.
https://github.com/mbuesch/avr-atomic
하지만 나는 스레드 간(인터럽트) 통신에 read/write volatile을 사용하는 것은 Rust에서 sound하지 않다는 결론에 이르렀다.
AVR 하드웨어는 바이트 크기 객체에 대해 비원자적 read/write를 수행할 수 없다. 따라서 관련된 모든 경우에서 하드웨어는 문제가 없다.
하지만 read/write volatile 문서는 Rust 가상 머신이 동시 volatile 접근을 sound하지 않다고 간주하며 이를 마음대로 최적화해도 된다고(엉뚱하게 만들어도 된다고) 명확히 말하는 듯했다.
https://doc.rust-lang.org/std/ptr/fn.read_volatile.html
어떤 연산이 volatile인지 여부는 여러 스레드로부터의 동시 접근과 관련된 문제에는 아무런 영향이 없다
Volatile 접근은 그 점에서 비원자적 접근과 정확히 동일하게 동작한다.
이 접근은 여전히 원자적(atomic)으로 간주되지 않으며, 따라서 스레드 간 동기화에 사용할 수 없다.
내 구현은 잘 동작했고, 생성된 어셈블리 코드는 완벽했다.
그러나 Rust 문서가 추가 동기화 없이 동시 read/write volatile을 사용하는 것을 sound하지 않다고 보는 것 같아서, 덜 최적이지만 inline asm 구현으로 바꿨다.
(맞다, AtomicXX가 있는 건 알고 있고, 여러 이유로 효율적이지 않다는 것도 안다... 그리고 문제를 우회하는 대신 컴파일러의 atomic intrinsic을 고쳐야 한다는 것도 안다... 알고 있다 :)
2026년 1월 9일 19:20 UTC(금) josh (구독자, #17465) [링크] (응답 8개)
맞다, AtomicXX가 있는 건 알고 있고, 여러 이유로 효율적이지 않다는 것도 안다
Relaxed atomic은 사실상 컴파일러 배리어(compiler-barrier) atomic이며, 런타임 오버헤드가 없어야 한다. 그보다 비효율이 더 큰 경우를 겪고 있나?
2026년 1월 9일 19:54 UTC(금) mb (구독자, #50428) [링크] (응답 1개)
그렇다. 없어야 한다.
말했듯이, AVR에서의 LLVM 문제다. (AVR은 안정적인 티어가 아니다.)
현재 컴파일러에서 AVR의 Atomic은 항상 무거운 동기화를 사용한다.
하지만 그게 내 요점은 아니다.
내 요점은 volatile 접근이 스레드 간 통신에서 Rust에서는 sound하지 않다고 생각한다는 것이다.
2026년 1월 9일 22:10 UTC(금) josh (구독자, #17465) [링크]
말했듯이, AVR에서의 LLVM 문제다. (AVR은 안정적인 티어가 아니다.)
현재 컴파일러에서 AVR의 Atomic은 항상 무거운 동기화를 사용한다.
아, 이해했다. 알려줘서 고맙다. 고쳐지면 좋겠다.
내 요점은 volatile 접근이 스레드 간 통신에서 Rust에서는 sound하지 않다고 생각한다는 것이다.
맞다, 그게 맞는 것 같다.
2026년 1월 10일 9:33 UTC(토) plugwash (구독자, #29694) [링크] (응답 5개)
Relaxed atomic은 사실상 컴파일러 배리어 atomic이며, 런타임 오버헤드가 없어야 한다.
상황은 그보다 조금 더 미묘하다.
특정 메모리 위치에 대한 relaxed atomic은, (다른 메모리 위치에 대한 연산과는 펜스(fence)를 쓰지 않으면 순서가 다를 수 있긴 하지만) 마치 잘 정의된 순서가 있는 것처럼 동작해야 하며, 이 요구는 그 위치에 대한 원자 연산 전체 집합에 적용되어야 한다. 특정 위치에 대해 load와 store만 사용하고 있을 수도 있지만, 컴파일러는 그것을 알 수 없다. 다른 코드가 그 위치에서 다른 atomic 연산을 수행할 수도 있다.
내 이해로는, 이것은 전역 락(global lock)을 사용해 read-modify-write 연산을 구현한다면, 단순한 write 연산도 그 동일한 전역 락을 사용하여 구현해야 함을 사실상 의미한다.
2026년 1월 10일 20:13 UTC(토) comex (구독자, #71521) [링크] (응답 1개)
그 말은 맞지만, Rust에는 실제로 적용되지 않는다. Rust는 C++과 C와 달리 전역 락으로 atomic을 구현하는 것을 허용하지 않기 때문이다. 대신 타겟이 원자 연산을 네이티브로 지원하지 않으면, Rust는 atomic API를 그냥 사용할 수 없게 만든다. 이는 Rust가 좀 더 좋은 프로그래밍 모델을 위해 약간의 이식성을 기꺼이 포기하는 트레이드오프 중 하나다.
2026년 1월 11일 17:49 UTC(일) garyguo (구독자, #173367) [링크]
커널은 모든 아키텍처에 대해 64비트 원자 연산을 무조건 제공한다는 점에 유의하라. 64비트 정수에서 네이티브 atomic을 지원하지 않는 아키텍처에서는 락을 사용해 구현한다. 이는 u64에 대해 READ_ONCE()를 원자 연산으로 사용하는 것이 잘못이라는 뜻이다(atomic64_read()가 필요하다).
u64에서의 READ_ONCE는, 원자성은 신경 쓰지 않되(즉, read가 찢어져도(torn) 된다고 허용하되) 데이터 레이스 없이 값을 읽고 싶을 때는 여전히 유용할 수 있다. 하지만 이것은 Rust 쪽에서 사람들이 원자 연산에 그냥 READ_ONCE()를 쓰게 하고 싶지 않은 또 하나의 이유다. 어떤 의미론이 원하는 것인지 직관적이지 않기 때문이다.
2026년 1월 10일 20:44 UTC(토) NYKevin (구독자, #129325) [링크] (응답 2개)
특정 메모리 위치에 대한 relaxed atomic은, (다른 메모리 위치에 대한 연산과는 펜스를 쓰지 않으면 순서가 다를 수 있긴 하지만) 마치 잘 정의된 순서가 있는 것처럼 동작해야 하며, 이 요구는 그 위치에 대한 원자 연산 전체 집합에 적용되어야 한다. 특정 위치에 대해 load와 store만 사용하고 있을 수도 있지만, 컴파일러는 그것을 알 수 없다. 다른 코드가 그 위치에서 다른 atomic 연산을 수행할 수도 있다.
그건 사실이지만 오해를 부른다. 괄호 속 단서가, 적어도 x86에서는 구현 비용이 큰 실제 보장들을 모두 무력화하기 때문이다.
참고로 기록해 둔다: relaxed atomic을 제한(restrict)하기 위한 fence는 그 relaxed atomic과 같은 스레드에 있어야 하며, 그 외에도 여러 조건이 있다. 더 궁금한 독자는 https://en.cppreference.com/w/cpp/atomic/atomic_thread_fence.html 및 관련 문서를 보라.
내 이해로는, 이것은 전역 락(global lock)을 사용해 read-modify-write 연산을 구현한다면, 단순한 write 연산도 그 동일한 전역 락을 사용하여 구현해야 함을 사실상 의미한다.
전역 락을 잡으면 두 개의 서로 다른 메모리 위치(락과 페이로드)가 관여하므로, 위의 괄호 속 단서가 이미 그 락이 효과가 없다는 것을 말해 준다(락이나 페이로드 어느 쪽이든 펜스 없는 relaxed atomic으로부터 보호하는 데).
아니면 내가 "전역 락으로 read-modify-write 연산을 구현"한다는 말의 의미를 오해했을 수도 있다. 보통 나는 "read-modify-write 연산"을 (하드웨어) 명령 하나(또는 명령 시퀀스)로 이해하는데, 그것은 애초에 우리가 "구현"할 일이 아니다. 만약 "에뮬레이트"를 뜻한다면 문제는, 에뮬레이터는 C 추상 머신을 에뮬레이트하지 않는다는 것이다. x86 같은 실제 하드웨어나 JVM 같은 가상 하드웨어를 에뮬레이트한다. 그런 플랫폼은 C 추상 머신보다 더 구체적인 자체 메모리 모델을 갖고 있고, 컴파일러 백엔드는 올바른 어셈블리/머신 코드를 내기 위해 반드시 그 메모리 모델을 활용해야 한다. 그러므로 우리의 에뮬레이터는 relaxed atomic에 대해 단지 락을 잡는 수준에서 멈출 수 없다. 어떤 store/load가 relaxed atomic에서 유래했는지 알 수 없을 수도 있기 때문에, 결국 모든 load/store에 대해 락을 잡아야 할 수도 있다. 물론 가능하다면 이런 연산을 락-프리로 구현하는 편이 바람직하다.
2026년 1월 10일 21:12 UTC(토) willmo (구독자, #82093) [링크] (응답 1개)
내 생각엔 comex의 인접 댓글이 암시하듯, "구현"의 세 번째 의미다. 즉 타겟 하드웨어가 원하는 C/C++ atomic 연산을 네이티브로 지원하지 않는 경우(예: 적절한 CAS조차 없어 RMW를 구현할 수 없는 경우), 컴파일러는 전역 락을 사용하도록 컴파일해야 한다. 이는 현대 x86, ARM 등에서 흔한 데이터 타입에는 확실히 해당하지 않는다.
전역 락을 잡으면 두 개의 서로 다른 메모리 위치(락과 페이로드)가 관여하므로, 위의 괄호 속 단서가 이미 그 락이 효과가 없다는 것을 말해 준다(락이나 페이로드 어느 쪽이든 펜스 없는 relaxed atomic으로부터 보호하는 데).
이 경우 컴파일러는 펜스 없는 relaxed atomic이 전역 락을 잡도록 컴파일해야 한다. plugwash가 뜻한 바가 그거다.
2026년 1월 12일 16:36 UTC(월) NYKevin (구독자, #129325) [링크]
그렇다. 플랫폼이 원자 연산을 제공하지 않는다면 할 수 있는 일은 한정적이다. 결국 원자 연산이 있는 (가상) 플랫폼(C 추상 머신)을 원자 연산이 없는 (물리) 플랫폼 위에서 에뮬레이션하는 셈이다. 그리고 말했듯 에뮬레이션은 느리다.
2026년 1월 9일 17:42 UTC(금) bertschingert (구독자, #160729) [링크] (응답 11개)
READ/WRITE_ONCE()가 relaxed atomic read/write의 불필요하게 엄격한 대체물로 사용되는 경우와, 추가적인 엄격함이 실제로 필요한 상황에서 사용되는 경우가 각각 얼마나 되는지 감이 있는 사람이 있나?
나는 구현 당시 현장에 있지 않았으니 추측이지만, READ/WRITE_ONCE()는 대부분 상황에서 최적이거나 원하는 의미론을 제공해서가 아니라, C11 atomic 모델이 나오기 전에는 그게 쓸 수 있는 최선의 도구였기 때문에 volatile 캐스트로 구현된 게 아닌가 하는 느낌이 든다.
매우 어렵겠지만, (맞는 경우에는) C 쪽을 relaxed atomic으로 바꾸는 것이 올바른 일처럼 보이긴 한다. 다만 얼마나 많은 사용처가 READ/WRITE_ONCE()가 제공하는 추가 "volatile" 보장을 실제로 필요로 하는지 잘 모르겠다.
2026년 1월 10일 1:39 UTC(토) wahern (구독자, #37304) [링크] (응답 8개)
내가 이해하기로(IIUC) 문제는, 적어도 일부 아키텍처에서 컴파일러와 ISA가 제공하는 atomic 인터페이스가 어떤 상황에서는 불필요하게 비용이 크다는 점이다(적어도 rwonce.h를 작성하고 사용하는 사람들의 평가에서는). rwonce.h의 코멘트는 교차 CPU 원자성을 보장하지 않고, 오늘날 어떤 컴파일러나 메모리 모델도 명시적으로 지원하지 않는 단일 CPU에서의 비동기 연산(예: 인터럽트 간)과 관련한 현실 세계의 동작에 의존하고 있음을 시사한다.
또한 C11 atomic은 GCC나 리눅스에서 atomic intrinsic이나 의미 있는 메모리 모델의 출발점이 아니다. 최종 형태도 아니고 100% 포괄적인 모델도 아니다. C, C++, 컴파일러에서 더 공식적인 메모리 모델을 밀어붙이는 과정은, 그 이전에는 그런 것이 완전히 없었고 오늘날에는 만족스럽다는 잘못된 인상을 준다고 생각한다.
[1] GCC는 C11 atomic을 지원하기 전에 이미 최소 두 세트의 intrinsic을 가지고 있었고, 물론 커널 같은 프로젝트는 그들만의 세트를 가지고 있었으며, 그것들은 최신 builtin이 나오기 전에도 그랬듯 지금도 잘 동작한다.
2026년 1월 10일 8:54 UTC(토) koverstreet (✭ 후원자 ✭, #4296) [링크] (응답 2개)
잠깐만, ISA 수준에는 "원자" load/store라는 개념이 없다. 그냥 load와 store가 있을 뿐이다. x86의 lock prefix 같은 원자성은, 하나의 명령 안에서 여러 일을 수행할 때만 의미가 있다. 예컨대 load, increment, add — 원자적 increment.
READ_ONCE()와 WRITE_ONCE()가 제공하는 "원자성" 보장은 컴파일러 수준에서만 들어온다. 컴파일러는 언어 수준에서 원자성 개념이 없으면 load/store를 합치거나, 레지스터 스필의 대체로 여러 번 load를 내보낼 수 있다.
READ_ONCE()와 WRITE_ONCE()의 "불필요하게 비용이 큰" 부분은, 원자성과 순서를 구분하지 않는다는 점이다. 이들은 순서도 엄격하게 지정하지만 컴파일러에 대해서만 그렇고 하드웨어에 대해서는 아니다(메모리 배리어를 내보내지 않는다).
Rust의 atomic load/store는 원자성과 순서를 분리해 주고 순서를 명시적으로 만들기 때문에 정말 더 낫다. 또한 별도의 메모리 배리어 호출을 여기저기 뿌리는 대신(주석이 있을 수도 없을 수도 있는), 필요한 연산에 순서 요구를 붙이므로 가독성에도 좋다.
2026년 1월 10일 17:45 UTC(토) excors (구독자, #95769) [링크]
ISA 수준에는 "원자" load/store라는 개념이 없다. 그냥 load와 store가 있을 뿐이다.
이 말이 무슨 뜻인지 확신이 없다. 예를 들어 ARM ARM은 "single-copy atomicity"를 정의하는데, 이는 단일 프로세서 코드에서도 중요하다. STP(Store Pair) 명령 도중 인터럽트가 발생하면(이 명령의 동작은 메모리에 대한 단일 대입으로 정의되어 있지만), 인터럽트 핸들러는 메모리의 첫 절반은 업데이트되었고 두 번째 절반은 업데이트되지 않은 것을 관측할 수 있다. STP는 두 개의 분리된 원자적 쓰기로 취급되기 때문이다. (인터럽트에서 복귀한 뒤 STP 명령은 재시작되어 결국 완료된다.) 그래서 나는 더 복잡한 연산에 이르기 전에도 ISA가 원자 load/store 개념을 정의한다고 생각한다.
(GCC는 int64_t 대입에 STP를 기꺼이 사용하여 비원자적으로 만들지만, 'volatile'을 추가하면 single-copy atomic인 단일 64비트 STR을 사용한다.)
2026년 1월 10일 20:18 UTC(토) comex (구독자, #71521) [링크]
엄밀히 말하면, 정렬되지 않은(misaligned) load는 x86에서 원자적이지 않으며, SIMD load는 원자적일 수도 아닐 수도 있다.
2026년 1월 10일 16:55 UTC(토) joib (구독자, #8541) [링크] (응답 4개)
또한 C11 atomic은 GCC나 리눅스에서 atomic intrinsic이나 의미 있는 메모리 모델의 출발점이 아니다. 최종 형태도 아니고 100% 포괄적인 모델도 아니다. C, C++, 컴파일러에서 더 공식적인 메모리 모델을 밀어붙이는 과정은, 그 이전에는 그런 것이 완전히 없었고 오늘날에는 만족스럽다는 잘못된 인상을 준다고 생각한다.
만약 C++/C11 메모리 모델과 atomic이 오늘날 개발된다면, 그때와 지금 사이에 세계가 얻은 지식의 양을 감안할 때 얼마나 달라질지 궁금하다.
확실히 consume 메모리 오더링처럼, 음, 덜 성공적인 부분도 있었지만, 그 외에도 일반적으로 더 잘, 더 다르게 만들 여지가 있을까?
2026년 1월 11일 20:33 UTC(일) pbonzini (구독자, #60935) [링크] (응답 1개)
내가 바꾸고 싶은 한 가지는 seq_cst store 및 메모리 연산을 없애는 것이다. 그 대신, 예를 들어 Linux의 atomic_add_return처럼, 양쪽에 seq_cst fence가 둘러싸고 있는 것처럼 동작하는 연산이 더 낫다고 본다. 차이는 seq_cst RMW 연산이 이후의 relaxed load 뒤로 재정렬될 수 있다는 점인데, LKMM과 atomic_add_return에서는 그렇지 않다. 따라서 이는 의미론을 느슨하게가 아니라 오히려 더 엄격하게 만든다.
지금 와서 이것이 받아들여질 가능성은 높지 않지만, "거의 아무도 순차 일관 변수 외에는 필요 없을 것이다"라는 주장이 틀렸다는 것이 드러났으니 가능성이 있을지도 모르겠다.
아직 완전히 공식화되지 않은 또 다른 것은 out-of-thin-air 값이다. 모두가 그런 일이 일어나지 않는다고 동의하지만, 엄밀히는 금지되어 있지 않다(적어도 마지막으로 확인했을 때는).
2026년 1월 12일 6:06 UTC(월) riking (구독자, #95706) [링크]
seqcst 연산만(하지만 seqcst fence는 아닌) 특별하게 전역 순서를 가지는 그 설계는 반드시 사라져야 한다. 그리고 "seqcst fence와 융합된 연산"이 더 낫다는 데 동의한다.
2026년 1월 15일 0:18 UTC(목) milesrout (구독자, #126894) [링크] (응답 1개)
이 설계에서 최악의 부분은 _Atomic/std::atomic이다. Atomic 연산은 원자 연산이다. 연산이 원자적인 것이다. 피연산자 자체에는 본질적으로 원자적이거나 비원자적인 것이 없다. 연산자 오버로딩도 그냥 나쁜 생각이다.
2026년 1월 15일 15:40 UTC(목) bertschingert (구독자, #160729) [링크]
GCC atomic intrinsic은 이 문제를 제대로 처리하는 것처럼 보인다. 다만 일반 int 타입에 대해 portable하게 atomic 연산을 하는 방법이 있는지는 모르겠다.
한편 내가 Rust(그리고 C/C++11?) atomic에서 좋아하는 점은, 타입 시스템이 실수로 데이터 레이스를 도입하는 것을 막아 준다는 것이다. unsafe 코드 없이는 atomic 타입에 대해 비원자적 load/store를 할 수 없기 때문이다. 글에서 C에서는 READ_ONCE()와 WRITE_ONCE()를 써야 했지만 쓰지 않은 경우가 있다고 언급하는 것을 보면, 이것이 실제 위험이라는 뜻처럼 보인다.
2026년 1월 10일 15:42 UTC(토) bjackman (구독자, #109548) [링크] (응답 1개)
{READ,WRITE}_ONCE의 가장 흔한 사용처 중 하나는, 병렬성(parallelism) 없이 동시성(concurrency)만 있는 경우라고 생각한다. 예를 들어 태스크와 IRQ 사이에서 CPU-로컬 데이터를 공유할 때 같은 경우다.
내가 이해하기로(IIUC) C11의 relaxed 오더링은 그 경우에는 너무 약하지만, 다른 C11 오더링은 불필요하게 엄격해서, 이미 일반 read/write로 충분히 안전한 상황에서도 특수(비용 큰) CPU 명령을 강제할 수 있다.
2026년 1월 11일 17:57 UTC(일) garyguo (구독자, #173367) [링크]
C11 오더링에서는 relaxed op가 너무 약하다고 생각하지만, relaxed op + atomic signal fence(대개 컴파일러 배리어일 뿐임)면 충분하다. 또는 volatile relaxed op도 충분할 것이다.
저작권 © 2026, Eklektix, Inc.
댓글 및 공개 게시물은 각 작성자의 저작권이 적용됩니다.
Linux는 Linus Torvalds의 등록 상표입니다.