반응형
volatile는 멀티스레드에서 “가벼운 동기화”를 위한 읽기/쓰기 가시성 보장 장치라고 이해하면 편합니다.
private volatile bool _isGrabbing = false;처럼 플래그에 쓰면, 다른 스레드가 값 변경을 즉시 보고 잘 멈추거나 시작할 수 있게 해줘요.
volatile가 뭔가요?
- 효과: 해당 필드에 대한 **읽기(Read)**와 **쓰기(Write)**가
- **CPU/컴파일러 재정렬(reordering)**로 서로 엇갈리지 않게 만들고,
- 코어별 캐시/레지스터에만 머물다 안 보이는(stale) 값이 되는 일을 막아,
- 다른 스레드에서 최신 값을 즉시 관측할 수 있게 합니다.
(쉽게 말해: “바로 보이게 하고, 읽기/쓰기 순서를 뒤바꾸지 말라”는 메모리 장벽을 넣어줍니다. 읽기는 acquire, 쓰기는 release 장벽에 해당.)
- 어디에 붙이나: 필드에 붙입니다. 로컬 변수/프로퍼티에는 직접 못 붙여요.
(프로퍼티나 배열 원소에는 System.Threading.Volatile.Read/Write 메서드를 사용) - 어떤 타입에 가능?
C#에서는 주로 참조형, bool/char/int/uint/short/ushort/byte/sbyte/float 등 기본형 일부가 허용됩니다. (그 외 타입은 Volatile.Read/Write나 다른 동기화 기법을 권장)
왜/언제 쓰나요?
1) “상태 플래그” 알리기 (취소, 종료, 시작)
백그라운드 루프를 멈추는 신호용 bool에 가장 흔히 씁니다.
volatile이 아니면, 어떤 CPU/최적화 조합에서는 작업 스레드가 변경을 못 보고 무한 루프에 빠질 수 있어요.
class Worker
{
private volatile bool _running = false; // 변경 즉시 보이게
public void Start()
{
_running = true;
new Thread(Run).Start();
}
public void Stop()
{
_running = false; // 다른 스레드가 바로 관측
}
private void Run()
{
while (_running)
{
// 작업...
Thread.Sleep(10);
}
}
}
2) 더블 체크 잠금(DCL: Double-Checked Locking)에서의 가시성
게으른 초기화 시 volatile 없으면 초기화가 부분적으로 보이는 문제가 생길 수 있어요.
(요즘은 Lazy<T>가 더 안전하고 간편합니다.)
class Cache
{
private static volatile Foo _instance; // 생성 재정렬 가시성 보장
private static readonly object _lock = new object();
public static Foo Instance
{
get
{
var tmp = _instance;
if (tmp == null)
{
lock (_lock)
{
if (_instance == null)
_instance = new Foo(); // 완전 초기화 보장
tmp = _instance;
}
}
return tmp;
}
}
}
3) 락 없이 “읽기 전용 참조”를 교체할 때
읽기는 많고 쓰기는 드문 경우, 전체 참조를 통째로 교체하는 패턴에서 가시성 확보용으로 사용합니다.
(복합 업데이트가 필요하면 락이나 Interlocked로 전환해야 함)
class Router
{
private volatile Handler _handler = new HandlerA();
public void SwapToB() => _handler = new HandlerB(); // 쓰기 즉시 보임
public void Handle(Request r) => _handler.Process(r); // 항상 최신 핸들러를 봄
}
volatile가 해주지 않는 것들 (중요!)
- 원자적 복합 연산 보장 X: x++, x += n, list.Add() 같은 연산은
“읽기→계산→쓰기”의 여러 단계로 이루어져 있어, volatile만으로는 경쟁 조건을 막지 못합니다.
→ 이런 경우 lock 또는 Interlocked 계열을 써야 해요. - 상호 배제/동시 접근 보호 X: 임계영역 보호를 원하면 lock이 필요합니다.
- 트랜잭션/일관성 보장 X: 여러 필드를 한꺼번에 일관되게 갱신하려면 lock으로 묶으세요.
잘못된 예
volatile int _count;
void Bad()
{
_count++; // (읽기 -> +1 -> 쓰기) 사이에 다른 스레드 끼어들 수 있음
}
올바른 대안
int _count;
void GoodWithInterlocked()
{
Interlocked.Increment(ref _count);
}
readonly object _lock = new object();
void GoodWithLock()
{
lock (_lock)
{
_count++;
// 여러 필드 갱신 등 임계영역
}
}
volatile가 필요한지 판단 기준
- 필드가 여러 스레드에서 읽히고/쓰이고, 그 용도가 단순 신호/상태 플래그인가?
→ 예: 취소/종료/시작 플래그, 참조 교체 플래그 → volatile 적합 - 연산이 복합적이거나 여러 필드를 함께 갱신해야 하나?
→ lock 또는 Interlocked 사용 (필요 시 volatile도 병행) - 항상 같은 락으로 접근한다면?
→ 이미 lock이 가시성과 재정렬을 모두 보장하므로 volatile은 불필요합니다.
프로퍼티/배열 원소엔 어떻게?
직접 volatile을 붙일 수 없으니 System.Threading.Volatile 유틸을 쓰세요.
using System.Threading;
class Flags
{
private bool _stop;
public bool Stop // 프로퍼티 내부에서 가시성 보장
{
get => Volatile.Read(ref _stop);
set => Volatile.Write(ref _stop, value);
}
}
class Arr
{
private int[] _a = new int[10];
public int ReadAt(int i) => Volatile.Read(ref _a[i]);
public void WriteAt(int i, int v) => Volatile.Write(ref _a[i], v);
}
정리
- volatile = 다른 스레드에 값 변경이 즉시 보이도록 하고 읽기/쓰기 재정렬을 막는 장치(가벼운 메모리 장벽).
- 주 용도: 취소/상태 플래그, 참조 교체 같은 단순 신호.
- 아닌 것: 락 대체품 아님, 복합 연산의 원자성 보장 못 함 → 그땐 lock/Interlocked.
- 프로퍼티/배열엔 Volatile.Read/Write를 사용.
_isGrabbing 같은 “루프 on/off 플래그”에는 volatile이 딱 맞아요. 루프 스레드가 바로 멈추거나 시작할 수 있게, 변경이 즉시 가시화되도록 해주니까요.
반응형
'C#' 카테고리의 다른 글
“IDisposable: 가비지 컬렉터가 다 못하는 ‘자원 반납’을 결정적으로 보장하는 계약” (0) | 2025.10.10 |
---|---|
C# “??는 ‘null이면 우변’ — 안전한 기본값 지정의 한 줄 공식” (0) | 2025.10.10 |
“C# 값 형식에도 없음(null)을 — T? = Nullable<T> 제대로 쓰는 법” (0) | 2025.10.10 |
error. 이벤트핸들러 이름 충돌 (0) | 2025.09.22 |
왜 IDisposable을 구현하나요? (0) | 2025.09.19 |
“직접 델리게이트 콜백” → “.NET 표준 이벤트 패턴(Event + EventArgs)” (0) | 2025.09.19 |
sealed를 왜 써야 할까? (0) | 2025.09.19 |
C#의 readonly 키워드 (0) | 2025.09.19 |