스레드 지역 변수

스레드 지역 변수 (Thread Local Variables)

멀티스레드 프로그램을 짜다 보면, 각 스레드가 저마다 따로 갖고 싶은 변수가 있어요. 예컨대 스레드별 카운터라든지, 스레드가 얼마나 일했는지를 세는 값 같은 게 그런 경우죠. 그런 값을 전역 변수 하나에 두면 여러 스레드가 같은 메모리를 두고 경쟁하게 되니까요. Zig에서는 이럴 때 스레드 지역 변수라는 걸 씁니다.

출처: Zig Documentation

본문

어떤 변수를 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로만 만들 수 있죠. 스레드마다 서로 다른 값을 가질 수 있어야 하니까, 선언 시점에 고정된 상수일 수는 없는 거예요.

더 알아보기