엄범


CPU bound(병렬처리) : multiprocessing

I/O bound : multithreading 또는 asyncio

GIL은 작은 단위 lock보다 IO에 장점이 있을 수 있음. 다만 race condition이나 deadlock 등의 문제가 발생할 소지는 여전히 있기 때문에, 웬만하면 asyncio 쪽이 괜찮아 보인다.


  • thread를 한 번에 ``c 0x100``개 정도 만들면 `` thread.error: can't start new thread``가 발생한다. 이는 thread 수가 너무 많아서 발생하는 문제. ``c 0x80``까지는 괜찮은 듯.
  • ``python threading.Timer``로 일정 시간마다 특정 작업을 수행할 수 있다.
  • 멀티 프로세싱 할 때, if __name__에서 부터 코드가 실행되도록 해야 recursive하게 프로세스가 실행되는 것을 방지할 수 있다.


threading ( multiprocess )

  1. 직접 `` threading.Thread`` 클래스의 객체를 생성하는 방법
  2. `` threading.Thread``를 상속받은 다음 이 클래스의 객체를 생성하는 방법

(한 가지 방법이 더 있지만 어차피 안쓴다)


#1

```python

import threading


def say(msg):

    for i in range(2):

        time.sleep(1)

        print(msg)


for msg in ['THREAD1', 'THREAD2', 'THREAD3']:

    t = threading.Thread(target=say, args=(msg, ))

    # recv() 같은 blocking 함수를 호출하는 thread는 flag를 False로 만든다고 해도

    # 애초에 recv()에서 넘어가질 않기 때문에 종료되지 않는다. 이런 경우 daemon으로 만들어준다.

    t.daemon = True

    t.start()


# thread에서 while flag로 도는 경우 flag를 False로 만들어 종료될 수 있도록 한다.(단, blocking함수를 사용하지 않는 경우.)

flag = False

```

args 마지막에 꼭 ,를 써줘야 한다.


#2

```python

class UserThread(threading.Thread):

    def __init__(self, msg):

        threading.Thread.__init__(self)

        self.msg=msg

    def run(self):

        while True:

            time.sleep(1)

            print(self.msg)



t = UserThread(msg)

t.start()

```

반드시 super class인 Thread의 생성자에 self를 넘겨줘야 한다

start()를 호출하면 내부의 run()이 자동으로 호출되어 실행된다.

스레드를 외부에서 종료하기 위해서는, 강제 종료하는건 좋지 않기 때문에 스레드가 `` stop_flag``를 가지고 있게 하고 이를 반복적으로 체크하도록 구성해야 한다. 메인 스레드에서는 플래그 set하고 join하는 방식으로...

근데 이렇게 스레드가 멤버를 가지도록 하려면 #2처럼 객체로 구성해야 한다.


lock

thread를 사용하는 경우 반드시 lock을 사용한다!
```python
from threading/multiprocessing import Lock

mutex = Lock()
### 1
with mutex:
    # routines

### 2
mutex.acquire()
# routines
mutex.release()
```

파이썬에서 mutex는 ``python Lock()`` 객체다. ``python acquire(), release()`` 두가지 함수만 지원한다. 

critical section에 진입하기 전 `` acquire()``, 빠져나오면서 `` release()`` 해주면 끝이다.


``python with``문을 사용할 때 각 객체의 컨텍스트 관리자는 ``python with`` 블록에 들어가기 전에 알아서 `` acquire()``를 호출하고, 빠져나오면서 `` release()``를 호출하게 된다.


* `` Lock``을비롯한 `` threading`` 모듈 객체에는 모두 컨텍스트 관리자가 있다. 


daemon

``python daemon=False``이면 메인 스레드가 이 스레드가 종료될 때 까지 대기한다.

더 이상 non-daemon threads가 존재하지 않고 daemon threads만 남았을 때 전체 프로그램을 종료하기 때문에

``python daemon=True``인 threads들은 더 이상 non-daemon threads가 없으면 바로 종료된다.


특별한 상황이 아니면 `` join()``이랑 같이 쓰면 안된다.

`` join()``을 호출한다는건 (메인) 스레드에서 워커 스레드의 종료를 기다리겠다는 것이다.

근데 daemon=True라는건 메인 스레드가 종료될 때 같이 종료되겠다는걸 의미한다.(주로 recv같은 blocking 함수.)

이게 좀 앞뒤가 안맞는다. 워커 스레드는 메인 스레드가 종료될 때 까지 일하다가 메인 스레드가 종료되면서 같이 꺼질건데, 메인 스레드가 워커 스레드의 종료를 기다리고 있다.

즉 서로가 종료되기를 기다리는 이런 상황에서, 워커 스레드에 blocking이 걸려 있는 상태라면 join이 끝나지를 않아 메인 스레드가 종료되지 않고, 이는 워커 스레드도 같이 종료되지 않는다는걸 의미한다.

그래서 스레드라고 무조건 종료 기다려야 하니까 join 불러주는게 아니라, 상황에 맞게 써야 한다.


직관적으로 daemon이면 메인 스레드가 종료되어도 백그라운드에서 실행되어야 하니까 ``python daemon=True``면 메인 스레드 종료되어도 계속 돌아가야 하는거 아닌가 싶지만

daemon thread와 daemon process는 역할이 다르다.

daemon process는 상기한 대로 백그라운드에서 계속 실행되며 특정 작업을 수행하는 역할을 하지만

daemon thread는 non-daemon thread(일반 스레드)의 작업을 돕는 역할을 한다.

