티스토리 뷰

시리즈/Concurrency

yield에 관한 고찰

빅또리 2022. 1. 23. 22:21

제너레이터, 시퀀스, fiber.zip
3.10MB

먼저 시작하기 전에 다음의 글(Implementing threads)을 배경 지식으로 같이 깔고 가면 좋겠다.

 

yield하면 생산이라는 뜻이 먼저 떠올라서 의미가 잘 안 와닿았는데, 멀티스레딩 관점에서는 양보라는 다른 뜻으로 해석하는게 더 잘 맞는 것 같다. running 스레드가 다른 스레드로 실행을 양보하는 것! 

 

In computer science, yield is an action that occurs during multithreading, of forcing a processor to relinquish control of the current running thread, and sending it to the end of the running queue, of the same scheduling priority - wikipedia

 

 

 

 

자바스크립트에서의 yield

 

제너레이터

🧸일반 함수 => run-to-completion behavior 일 것이라고 기대. 호출하면 끝까지 쭉 실행하고 1개 이하의 값만 반환. 그리고 보통 실행 중간에 preempted 될 수 없음.

🧸제너레이터 => run-stop-run behavior. 중간에 pause 됐다가 나중에 resume되고 하는 흐름을 n번(혹은 프로그램 실행 중인 한 무한히도 가능) 반복할 수 있음. pause, resume될 때마다 외부 caller와 값도 주고 받을 수 있다. => 이 특징으로 손쉽게 데이터 스트림을 만드는데 주로 사용된다!

 

어떻게 이게 가능하냐? 

제너레이터 객체를 cooperative 스케줄링 방식으로 동작하는 유저 레벨 스레드라고 생각하면 설명이 된다.

next를 호출해서 "제너레이터 객체 스레드"가 싱글 js 커널 스레드랑 매핑되서 실행되도록 하고, "제너레이터 객체 스레드가" 다시 yield 호출로 자발적으로 다른 유저 스레드 (메인 컨텍스트)에게 커널 스레드를 반환한다.

syntax

제너레이터 함수 function* => 제너레이터 객체를 반환한다.

function* generateSequence() {
  // do something
  yield 1;
  // do something
  yield 2;
  // do something
  yield 3;
}

제너레이터 객체의 next 메소드 호출 => 저번에 pause 됐던 라인 부터 가장 가까운 yield문 까지 실행. next에 같이 넘겨준 인자가 있다면 지난번 pause된 yield expression의 evaluated value가 그 값으로 치환된다.

yield문을 만남 => 제너레이터 실행 멈추고 yield 뒤의 value가 리턴됨. {value:2, done:false} 이렇게 생긴 객체 리턴.

 

제너레이터는 이터러블이다 -> 고로 for..of 반복문을 사용해 값을 얻을 수 있음.

let generator = generateSequence();

for(let value of generator) {
  console.log(value); // 1, 2, 3이 출력됨
}

 

 

 

 

kotlin에서의 yield

 

코틀린에서도 자바스크립트와 거의 비슷한 맥락으로 yield 키워드가 쓰인다.

자바스크립트 제너레이터 특징 2가지 :

(1) 제너레이터 코드 블럭 부분이 실행 flow적인 측면에서 pause, resume을 반복할 수 있다. (user level 멀티 스레딩)

(2) 제너레이터는 이터러블이다. 데이터 스트림 생성에 주로 사용된다.

중 특히 (2)번째인 제너레이터가 "이터러블"이라는 관점에 초점을 맞춰 사용되는 듯 하다. => 시퀀스 객체에서!

 

collection (Iterable) vs sequence

kotlin collection 상속 관계
출처 => https://medium.com/androiddevelopers/collections-and-sequences-in-kotlin-55db18283aca

collection : eager evaluation vs sequence : lazy evaluation 

sequence는 lazy evaluation이기 때문에 필요할 때마다, 필요한 만큼 호출한뒤 suspend하면서 점화식으로 정의한 수열  마냥 무한한 개수의 원소를 가질 수 있다!

val sequence = generateSequence(0) { it + 1 }
// (0) => 초기 값, {it+1} => 다음 value 계산하는 함수, [0,1,2,3,4,5 ....]

 

yield 함수

fun fibonacci() = sequence {
    var terms = Pair(0, 1)

    // this sequence is infinite
    while (true) {
        yield(terms.first)
        terms = Pair(terms.second, terms.first + terms.second)
    }
}

println(fibonacci().take(10).toList()) // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

실질적으로 js 제너레이터 - kotlin 시퀀스에 대응, yield는 이 (제너레이터 혹인 시퀀스)객체를 호출한 caller에 value 돌려주면서 코드 블럭 suspend 한다고 보면 둘이 거의 똑같다.

 

 

 

+) kotlin 코루틴은 (1)의 특징을 가진 요소. async, concurrency를 위해 사용한다.

resume & suspend

 

Fluent python 목차인데

"코루틴은 제너레이터에서 어떻게 진화했는가?" 이거 보면 진짜 제너레이터랑 코루틴이랑 연관관계과 확실히 있나보다!

저 책도 꼭 읽어 봐야지!🙃

 

 

 

ruby에서의 yield

[1]

ruby fiber

역시 자바스크립트 제너레이터 같은 cooperative 스레드 스케줄링을 하는 (explicit하게 자발적으로 yield 하기전에는 인터럽트할 수 없음) 유저 레벨 스레드로 보면 된다.

