Thread

Thread (스레드)

Thread는 Ruby의 동시성(concurrent) 프로그래밍 모델을 구현한 클래스예요. 여러 스레드(병렬 실행 단위)가 필요한 프로그램에 딱 맞는 도구랍니다.

출처: Ruby 3.3 API

본문

메인 스레드와는 별개의 새 스레드를 ::new로 만들 수 있어요.

thr = Thread.new { puts "What's the big deal" }

그리고 join으로 메인 스레드의 실행을 잠시 멈추고 새 스레드가 끝나길 기다릴 수 있어요.

thr.join  #=> "What's the big deal"

메인 스레드가 종료되기 전에 thr.join을 호출하지 않으면, thr를 포함한 다른 모든 스레드가 종료돼요.

배열로 여러 스레드를 한 번에 다룰 수도 있어요.

threads = []
threads << Thread.new { puts "What's the big deal" }
threads << Thread.new { 3.times { puts "Threads are fun!" } }

threads.each { |thr| thr.join }

스레드의 마지막 값을 얻으려면 value를 사용해요.

thr = Thread.new { sleep 1; "Useful value" }
thr.value  #=> "Useful value"

스레드 초기화

새 스레드를 만들기 위해 Ruby는 ::new, ::start, ::fork를 제공해요. 각 메서드에 반드시 블록을 주어야 하며, 주지 않으면 ThreadError가 발생해요.

Thread 클래스를 서브클래싱할 때, 서브클래스의 initialize 메서드는 ::start::fork에서 무시돼요. 그 외에는 initialize에서 반드시 super를 호출하세요.

스레드 종료

스레드를 종료하는 방법은 여러 가지가 있어요. 클래스 메서드 ::kill은 주어진 스레드를 종료해요.

thr = Thread.new { sleep }
Thread.kill(thr)  # sends exit() to thr

또는 인스턴스 메서드 exit(별칭 kill, terminate)을 사용할 수 있어요.

thr.exit

스레드 상태

status로 현재 스레드 상태를 나타내는 문자열을 얻을 수 있어요.

thr = Thread.new { sleep }
thr.status  # => "sleep"
thr.exit
thr.status  # => false

alive?로 스레드가 실행 중이거나 잠들어 있는지, stop?으로 죽었거나 잠들어 있는지를 알 수 있어요.

스레드 변수와 스코프

스레드는 블록으로 만들기 때문에 일반 Ruby 블록과 같은 스코프 규칙이 적용돼요. 블록 안에서 만든 지역 변수는 그 스레드에서만 접근 가능해요.

Fiber-local vs Thread-local

각 fiber는 Thread#[] 저장소를 위한 자체 버킷을 가져요. 새 fiber-local을 설정하면 그 Fiber 안에서만 접근할 수 있어요.

Thread.new {
  Thread.current[:foo] = "bar"
  Fiber.new {
    p Thread.current[:foo]  # => nil
  }.resume
}.join

이 예시는 fiber-local을 가져오기 위해 []를, 설정하기 위해 []=를 썼어요. keys로 스레드의 fiber-local을 나열하고, key?로 fiber-local 존재 여부를 확인할 수 있어요.

스레드-local(thread-local)은 스레드 전체 스코프에서 접근 가능해요.

Thread.new {
  Thread.current.thread_variable_set(:foo, 1)
  p Thread.current.thread_variable_get(:foo)  # => 1
  Fiber.new {
    Thread.current.thread_variable_set(:foo, 2)
    p Thread.current.thread_variable_get(:foo)  # => 2
  }.resume
  p Thread.current.thread_variable_get(:foo)  # => 2
}.join

thread-local인 :foo가 fiber 안으로 이어졌다가(fiber-local과 달리), 스레드 끝에 가서는 2로 바뀐 걸 볼 수 있어요.


