티스토리 뷰

👾아래 문서 정리 ⬇️

https://engineering.universe.com/introduction-to-concurrency-models-with-ruby-part-ii-c39c7e612bed

 

Introduction to Concurrency Models with Ruby. Part II

In the second part of our series we will take a look at more advanced concurrency models such as Actors, CSP, STM and of course Guilds

engineering.universe.com

파트2에서는 Actor, CSP, STM, Guild 같은 심화된 동시성 모델을 살펴볼 것이다.  야호 😎💃

 

 

 

 

 

🐶 Actor

Actor operation

- 다른 액터 생성하기

- 메세지 전송하기

- 다음 메세지를 어떻게 핸들링할지 지정하기

 

액터는 위와 같은 작업을 수행한다. 각자 private state를 저장하고 있고 서로 공유하지 않음. 따라서 actor 끼리는 공유 메모리 같은 것 없이 오직 메세지를 통해서만 통신함. 공유 상태 같은 것이 없기 때문에 락을 사용할 필요도 없다.

Do not communicate by sharing memory; instead, share memory by communicating.

다른 language 유명한 구현체

=> Erlang, Elixir, Akka 라이브러리 (JVM위에서 도는 application을 위한 concurrency 툴킷. java, Scala)

 

루비에서는 Celluloid가 가장 유명한 구현체이다. 

기저 작동 방식을 보면, 각 Actor가 서로 다른 스레드 위에서 돌아가고, 다른 Actor의 응답을 기다리는 동안 메소드가 blocking되는 것을 막기 위해 모든 메소드 호출에 Fiber를 사용한다.

 

( 추가 설명 하자면 )

클래스 안에 include Celluloid를 넣어 정의하면 이 클래스의 인스턴스가 액터가 된다.

new 할 때 새로운 native 스레드가 spawn 되어 이 액터의 메소드 코드는 이 새로운 커널 스레드에서 실행된다, 다른 일반 루비 객체처럼 자동으로 garbage collecting 해주지 않기 때문에 생명을 다한 액터라면 명시적으로 terminate 해줘야 한다.

*각 액터마다 separate 스레드에서 동작하게 설계되어있는데, celluloid를 ruby MRI에서 사용하면 GIL때문에 성능을 못 뽑아내고. (1코어 utilize 밖에 못함..) GIL 없는 RVM 위에서 도는 루비 (JRuby, Rubinius)를 사용할 것을 권고하고 있는 것 같다..

Celluloid experiment with MRI / JRuby

 

액터 모델에서는 서로 msg sending을 통해서만 소통하는데, celluloid에서는 함수 호출이 msg send하는 액션에 해당함.

( 루비 문법중에 send 구문으로 함수 호출하는 것도 있으니 그거랑 연관지을 수도 있으려나..?)

일반 호출은 resonse가 있을때까지 sender (=function caller)를 blocking하고, async 키워드를 붙여서 호출하거나 bang(!) 호출하면 sender는 blocking 되지 않고 바로 다음 줄 코드 진행한다.

async 호출 시 return value로 뭔갈 하고 싶을 땐 future를 사용할 것. (=> 자바스크립트 프로미스 같은거)

 

또 여러가지 기능들을 제공하는데 이를테면,

Celluloid::Actor[:world] = World.new이런식으로 Celluloid repository에서 이름으로 해당 액터에 접근할 수 있게 등록해 둘 수 있음.

그리고 supervisor 기능을 사용하면 celluloid가 알아서 celluloid repository에 등록한다음에 exception 발생등 액터에서 실패가 발생하면 retrial 하도록 관리해줌.

Pool은 코어 개수만큼 해당 클래스의 액터 객체 생성한다음에 함수 호출 있으면 이 풀에서 적당한 액터 배정해주는 방식.

link로 서로 다른 액터끼리 연결해서 crash notification을 줄 수도 있다.

 

자세한 설명은 다음 링크를 참조하시길. (나도 슬쩍만 봤다)

https://github.com/celluloid/celluloid/wiki

An Introduction to Celluloid, Part I

An Introduction to Celluloid, Part II

An Introduction to Celluloid, Part III

 

# actors.rb
require 'celluloid'

class Universe
  include Celluloid
  def say(msg)
    puts msg
    Celluloid::Actor[:world].say("#{msg} World!")
  end
end

class World
  include Celluloid
  def say(msg)
    puts msg
  end
end

Celluloid::Actor[:world] = World.new
Universe.new.say("Hello")

실행 결과

 

 

장점

- 개발자가 직접 멀티 스레드 프로그래밍을 할 필요가 없다. 액터간 공유하는 메모리가 없다는 것은 명시적인 락 없이도 deadlock-free 동기화가 가능하다는 것을 의미한다.

