lang/py

Book 파이썬답게 코딩하기 - 동시성

C/H 2020. 1. 16. 08:30

동시성

파이썬답게 코딩하기
CPython 기준으로 설명한다.
그 외 Jpthon, Pypy 등이 있다.

  • temporal multithreading 종류
    • coarse-grained lock: 큰 단위로 락을 건다.
    • find-grained: 작은 단위로 락을 건다.

CPU 자원은 코어가 여러 개더라고 한 번에 하나의 코어만 사용할 수 있다.
하지만 CPU 자원은 코어가 여러 개더라도 한 번에 하나의 코어만 사용하 ㄹ수 있다.
그래서 멀티 코어 CPU에서 coarse-grained 방식으로 락을 잡는 것보다 fine-grained 방식으로 락을 세분화해서 잡는 것이 더 좋다.
그래야 하나의 작업이 락을 잡고 있는 시간이 줄어드게 되고 여러 스레드에서 락을 잡을 수 있는 확률이 높아져서 작업을 빠르게 처리할 수 있다.
빈번한 락은 대기시간이나 문백 교환 비용이 들기 때문에 락을 덜 잡는 방향이 좋다.

GIL(Global Interpreter Lock)

GIL은 Cpython 이 제공하는 스레드 자원의 무결성과 동기화를 위한 로직(coarse-grained)을 제공한다.
하지만 GIL global lock으로 인해 스레드가 스레드의 기능을 온전히 하지 못한다.

파이썬 문서에서도 CPython에서는 GIL의 영향 때문에 멀티 코어 CPU 장비에서는 스레드보다 멀티 프로세싱이나 concurrent.futures.ProcessPollExecutor를 사용하라고 권고한다.

CPython에서 CIL은 단일 코어 CPU환경에서 성능이 빠르다.
C 라이브러리를 사용할 때 GIL을 사용하면 스레드에 대한 고려를 할 필요가 없다. 전역락이기 때문이다.
C언어로 작성된 파이썬이기 때문에 C 라이브러리에 대한 편의성도 좋다.
CIL은 CPU bound에는 취약하지만 I/O bound에는 영향이 없다.
구현이 쉽다.
스레드 사용은 효과를 얻을 수 없다.

Thread 구현

  1. 저수준 라이브러리 사용
    • Python2: thread
    • Python3: _thread
  2. 고수준 라이브러리 사용
# basic_threading_function.py
name : thread 0, argument : 0
name : thread 1, argument : 1
name : thread 2, argument : 2
name : thread 3, argument : 3
name : thread 4, argument : 4
# basic_threading_class.py
name : Thread-1, argument : 0
name : Thread-2, argument : 1
name : Thread-3, argument : 2
name : Thread-4, argument : 3
name : Thread-5, argument : 4

Thread의 로깅

파이썬 로그는 logging 모듈을 사용하거나 print문을 사용한다.
Python2에서 pirnt는 스레드별 별 다른 조치가 없이 실행된다. 락과 같은 메커니즘이 없고 원자적(atomic)으로 실행되지 않아서 출력이 동시에 되거나 flush 타이밍이 알맞지 않게 되는 의도치 않은 결과가 나올 수 있기 때문에 안전한 함수가 아니다.

안전하게 사용하려면 logging 모듈을 이용하는것이 좋다.

# logging_and_threading.py
name : thread 0, argument : 0
name : thread 1, argument : 1
name : thread 2, argument : 2
name : thread 3, argument : 3
name : thread 4, argument : 4

Deamon Thread

데몬 스레드로 실행하면 메인 프로그램 로직이 종료되면 스레드가 자동으로 종료된다.
반면 프로그램에 필수적인 작업들은 데몬이 아닌 스레드로 실행한다.

# daemon_thread.py
# 프로그램이 종료되면 스레드가 자동으로 종료되기 때문에  Exit 는 출력되지 않는다.
(daemon) Start
# daemon_thread_join.py
(daemon) Start
(daemon) Exit

Thread Event

threading 모듈은 스레드 간의 간단한 통신은 이벤트를 사용한다.
스레드에서 제공하는 이벤트는 이벤트 설정, 초기화, 기다리는 기능을 제공한다.

# thread_event.py
(MainThread) Wait ...
(first) Event status : (False)
(second) Event status : (False)
(first) Event status : (False)
(second) Event status : (False)
(first) Event status : (False)
(second) Event status : (False)
(first) Event status : (False)
(second) Event status : (False)
(MainThread) Set e1
(first) Event status : (True)
(first) e1 is set.
(second) Event status : (False)
(second) Event status : (False)
(second) Event status : (False)
(first) Set e2
(second) Event status : (True)
(second) e2 is set.
(MainThread) Exit

Thread Lock

내장된 리스트나 사전과 같은 자료 구조는 값을 설정할 때 원자적(atomic)으로 실행되므로 스레드에서도 무결성 보장할 수 있다.
하지만 int, float같은 자료형들은 원자적(atomic)으로 실행되지 않아 무결성이 보장되지 않는다.

가장 쉬원 무결성 보장은 락(lock)을 사용하는 것이다.
데이터 수정전 락을 잡아 다른 스레드에서 수정하지 못하게 하는 것이다.
수정 후 락을 풀어 다른 스레드에서 이용 가능하게 한다.