클래스 메서드 (Public Class Methods)

  • abort_on_exception → true or false - 전역 "abort on exception" 조건의 상태를 돌려줘요. 기본값은 false. true로 설정하면 어떤 스레드가 예외로 중단됐을 때 그 예외가 메인 스레드에서 다시 던져져요. $DEBUG 플래그로도 지정 가능해요.
  • abort_on_exception= boolean → true or false - true로 설정하면 예외로 중단된 스레드의 예외가 메인 스레드에서 다시 던져져요. 새 상태를 돌려줘요.
  • current → thread - 현재 실행 중인 스레드를 돌려줘요. Thread.current #=> #<Thread:0x... run>
  • each_caller_location{ |loc| ... } → nil - 현재 실행 스택의 각 프레임을 backtrace location 객체로 yield해요.
  • exit → thread - 현재 실행 중인 스레드를 종료하고 다른 스레드의 실행을 예약해요. 이미 kill 표시된 스레드면 ::exit는 그 Thread를 돌려줘요. 메인 스레드나 마지막 스레드면 프로세스를 종료해요.
  • start([args]*) {|args| block } → thread / fork - 기본적으로 ::new와 같아요. 다만 Thread를 서브클래싱한 경우 서브클래스에서 start를 호출해도 그 서브클래스의 initialize 메서드를 호출하지 않아요.
  • handle_interrupt(hash) { ... } → result of the block - 비동기 인터럽트 타이밍을 변경해요. Thread#raise, Thread#kill, 시그널 trap, 메인 스레드 종료 같은 비동기 이벤트의 처리를 조정해요.
  • ignore_deadlock → true or false - 전역 "ignore deadlock" 조건의 상태를 돌려줘요. 기본값은 false라 데드락 조건을 무시하지 않아요.
  • ignore_deadlock= boolean → true or false - VM이 데드락 조건을 검사하지 않게 해요. 시그널 같은 다른 수단으로 데드락을 풀 수 있을 때만 유용해요.
  • kill(thread) → thread - 주어진 스레드를 종료시켜요. Thread.exit 참고.
  • list → array - 실행 가능(runnable)하거나 정지된 모든 스레드의 배열을 돌려줘요.
  • main → thread - 메인 스레드를 돌려줘요.
  • new { ... } → thread - 주어진 블록을 실행하는 새 스레드를 만들어요. ::new에 준 인자는 블록에 전달돼요.
    • new(*args, &proc) → thread - ::new&proc(Proc)을 주면 그 Proc을 실행하는 스레드를 만들어요.
    • new(*args) { |args| block } → thread
    • 인자 없이 ::new를 호출하면 ThreadError가 발생해요.
  • pass → nil - 스레드 스케줄러에게 다른 스레드로 실행을 넘기라는 힌트를 줘요. 실행 중인 스레드가 실제로 바뀔지는 OS/프로세서에 따라 달라요.
  • pending_interrupt?(error = nil) → true/false - 비동기 큐가 비어 있는지 여부를 돌려줘요. 지연된 이벤트가 있는지 확인하는 데 써요.
  • report_on_exception → true or false - 전역 "report on exception" 조건 상태. 기본값은 Ruby 2.5부터 true예요. 스레드를 예외로 죽이면 $stderr에 메시지를 보고해요.
  • report_on_exception= boolean → true or false - true로 설정하면 그 뒤에 만들어진 모든 스레드가 상속받아 예외로 죽을 때 $stderr에 보고해요.
  • stop → nil - 현재 스레드 실행을 멈추고 "sleep" 상태로 만들어 다른 스레드 실행을 예약해요.
a = Thread.new { print "a"; Thread.stop; print "c" }
sleep 0.1 while a.status != 'sleep'
print "b"
a.run
a.join  #=> "abc"