- Erlang과 비슷하게, Celluloid는 액터를 Fault-tolerant하게 구현했다. Supervisor를 사용해서 박살난 Actor를 재부팅 시키려 할 것이다.

- 액터는 분산 문제를 해결하기 위해 디자인 되었다. 여러대의 머신으로 스케일링 할 때 좋은 선택지이다.

 

단점

- 시스템에서 공유 상태를 써야하거나, 특정 순서대로 동작함을 보장해야 할 경우에는 액터를 사용하는 것이 맞지 않는다.

- 디버깅이 까다롭다. 수많은 액터 사이에서 코드 flow를 파악하기, 또 특정 액터가 message를 변경할 수도 있음.

- 스레드를 직접 다루는 것 보다 celluloid로 복잡한 동시성 시스템을 더 쉽고 빠르게 구축할 수 있음. 하지만 runtime cost는 훨씬 크다. (5배 더 느리고 8배 메모리를 더 소요)

- 불행히도, 루비 액터 구현체는 여러 대의 분산 서버에서 운영하기에 그렇게 효율적이지 못하다. 예를들어 0MQ를 사용하는 Dcell은 아직 production 환경에서 사용하기 불충분함.

 

예시

- Reel => event-based 웹서버. celluloid-based로 작성한 어플리케이션을 돌리는데 좋다. connection 1개당 액터 1개. 스트리밍이나 웹소켓에 사용될 수 있음

- Celluloid::IO => 액터 + evented I/O loop. Eventmachine과 다르게 여러개의 액터를 생성하는 것으로, 원하는 개수 만큼의 event-loop를 사용할 수 있다. (한 프로세스 당)

 

 

 

 

 

 

 

🐶 CSP (Communicating Sequential Processes)

CSP is fully synchronous. read, write 모두 blocking!

Actor모델과 굉장히 비슷한 패러다임이다. 마찬가지로 공유 메모리 없이 message passing을 사용하는 방식이다. 

하지만 Actor 모델과 CSP는 2가지 중요한 차이점이 있다.

- CSP의 프로세스는 익명이다. Actor는 identity가 있음. (Celluloid예시에서도 봤듯이 Celluloid repository에서 이름으로 참조해서 함수 호출할 수 있음). 따라서 CSP는 메세지 패싱을 위해 명시적인 channel을 사용한다! Actor에서는 채널 같은거 없이 직접 메세지 전달.

- CSP에서는 receiver가 받을 준비가 됐을때까지 sender가 메세지를 부치지 못한다. 액터는 비동기적으로 메세지 전송 가능. (Celluloid async 호출 처럼)

 

go 채널 structure

Golang: channels implementation

 

 

다른 language 유명한 구현체

=> Go 의 goroutine = 경량스레드 and channels, Clojure의 async.core 라이브러리, Crystal( Ruby에서 영감받은 언어인 듯?) 의 fibers and channels.

 

루비에서는 CSP를 구현한 몇가지 gem들이 있다. 그 중 하나가 concurrent-ruby(루비 concurrency 툴 라이브러리)에 구현되어 있는 Channel 클래스가 있다.

# csp.rb
require 'concurrent-edge'
array = [1, 2, 3, 4, 5]
channel = Concurrent::Channel.new
Concurrent::Channel.go do
  puts "Go 1 thread: #{Thread.current.object_id}"
  channel.put(array[0..2].sum) # Enumerable#sum from Ruby 2.4
end
Concurrent::Channel.go do
  puts "Go 2 thread: #{Thread.current.object_id}"
  channel.put(array[2..4].sum)
end
puts "Main thread: #{Thread.current.object_id}"
puts channel.take + channel.take

Concurrent::Channel.go에 주어진 block 코드는 다른 스레드에서 실행 된다. 메인 스레드에서 결과 동기화하고 토탈 value 계산.

모든 것은 명시적인 락 없이 channeld을 통해 이루어진다.

Channel.go가 스레드 풀에 있는 각기 다른 스레드에서 실행 된다. 이 풀은 더이상 남은 free 스레드가 없으면 자동으로 개수 늘림.

 

 

장점

- CSP 채널은 최대 1개 메세지만 들고 있을 수 있어서 코드 분석하기가 쉬워진다. (액터 모델에서는 무한개의 mailbox와 message를 가질 수 있는 가능성이 있음)

- CSP는 channel을 사용함으로써 producer, consumer를 decoupling할 수 있게 해줌. 서로에 대해 알필요가 없음.

- 메세지가 순서대로 전달된다.

 

단점

- CSP는 일반적으로 1대의 머신에서만 사용됨. 분산 프로그래밍에서는 Actor보다 안 좋음.

- 루비에서는 대부분의 구현체가 M:N 스레딩 모델을 사용하지 않음. ( 루비 1.9 부터 Thread는 native 스레드 활성화함)

