서버의 특정 부분에서 써보자는 얘기가 나와서 사실 관계랑 rein의 개인적인 견해를 정리.
Windows API에는 *nix 에서 user–level thread라고 부르기도하는 “fiber”에 대한 API가 존재한다. 사실 이 Fiber(섬유)란 단어 자체가 약간 말장난이기도한데 ((Thread는 실이고 실은 섬유라는 더 작은 단위가 합쳐져서 나온다)), 크게 보면 다음과 같은 구성이 된다. ((Fiber를 User–level thread로 보면 Unix–like OS에서도 유사하다))
- 각각이 별도의 메모리(=주소 공간)를 사용하는 프로세스;process
- 하나의 프로세스 안에서 메모리를 공유하는 스레드;thread들.
- 역시 하나의 프로세스 안에서 메모리를 공유하지만 스레드가 실행해줘야 돌아가는 fiber. 그리고 OS 커널;kernel의 관점에선 보이지 않는다 ((대조적으로 Win32나 linux에서는 개별 스레드가 커널 관점에서 잘 보이며(?), 스케쥴링의 기본단위가 된다))
Fiber는 ::ConverThreadToFiber, ::CreateFiber, ::DeleteFiber, ::SwitchToFiber 등의 API를 통해서 제어되는데, 그 시작, 종료 그리고 Fiber간의 context–switching을 사용자가 조절할 수 있다(스케쥴링)는 점에서 스레드랑 가장 차이가 난다. 물론 스레드 A에서 만든 fiber를 스레드 B에서 동작시켜도 되기 때문에 스케쥴링 할 때 스레드–fiber 소유관계를 확인해야하는 문제는 없다 ((다만 특정 스레드에서 만든 fiber들은 스레드가 종료하거나 같은 스레드에서 만들어진 fiber 중에 하나가 종료하면 모두 삭제 된다)).
Windows 시스템 프로그래밍 ((7장의 파이버 절에서 요약. J. Hart 저. 류광 번역. 정보문화사에서 발행한 3판이 나와있다.)) 에서 소개하는 fiber의 용도는,
- 특정 유형의 유저레벨 스레드로 구현한 Unix 응용 프로그램의 포팅 ((이 부분은 Windows via C/C++ 12 장에서 설명하는 fiber의 용도이기도 하다.))
- Polling을 담당하는 스레드 대신 fiber를 활용
- Co–routine의 구현. Lua 같은데서 사용되는 코루틴 비슷한 것을 만들 수 있다. Lua와 유사하게(하지만 제한된 형태로) 다른 코루틴 스택에 데이터를 찔러넣을 수도 있다.
- 스레드 간의 문맥 전환;context–switching보다 fiber 간의 문맥 전환이 훨씬 싼 작업이다. 문맥 전환이 자주 일어나야 하면 더 쓰기 좋을 수 있다.
정도다. 일단 첫번째 유형이 내재된 목적인 것 같은데 ((MSDN에 따르면 fiber는 잘 정의된 멀티스레딩 응용에 비해 장점이 없으며, 자기 자신의 스레드를 스케쥴링하게 구현된 응용을 포팅하는데 적당하다고 되어 있다)) 코루틴이나, 절대로 blocking되면 안되는 서버프로그램의 작업 스레드등에의 blocking-job 처리에는 쓰일 수 있을듯도 하다. ((예를 들어 다른 서버에 특정 데이터를 요청하고 이게 올 때가지 기다리면서 다른 fiber가 실행되게 하거나, 때때로 스케쥴링되서(물론 유저가 해줘야) 데이터가 왔는지 해당 파이버에서 검사하는 식으로 구현될 수 있다)) 덤으로 ::TerminateThread()가 아주 예외적인 경우를 제외하고는 쓰지 말아야하는 함수인 것과는 대조적으로 ::DeleteFiber()는 아주 안정적으로 동작할 수 있다 — 왜냐하면 특정 fiber가 실행될지 말지는 유저 관점에서 완벽히 알 수 있기 때문에.
장점이 있긴하지만 다음과 같은 문제는 좀 괴롭다.
스케쥴링을 직접해야 한다
커널에서 지원되는 스레드나 성숙된 유저 레벨 스레드 라이브러리가 편한(?) 것은 스케쥴링 문제가 직접 노출되어 있지 않기 때문이다. Fiber는 SwitchToFiber()를 호출해서 직접 실행시켜주기 전에는 돌지 않기 때문에 (thread는 언젠가는 OS가 CPU를 쓰게 해준다), 적절한 시점에 적절한 Fiber를 골라서 동작하게 만들어 줄 수 있다. 그리고 스케쥴링은 절대 쉬운 문제가 아니다 — 물론 잘만들면 성능 향상을 기대할 순 있다.
Fiber는 비동기 작업의 대안이 아니다
Fiber로 비동기 작업을 할 수 있긴 하지만(polling이나 fiber가 스케쥴링으로), scalable하진 않다. 비동기 작업 당 1개의 fiber가 필요해서 최대 비동기 작업 수를 알아내거나, 전통적인 비동기 작업 처리 방식들을 섞어써야한다 — 그리고 그럴 바엔 그냥 잘 정의된 멀티스레드 응용을 쓰는게 낫다.
Fiber가 블럭되려면 결국엔 스레드가 블럭된다
Fiber는 OS관점에서 보이는게 아니기 때문에, 서로 다른 fiber가 동기화되려면 결국엔 fiber밑에 있는 스레드들을 동기화 시키는 수 밖에 없다. Mutex 같은 커널 객체에 락을 거는 주체는 thread지 fiber가 아니다.
뭐 이런 이유로 정말 필요한 상황 — 예를 들어 co–routine이 필요하다거나 — 이 아니면 멀티스레드 응용에서 fiber 사용은 권장할만한 일이 아니라고 생각한다.
한번에 한 파이버밖에 못 도니까 성능으로는 MT를 따라잡진 못할 거고, 따라서 ST 기법과 비교하자면, 여러 작업이 병렬적으로 수행되는 코드를 짤 때 수행의 연속성이 코드상에 드러난다는 게 파이버의 주요 장점일 듯.
그런데 코드 중간에 스위치해서 나가 버리면 역시 코드 짜기 짜증날 거라는 생각이 드네. 파이버와 MT를 섞어 쓰면 ‘락을 잡은 상태에서 나가면?’ 이런 문제도 있고. MT만큼 예측불허의 사건이 벌어지는 것은 아니지만, 코딩할 때 ST보다 더 많은 것을 머리에 올려놔야 할듯해.
(어제 저녁에 컨테이너 소멸중에 컨테이너 다시 건드리는재진입 문제로 고생했ㅎ)
사실 MT IOCP가 패킷을 뽑아다가 큐에 쏟아넣고 로직 스레드는 ST+파이버로 쓰는 구조를 고려해봤었는데 성능이 CPU 개수에 대해 스케일러블하지 않을듯해서 버렸음.
그 scalable하지 않을 듯한 방법(로직 부분에 ST + fiber)을 쓰자는 사람이 있는데 조금만 더 생각해서 주장해줬으면 하는 소망이 있음;
직접 스케쥴링하는데서 뽑아낼 수 있는 이득이 없으면 fiber가 득보다 실이 클 것 같은데 말이지 -_-a
제생각엔 구조상 뭔가를 요청하고 대기하는 구조에 적합할껏 같습니다만… 그렇다면 클라이언트가 더 적당할껏 같은데요.
이를테면.. 서버에 뭔가를 요청할때,,
void foo()
{
메세지 전송..
응답이 올때까지 대기…
응답을 처리..
}
이처럼 한 함수안에서 모두 처리가 가능하니 편리할껏 같은데요. 데이타 추상화나, 응답에 대한 콜백같은것도 필요없고,,,
물론 스레드로도 가능하지만 스레드 전체가 블로킹되니 파이버가 더 좋을듯 싶은데요.
파이버는 유저 레벨 스레드 (*nix에서 흔히 쓰는) 를 윈도우즈에 가져온 개념에 가깝습니다.
말씀하신 형태의 내용은 흔히 co-routine 개념으로 구현하는 경우가 많고요, 언급한 장점때문에 씁니다.
다만 Windows fiber는 실제 실행 자체는 thread 문맥에 의존하기 때문에, thread A위에서 도는 fiber1이 CriticalSection 같은 커널 동기화 객체를 획득하고, fiber2가 다시 thread A 위에서 돌면 fiber2는 해당 동기화 객체를 획득한 것으로 인식합니다. (커널 수준에서 fiber란게 없기 때문에…)
그래서 매우 미묘하고 잡기 힘든 버그를 만들어서 Unix 응용 프로그램을 포팅하는 수단 외에는 쓰지 말라는 얘기도 있습니다.