인스턴스 메서드 (Public Instance Methods)

  • thr[sym] → obj or nil - fiber-local 변수 값을 돌려줘요. (명시적으로 Fiber 안이 아니면 현재 스레드의 루트 fiber) 존재하지 않으면 nil.
  • thr[sym] = obj → obj - fiber-local 변수 값을 설정/생성해요. thread-local은 thread_variable_set/thread_variable_get 사용.
  • abort_on_exception → true or false / abort_on_exception= boolean - 이 스레드의 로컬 "abort on exception" 상태.
  • add_trace_func(proc) → proc - proc을 추적 핸들러로 추가해요. (Thread#set_trace_func, Kernel#set_trace_func 참고)
  • alive? → true or false - thr가 실행 중이거나 잠들어 있으면 true를 돌려줘요. stop?status 참고.
  • backtrace → array or nil - 대상 스레드의 현재 backtrace를 돌려줘요.
  • backtrace_locations(*args) → array or nil - 대상 스레드의 실행 스택을 backtrace location 객체 배열로 돌려줘요. Kernel#caller_locations처럼 동작하되 특정 스레드에 적용돼요.
  • exit → thr (terminate, kill 별칭) - thr를 종료하고 다른 스레드 실행을 예약해요.
  • fetch(sym) → obj - 키에 해당하는 fiber-local을 돌려줘요. 없으면 다른 옵션: 인자 없이 KeyError, default가 있으면 그것, 블록이 있으면 블록 결과.
  • group → thgrp or nil - 스레드를 담은 ThreadGroup을 돌려줘요. Thread.main.group #=> #<ThreadGroup:...>
  • join → thr / join(limit) → thr - 호출 스레드는 실행을 멈추고 이 thr을 실행해요. thr이 끝나거나 limit 초가 지날 때까지 반환하지 않아요. 제한 시간이 지나면 nil을 돌려줘요. join하지 않은 스레드는 메인 스레드 종료 시 모두 종료돼요.
  • key?(sym) → true or false - 주어진 문자열(또는 심볼)이 fiber-local 변수로 존재하면 true.
  • keys → array - fiber-local 변수 이름(심볼)들의 배열.
  • kill → thr - thr를 종료하고 다른 스레드 실행을 예약해요.
  • name → string - 스레드 이름을 보여줘요. name=(name) - 이름을 설정해요 (일부 플랫폼에선 pthread/커널에도 설정).
  • native_thread_id → integer - Ruby 스레드가 쓰는 네이티브 스레드 ID. OS에 따라 달라요. Linux에선 gettid(2)의 TID, macOS에선 pthread_threadid_np의 시스템 고유 ID.
  • pending_interrupt?(error = nil) → true/false - 대상 스레드의 비동기 큐가 비어 있는지.
  • priority → integer / priority= integer → thr - 스레드 우선순위. 우선순위가 높은 스레드가 더 자주 실행돼요(낮은 스레드도 실행될 수 있음). Ruby 스케줄러에 대한 힌트일 뿐 플랫폼에 따라 무시될 수 있어요.
  • raise - 주어진 스레드에서 예외를 던져요. 호출자는 thr일 필요 없어요. raise(string), raise(exception [, string [, array]]) 형태.
Thread.abort_on_exception = true
a = Thread.new { sleep 200 }
a.raise("Gotcha")
  • report_on_exception → true or false / report_on_exception= boolean - 이 스레드의 로컬 "report on exception" 상태.
  • run → thr - thr을 깨워 스케줄링 대상으로 만들어요.
a = Thread.new { puts "a"; Thread.stop; puts "c" }
sleep 0.1 while a.status != 'sleep'
puts "Got here"
a.run
a.join
# a
# Got here
# c
  • set_trace_func(proc) → proc / set_trace_func(nil) → nil - thr의 추적 핸들러를 설정하거나(nil이면) 추적을 비활성화해요.
  • status → string, false or nil - 스레드 상태를 돌려줘요.
    • "sleep" - 잠들거나 I/O를 기다리는 중
    • "run" - 실행 중
    • "aborting" - 중단되는 중
    • false - 정상 종료됨
    • nil - 예외로 종료됨
  • stop? → true or false - thr이 죽었거나 잠들어 있으면 true.
  • thread_variable?(key) → true or false - thread-local 변수가 존재하면 true.
  • thread_variable_get(key) → obj or nil - 설정된 thread-local 변수 값을 돌려줘요.
  • thread_variable_set(key, value) - keyvalue로 설정해요.
  • thread_variables → array - thread-local 변수 이름(심볼) 배열.
  • to_s → string (inspect 별칭) - 이름, id, 상태를 문자열로 덤프해요.
  • value → obj - join을 써서 thr이 끝나길 기다리고, 그 값 또는 스레드를 종료시킨 예외를 돌려줘요.
a = Thread.new { 2 + 2 }
a.value  # => 4
b = Thread.new { raise 'something went wrong' }
b.value  # => RuntimeError: something went wrong
  • wakeup → thr - 주어진 스레드를 스케줄링 대상으로 표시해요. 단 I/O에는 여전히 블록될 수 있어요. 스케줄러를 호출하지는 않아요(run 참고).
c = Thread.new { Thread.stop; puts "hey!" }
sleep 0.1 while c.status != 'sleep'
c.wakeup
c.join  #=> "hey!"

더 알아보기

Thread::Backtrace::Location(backtrace 위치 정보), Thread::Mutex, Thread::Queue, Thread::SizedQueue, Thread::ConditionVariable 등 관련 클래스와, Kernel#set_trace_func을 함께 보면 도움이 돼요.