따라서 Ruby 스레드를 사용하는 "goroutine"은 실질적으로 커널 스레드이다. ( user level thread인 Golang의 고루틴 처럼 경량이 아님)

- 루비에서 CSP를 사용하는 방식은 유명하지 않음. 그래서 안정화 되지도 않음.

 

예시

- Agent => 다른 루비 CSP 구현체. 이 gem 역시 각 go 블럭을 별개의 루비 스레드에서 실행한다.

 

 

 

 

 

 

🌻Actor vs CSP 간단정리

참고 문서 => Parallelism models. Actors vs CSP vs Multlithreading

  Actor CSP
공통점 message passing 방식 ( no shared memory )
차이점 message 전송 비동기 channel reader가 읽을 때 까지 channel writer block 된다. channel 은 msg 1개만 들고 있을 수 있음.
각 Actor 서로 식별 가능. address system.
mailbox (= 메세지 큐). 채널 같은거 없이 메세지 직접 전달. (directly)
프로세스는 channel (=일급 객체) 을 사용해서 통신한다.
Best effort, at-most-once delivery.
순서 장담 못함
메세지 순서대로 전달됨.
여러대 머신에 스케일아웃 해서 적용하기 좋음. 분산시스템에 최적. single machine에 가장 fit 하다

 

 

 

 

 

 

 

 

🐶 STM (Software Transactional Memory)

액터와 CSP가 message passing을 기반으로 하는 동시성 모델이었다면, STM은 공유 메모리를 사용하는 모델이다.

락 기반 동기화의 대안으로 사용할 수 있다. DB 트랜잭션과 비슷하게 아래와 같은 것들이 주요 개념이다.

- 트랜잭션 안에서 값이 바뀔 순 있지만, 트랜잭션이 커밋되기 전까지는 변화가 밖에선 안보임

- 트랜잭션 안에서의 에러는 모든 변화를 롤백하고 이전으로 돌아가게함.

- 변경사항 충돌때문에 트랜잭션 커밋이 안되면 성공할 때까지 재시도 한다.

 

Clojure => core language 레벨에서 STM 지원함

 

루비 구현체로는 Concurrent::Tvar 클래스가 있다. (ractor-tvar for Ruby3.0 도 있음) 

# stm.rb
require 'concurrent'
account1 = Concurrent::TVar.new(100)
account2 = Concurrent::TVar.new(100)
Concurrent::atomically do
  account1.value -= 10
  account2.value += 10
end
puts "Account1: #{account1.value}, Account2: #{account2.value}"

Tvar (transactional variable)는 single value를 들고있는 객체다. atomically와 함께 써서 트랜잭션 안에서의 데이터 변경을 구현하는데 사용됨.

 

장점

- 락 기반 프로그래밍 보다 STM을 사용하는 것이 훨씬 쉽다. 데드락을 피할 수 있게 히주고 race condition을 고려하지 않아도 되서 코드 파악하는게 훨신 쉬워짐.

- 코드 구조를 완전 다시 짜야하는 액터나, CSP랑 다르게 적용하기 훨씬 쉽다.

 

단점

- STM이 트랜잭션 롤백에 의존하기 때문에, 트랜잭션 내의 어느 포인트에서든지 했던 작업이 되돌려질 수 있다. 예를 들어 Post Http request같은 I/O작업을 수행했다고 보장하기 어렵다. 

- GIL 때문에 Ruby MRI와는 성능 개선이 어려움. 프로세스 당 1 cpu 이상을 utilize할 수 없기 때문에.

 

예시

- concurrrent-ruby의 Tvar

 

 

 

 

 

 

 

 

 

🐶 Guilds

** guild는 Ractor로 이름 변경 되어 Ruby3.0에 release 되었음!!!

Ruby's Actor-like concurrent abstraction

=> 한마디로 요악하자면 Actor + CSP(채널)모델에서 영감 받은 Ruby MRI GIL문제 해결할 feature

 

Koichi sasada ( 현재의 루비 VM, Fiber, GC 같은거 디자인한 Ruby 코어 개발자 )에 의해 Ruby3에 제안 된 새로운 동시성 모델이다.

다음과 같은 기준에 의해 개발됨

- 새로운 모델은 Ruby2 와도 호환 가능하고 더 나은 동시성을 제공해야 할 것

- Elixir같은 immutable 데이터 구조를 고집하는 것은 엄청 느릴 것이다. Ruby는 "write" operation을 많이 사용하기 때문에.

대신 Racket 언어의 Place처럼 공유 mutable 객체를 copy하는 방식이 더 나을 것 같다. copy 속도 빨라야 함.

- mutable 객체를 공유해야 한다면 Clojure의 STM처럼 특별한 자료구조가 필요하다.

 

 

위의 아이디어들이 다음과 같은 Guild의 메인 개념으로 이어졌다.