그래서 non-daemon thread 더 이상 남아있지 않으면 데몬 스레드도 종료되어야 하는 것이 맞고, 실제로 특정 스레드가 종료되었는데 그를 보조하는 스레드가 계속 작업을 수행하는 것을 막기 위해 사용한다.


critical section

critical section은 스레드들이 동시 접근해서는 안되는, 공유 자원에 접근하는 코드 영역을 말한다. 즉 변수나 I/O 모듈을 의미하는게 아니라 코드의 일부분을 의미한다. (windows에서는 이러한 critical section의 상호 배제를 위해 제공되는 자료구조 그 자체를 의미하기도 한다.)

Race condition이 발생해 자원이 이상한 값을 가지지 않도록 critical section에서는 연속적인 실행이 보장되어야 한다.


파이썬의 멀티스레드 모델도 critical section이 존재할 수 있다.
아니, GIL때문에 한 순간에 하나의 스레드만 실행되니까 자원 접근도 한 순간에 하나의 스레드만 하게 되는 것 아닌가라고 착각할 수 있겠지만, 자원 접근 수정같은 오퍼레이션은 여러 instruction으로 구성되기 때문에 atomic operation이 아니면 그냥 실행하다가 컨텍스트 스위칭 되면서 동시성 문제가 발생할 수 있다. 단일 코어에서도 동시성 문제가 발생할 수 있는 맥락이랑 같다고 보면 된다.

semaphore, queue

세마포어를 사용해야 하는 경우 queue나 deque 모듈이 더 적합할지를 한 번 생각해 본다.

GIL ( Global Interpreter Lock )

파이썬 코드는 파이썬 가상머신( 인터프리터의 메인 루프 ) 위에서 돌아간다.

문제는, 인터프리터에서 한번에 하나의 스레드만 수행되도록 설계되었다는 점이다.

그래서 어떤 한 순간에, 코어가 여러 개 있어도 단 하나의 스레드만 돌아가게 된다.

이는 인터프리터에서 파이썬 가상머신에 엑세스할 때 전역 인터프리터 락(GIL)을 사용하기 때문이다.

인터프리터는 다음과 같은 행위를 반복하게 되므로, Lock이 걸려 스레드가 여러개의 코어에서 돌아갈 수가 없다.

  1. GIL을 설정하고,
  2. 어떤 스레드 실행
  3. 스레드 슬립
  4. GIL 해제
그래서 연산을 병렬처리해야 하는 경우에는 손해가 크다.(이 경우 스레드는 그냥 라운드 로빈 방식으로 처리되므로 컨텍스트 스위칭 오버헤드만 생긴다.)
GIL 방식은 I/O에 사용하는 경우 작은 단위로 Lock을 하는 방식보다 더 빠를 수 있다는 장점이 있다. 또한 C와 바인딩 하는 경우도 더 빠르다고 한다.

아무튼, 이러한 단점때문에 파이썬은 `` threading`` module과 동일한 인터페이스를 가진 `` multiprocess`` module을 제공한다.
IPC에서 약간 손해를 보겠지만, 이를 지원하는 각종 메커니즘도 존재하며 thread와 똑같이 사용하면 된다.

thread 객체는 한 번만 start()할 수 있다.

```python
import threading
import time

class TestThread(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)

    def run(self):
        print(1)


a = TestThread()
a.start()
time.sleep(1)
print(a.isAlive())
a.start()
```
```
1
False
Traceback (most recent call last):
  File "test.py", line 16, in <module>
    a.start()
  File "D:\Python\Anaconda3\lib\threading.py", line 842, in start
    raise RuntimeError("threads can only be started once")
RuntimeError: threads can only be started once
```
그래서 짧은 작업을 수행하고 종료되는 스레드 같은 경우 객체를 새로 만들어야 함.


조건에 따라 스레드 실행 wait 관리

notify(), wait()를 호출해서 스레드 간 실행 흐름을 제어할 수 있다.


ThreadPoolExecutor

어차피 thread를 쓸거라면, ``py concurrent.futures.ThreadPoolExecutor``를 사용해 스레드 풀을 구성하는 편이 좋다.
코드도 그냥 스레드를 사용하는거랑 크게 차이 없어서 간단하게 사용할 수 있다.

```python

with ThreadPoolExecutor(max_workers=20) as e:

    for i, hash_digest in enumerate(hash_list):

        t = QueryThread(file_list[i], hash_digest)

        e.submit(t.run)   

        # ()없음에 유의. 함수 포인터 넘기면 t.run() 호출해준다. 인자는 뒤 파라미터로 추가적으로 넘길 수 있음.

```


ProcessPoolExecutor와 multiprocessing.pool

* 주의. ``py if __name__``에 써야함. 전역 스코프에서 쓰면 에러난다. recursive하게 프로세스가 뜨면서 에러가 발생한다고 함.
```python
from concurrent.futures import ProcessPoolExecutor
 
def f(x):
    return x*x

result = []
def main():
    with ProcessPoolExecutor(max_workers=3) as e:
        for square in e.map(f, range(10)):
            result.append(square)
    return result

if __name__ == "__main__":
    print(main())
```
```python
from multiprocessing import Pool

def f(x):
    return x*x

if __name__ == '__main__':
    with Pool(5) as p:
        a = p.map(f, range(10))
        print(a)
```
이렇게 하면 뭘 쓰든 결과 똑같다.
그래도 ProcessPoolExecutor가 좀 더 wrapping되어 편리한 부분이 있고, ThreadPoolExecutor와 동일한 인터페이스로 제공되기 때문에 이걸 쓰는게 좋아보인다.