이번엔 코틀린에서와 다르게 위에 적어 둔 (1), (2) 두가지 특징중 (1) 제너레이터 코드 블럭 부분이 실행 flow적인 측면에서 pause, resume을 반복할 수 있다. (user level 멀티 스레딩)에 더 포커스를 맞춰 해석하면 된다.

Fibers are means of writing code blocks which can be paused or resumed, much like threads, but the difference is it's scheduling is done by the programmer and not by the operating system.

스레드는 호출한 컨텍스트의 background에서 작업하지만(아예 새로운 커널 스레드를 매핑해줌), fiber를 run하면 stop할때 까지 fiber가 main program이 된다. (호출 컨텍스트가 큐에서 대기하고 fiber가 커널 스레드에 매핑되어 실행됨)

 

루비 스레드 => 언어 라이브러리 단에서 스케줄링 함. 동일한 타임 슬라이스 만큼 할당. preemptive scheduling

fiber => 개발자가 스케줄링함. yield 호출 전까지 인터럽트 당하지 않음. cooperative scheduling

syntax

factorial =
Fiber.new do
  count = 1
  loop do
    Fiber.yield (1..count).inject(:*)
    count += 1
  end
end

Array.new(4) { factorial.resume }
=> # [1, 2, 6, 24]

문법은 다른 언어들이랑 크게 다를게 없는 듯 하다.

 

[2]

block, proc, lambda

2022.01.23 - [언어와 프레임워크/ROR] - 루비 block, lambda, proc

 

이 경우는 위와 크게 비슷하진 않은 것 같은데, 굳이 끼워 넣자면 코드 블럭으로 실행 flow를 이관한다는 점에서 yield라는 용어를 썼지 않을까 추측해본다..

 

 

정리하자면

2가지 포인트가 있는데

(1) 코드 블럭 부분이 실행 flow적인 측면에서 pause, resume을 반복할 수 있다. (user level 멀티 스레딩)

=> 영단어 yield의 의미를 '양보'로 해석하는 쪽이 잘 설명됨

(2) 데이터 스트림 생성에 주로 사용된다.

=> 그 와중에 yield (value) 이런 식으로 호출하면 caller 컨텍스트에 값을 리턴할 수 있으므로 '생산'으로 해석하는 것도 어울린다.

 

js 제너레이터, kotlin 시퀀스, 루비 fiber 셋다 거의 그놈이 그놈인데

제너레이터는 1,2 둘다. 시퀀스는 2에, fiber는 1에 초점을 맞춰 설계되었다는 것을 이해하면 된다.

 

그리고 루비에서 thread vs fiber

thread는 새로운 virtual processor(커널 스레드)에 맵핑해 줘가지고 해당 코드가 현재 컨텍스트의 background에서 시간적으로 동시에 실행될 수 있도록 하는데 (근데 루비나 파이썬에선 GIL때문에 프로세스당 락을 획득한 한 스레드만 실행될 수 있음. 루비 3.0에선 이거 해결해 본다고 Ractor라는 것 도입.)

Fiber는 그건 아니고 그냥 개발자가 stop, run하는 실행 흐름을 제어할 수 있는 편의성 때문에 다른 concurrency 메소드와 함께 사용 되는 것(커널 스레드 매핑을 현재 컨텍스트 <-> Fiber 로 옮기는 것뿐..)

Fiber(다른 언어에선 coroutine)로 얻을 수 있는 것은 parallelism이 아닌 concurrency임에도 그것이 효용이 있는 것은 IO operation에서이다!  

[ fiber1 에서 오래 걸리는 API 요청 보냄; stop ] -- [ 다른 instructions. 스레드든지, 프로세스든지, 등등..] -- [ resume; fiber1 다음 코드 실행 ] 

DB 쿼리, API 응답 기다리는 동안 cpu 놀리지 않고 다른 instruction 수행할 수 있게 하니까! 노드도 어떻게 보면 이걸 잘 활용해서 single 스레드 논블로킹으로 준수하게 여러 requests들을 처리해주는 것이니까.

 

 

Ruby 3.0 non-blocking fiber

Thread 마다 fiber 스케줄러 설정할 수 있는데 Fiber::SchedulerInterface이 인터페이스 대로 개발자가 직접 클래스 구현해서 세팅해줘야 함. ( fiber에서 blocking 상황 발생하면 일단 yield 해서 다른 fiber 실행시키고, ready 상태일 때 스케줄러가 다시 resume 해주기)

 

참고 문서

https://ko.javascript.info/generators

https://davidwalsh.name/es6-generators

좋음 👍=> 자바스크립트 generator를 멀티스레딩 관점에서 설명하는 글은 잘 없는데 여긴 그렇게 설명해서 좋았다.

https://dev.to/lydiahallie/javascript-visualized-generators-and-iterators-e36

https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.sequences/-sequence-scope/

https://blog.kiprosh.com/ruby-fibers/

https://blog.monotone.dev/ruby/2020/01/20/event-driven-with-ruby-fiber.html

👍=> fiber적용한 event driven non blocking IO

https://blog.monotone.dev/ruby/2020/12/25/ruby-3-fiber.html

=> non-blocking fiber 스케줄러 구현 예시

댓글
공지사항
최근에 올라온 글