- guild는 여러개의 Fiber를 가질 수 있는, 여러개의 Thread를 가지는 동시성 요소이다.

- guild 오너만 해당 guild의 mutable 객체에 접근할 수 있다. 그러므로 락을 사용할 필요가 없음.

- guild끼리는 채널을 통해 copy나 move(=transfer_membership) 방식으로 mutable 데이터를 주고 받을 수 있음.

- freeze된 immutable 데이터는 그냥 공유 가능. (예- numbers, symbols, true, false, deeply frozen objects.)

 

 

( 추가로 정리해 보자면 이런거다 )

문서 참고 하기 => Concurrency in Ruby3 with Guilds

You can think of a Ruby 2.x program as having a single Guild.

Threads T1 & T2 belong to Guild G1 and can't run in parallel, but T3 belongs to G2, and it can run while Threads from G1 are executing.

 길드 1개당 GGL(Giant Guild Lock) 적용, 코어 1개 utilize 

 

guild = Ruby2.x의 프로세스 같은 개념.. 프로세스 당 인터프리터 당 GIL 때문에 스레드 여러개여도 최대 1 코어만 utilize 했던..

Ruby3 부터는 프로세스 1개에 길드 n개... GIL 대신 길드 마다 GGL

길드끼리는 no shared memory. 그래서 서로 다른 길드 끼리는 락 같은 거 신경 안써도 동시에 실행가능. => actor나 csp 같은 메세지 패싱으로 데이터 주고 받을 것. 채널 통해서 copy나 move 방식으로 mutable 데이터 공유함. immutable 객체는 동기화 이슈 없으니까 장치 없이 그냥 sharing 가능함.

 

bank = Guild.new do
  accounts = ...
  while acc1, acc2, amount, channel = Guild.default_channel.receive
    accounts[acc1].balance += amount
    accounts[acc2].balance -= amount
    channel.transfer(:finished)
  end
end
channel = Guild::Channel.new
bank.transfer([acc1, acc2, 10, channel])
puts channel.receive
# => :finished

계좌 잔액에 대한 정보는 bank 길드만 가지고 있고, 고로 bank만 해당 데이터 수정 작업을 수행할 수 있다. bank 길드 외부에서는 채널을 통한 요청만 가능함.

 

장점

- 길드 사이에 mutable 데이터 공유가 없다는 것은 락 메커니즘을 도입할 필요가 없음을 의미한다. 따라서 데드락도 발생 안한다.

길드 사이의 통신은 안전하게 디자인 되어있다 (by channel)

- 길드에서 immutable 데이터 sharing은 독려한다. 왜냐면 이것이 여러 길드 사이에서 데이터 공유할 수 있는 가장 쉽고 빠른 방법이기 때문에. 파일 앞에 frozen_string_literal: true 를 추가함으로써 가능한 많은 데이터를 freeze 해라.

- 길드는 Ruby2와 완전히 호환가능하다. 이 말은 당신의 Ruby2.0 코드가 싱글 길드 위에서 동작할 것임을 의미함.

- MRI에서 더 나은 동시성을 할 수 있게 만듦. 드디어 싱글 루비 프로세스에서도 multiple 코어 utilization이 가능해졌다.

 

단점

- 성능에 대한 예측을 하긴 너무 이르지만, 길드 사이에 통신, mutable 객체 공유가 스레드 보다 오버헤드가 훨 클 것이다.

- CSP (길드 간 채널을 통한 통신), STM ( mutable 데이터 sharing에서 더 나은 성능을 위한 특수 자료구조), 한 길드 내부에서 멀티 스레드 프로그래밍 같은 너무 많은 동시성 요소가 한꺼번에 사용되어 복잡하다.

- 자원 활용 측면에서 멀티 프로세싱 보다는 싱글 프로세스에서 여러개의 길드를 운영하는 것이 훨씬 이득이겠지만, 길드는 그다지 경량이 아니다. 루비 쓰레드보다 무겁다. 이 말은 길드 만으로는 수만개의 웹소켓 커넥션을 핸들링 할 수 없을 것임을 뜻한다.

 

 

 

 

 

 

 

🐶결론

1편에서와 마찬가지로 만능해결책이란 없다. 

CSP -> 싱글 머신에 제일 fit 한 방식. 

Actor -> 여러 대의 머신으로 쉽게 scale out 할 수 있다.

STM -> 트랜잭션 방식으로 동시성 코드를 쉽게 작성할 수 있도록 해준다.

하지만 이 모든 것들은 루비에서 first citizen이 아니다. 루비의 표준 concurrency primitive는 스레드와 Fiber임.

Ruby3에는 길드(Ractor)가 릴리즈 되었으니 루비 동시성 모델에 큰 발전이 있을 것이다.

 

 

 

 

 

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