스레드 지역 변수
스레드 지역 변수 (Thread Local Variables)
멀티스레드 프로그램을 짜다 보면, 각 스레드가 저마다 따로 갖고 싶은 변수가 있어요. 예컨대 스레드별 카운터라든지, 스레드가 얼마나 일했는지를 세는 값 같은 게 그런 경우죠. 그런 값을 전역 변수 하나에 두면 여러 스레드가 같은 메모리를 두고 경쟁하게 되니까요. Zig에서는 이럴 때 스레드 지역 변수라는 걸 씁니다.
본문
어떤 변수를 threadlocal 키워드로 선언하면 그 변수는 스레드 지역 변수가 돼요. 즉 각 스레드가 자기만의 별도 인스턴스를 갖고 작업하게 됩니다. 같은 이름이지만, 스레드마다 서로 다른 메모리를 바라보는 거죠.
const std = @import("std");
const assert = std.debug.assert;
threadlocal var x: i32 = 1234;
test "thread local storage" {
const thread1 = try std.Thread.spawn(.{}, testTls, .{});
const thread2 = try std.Thread.spawn(.{}, testTls, .{});
testTls();
thread1.join();
thread2.join();
}
fn testTls() void {
assert(x == 1234);
x += 1;
assert(x == 1235);
}
$ zig test test_thread_local_variables.zig
1/1 test_thread_local_variables.test.thread local storage...OK
All 1 tests passed.
여기 흐름을 짚어볼게요. testTls 함수는 x가 1234인지 확인하고 1을 더해 1235로 만든 다음 다시 확인합니다. 이 함수를 메인 스레드에서 한 번, 별도로 띄운 스레드 두 개에서 각각 한 번씩 총 세 번 실행하죠. 만약 x가 그냥 전역 변수였다면 이 세 실행이 같은 x를 두고 뒤섞여서 마지막 값이 예측 불가능해졌을 거예요. 하지만 threadlocal로 선언했기 때문에 스레드마다 독립적인 x가 하나씩 있어서, 각 실행은 자기 스레드의 x만 건드립니다. 그래서 세 번이 모두 똑같이 통과하는 거예요.
싱글 스레드 빌드에서는 얘기가 조금 달라져요. Single Threaded Builds에서는 모든 스레드 지역 변수가 평범한 Namespace Level Variables처럼 취급됩니다. 스레드가 어차피 하나뿐이니 별도 인스턴스를 만들 이유가 없어서죠.
한 가지 주의할 점이 있어요. 스레드 지역 변수는 const로 선언할 수 없습니다. 값을 바꿀 수 있는 var로만 만들 수 있죠. 스레드마다 서로 다른 값을 가질 수 있어야 하니까, 선언 시점에 고정된 상수일 수는 없는 거예요.
더 알아보기
- Single Threaded Builds — 싱글 스레드 빌드에서 스레드 지역 변수가 어떻게 다뤄지는지
- Namespace Level Variables — 네임스페이스 수준 변수의 기준 동작