# thread_lock.py
(blocking) Start blocking lock
(nonblocking) Start nonblocking lock
(blocking) Grab it
(nonblocking) Attempt
(blocking) Release
(nonblocking) Attempt
(nonblocking) Grap it
(nonblocking) Release
(blocking) Grab it
(nonblocking) Attempt
(blocking) Release
(nonblocking) Attempt
(blocking) Grab it
(blocking) Release
(nonblocking) Attempt
(nonblocking) Grap it
(nonblocking) Release
(blocking) Grab it
(blocking) Release
(nonblocking) Attempt
(nonblocking) Grap it
(nonblocking) Release
(nonblocking) Attempt : 6, grab : 3

Thread Reentrant Lock (RLock 재진입 가능 잠금)

교착 상태는 락을 잡고 해제하지 않은 상태에서 락을 잡으려 할 때 발생한다.

# thread_rlock.py
(zero) Start set zero
(zero) Grab lock and set RESOURCE to 0.
(one) Start set one
(one) Grab lock and set RESOURCE to 1.
(zero) Grab lock and set RESOURCE to 0.
(one) Grab lock and set RESOURCE to 1.
(zero) Grab lock and set RESOURCE to 0.
(one) Grab lock and set RESOURCE to 1.
(zero) Grab lock and set RESOURCE to 0.
(one) Grab lock and set RESOURCE to 1.
(reverse) Start batch
(reverse) Grab lock!
(reverse) Start set one
(reverse) Grab lock and set RESOURCE to 1.
(reverse) Reversed

Thread Condition

모든 스레드가 락을 받은 것처럼 멈춰 있고, notify를 받를 받으면 동작한다.
condition의 경우 lock, release를 notify로 대체한다.

# thread_condtion.py
(receiver 0) Start receiver
(receiver 0) Waiting...
(receiver 1) Start receiver
(receiver 1) Waiting...
(receiver 2) Start receiver
(receiver 2) Waiting...
(receiver 3) Start receiver
(receiver 3) Waiting...
(receiver 4) Start receiver
(receiver 4) Waiting...
(receiver 0) End
(sender) Start sender
(sender) Send notify
(sender) End
(receiver 3) End
(receiver 4) End
(receiver 2) End
(receiver 1) End

5개 스레드를 시작하고, condition.wati()로 기다린다.
한개 스레드가 condition.notipy(1)를 받고 종료된 뒤 condition.notifyAll()로 전체 스레드에서 작동한다.
모든 스레드는 동시에 실행되지 않고, 한번에 하나씩 실행되며 메모리 공유해서 사용을 위해 자원 무결성을 보장한다.

Lock, Mutex, Sempphore

락은 스레드간 자원 접근시 무결성을 보장하기 위해 사용하고, 락은 하나의 프로세스안에서만 유효하다.
뮤텍스틑 락과 동일한 일을 하지만 전체 시스템에서 여러 프로세스가 한번에 하나의 스레드만 자원에 접근하도록 한다.
세마포어는 뮤텍스를 확장한 개념으로 전체 시스템에서 정해진 개수만큼 스레드를 점유할 수 있다.

# thread_semaphore.py
(thread-0) Waiting to enter the pool.
(thread-1) Waiting to enter the pool.
(thread-0) Enter the pool.
(thread-1) Enter the pool.
(thread-2) Waiting to enter the pool.
(thread-0) List of threads in resource pool : ['thread-0']
(thread-3) Waiting to enter the pool.
(thread-4) Waiting to enter the pool.
(thread-2) Enter the pool.
(thread-1) List of threads in resource pool : ['thread-0', 'thread-1']
(thread-2) List of threads in resource pool : ['thread-0', 'thread-1', 'thread-2']
(thread-0) List of threads in resource pool : ['thread-1', 'thread-2']
(thread-2) List of threads in resource pool : ['thread-1']
(thread-3) Enter the pool.
(thread-3) List of threads in resource pool : ['thread-1', 'thread-3']
(thread-4) Enter the pool.
(thread-4) List of threads in resource pool : ['thread-1', 'thread-3', 'thread-4']
(thread-1) List of threads in resource pool : ['thread-3', 'thread-4']
(thread-3) List of threads in resource pool : ['thread-4']
(thread-4) List of threads in resource pool : []

Thread Local Daat

스레드에서 공유하지 않을 로컬 자원

# thred_local.py
(MainThread) Value not set yet.
(MainThread) value : 0
(thread-0) Value not set yet.
(thread-0) value : 1
(thread-1) Value not set yet.
(thread-2) Value not set yet.
(thread-1) value : 2
(thread-3) Value not set yet.
(thread-2) value : 3
(thread-4) Value not set yet.
(thread-3) value : 4
(thread-4) value : 5

CPython의 경우는 GIL 영향으로 제 성능을 발휘 하지 못한다.
스레드는 동시다발적으로 발생하므로 어떤 로직이 먼저 실행되는지 보장을 하지 못한다.
비결정적, 비순차적이다. 그래서 문제나 버그 발생시 문제 원인 파악이 어렵다.

반응형