Tagged: thread

Thread

이 문서는 Perl Thread 문서를 기반으로 작성되었습니다. Thread 의 기초부터해서 Thread의 구현을 Perl, Python 으로 직접 구현해보고 비교하도록 하겠습니다.

출처: http://www.xav.com/perl/lib/Pod/perlthrtut.html

Thread?

쓰레드(Thread)는 프로세스(Process)보다 작은 프로그램 단위 입니다. 좀 더 정확하게 말하자면 프로그램은 적어도 한개의 프로세스로 구성되며 적어도 하나의 쓰레드로 구성됩니다. 적어도 하나의 프로세스에서 하나의 쓰레드는 결국 하나의 실행 포인트를 가집니다. 적어도 하나의 프로세스내에서 다중 쓰레드를 쓴다는 것은 결국 다중의 실행 포인트를 가진다는 것을 의미 합니다.

다중의 실행 포인트?

요즘에 멀티코어(Multi Core) CPU가 많이 나옵니다. 프로그램은 이 멀티코어 CPU에 의해서 실행되는데 프로그램이 실행될때에 1개의 코어만 사용한다면 비효율적일 것입니다. 그래서 프로그램이 실행될때에 멀티코어를 이용한다면 빠르게 동작하게 될 것입니다.

프로세스도 마찬가집니다. 비록 멀티코어가 아닐지라도 하나의 프로세스내에서 다중의 실행 포인트로 동시에 한꺼번에 실행이된다면 빠를 것입니다. 이것이 바로 쓰레드 입니다. 하나의 프로세스내에서 다중 실행 포인트로 동시에 실행되는 것, 그것이 쓰레드 입니다.

쓰레드 프로그램 모델

쓰레드에는 기본적으로 3개의 모델이 있습니다. 정확하게 말하자면 프로그램상에서 구현하는 방법론적인 것입니다.

Boss/Worker 모델

보스(Boss), 실행(Worker) 모델입니다. 하나의 보스(boss)와 여러개의 실행기들로 구성됩니다. 보스는 일을 처리하기위한 태스크(Task)를 생성하고 모은다음에 적절한 실행기에 할당합니다.

이런 모델은 GUI나 서버 프로그램에서 흔하게 사용됩니다. 메인 쓰레드는 특정 이벤트(Event)가 발생할때까지 기다리며 처리(process)를 위해서 실행기(Worker)에게 이벤트를 통해서 보내집니다. 이렇게 이벤트를 실행기(Worker)에게 보내고 난후에 보스는 다른 이벤트를 기다립니다.

보스(Boss) 쓰레드는 적은 일을 합니다. 태스크가 다른 메소드보다 좀더 빠른 처리가 필요하지 않는다면 가장 최적의 사용자 응답시간을 가지게 됩니다.

Work Crew 모델

work crew 모델은 기본적으로 서로다른 데이터 부분을 갖는 같은 일을 처리하기위해서 여러개의 쓰레드를 가집니다. 이것은 전통적으로 병렬프로세싱(Paraller Processing)과 벡터 프로세싱(Vector Processor)와 유사합니다.

이 모델은 특정한 환경에 적합한데, 서로다른 프로세서들을 통해서 다중의 쓰레드들 분배해 프로그램을 실행하는 시스템과 같은 특정한 경우입니다. ray tracing 이나 렌더링 엔진에 적합한데, 개별적인 스레드들이 사용자에게 비주얼한 피드백을 줄수 있는 중간 결과물들을 전달할 수 있는 곳에도 적합니다.

Pipeline 모델

파이프라인(Pipeline)은 태크스(Task)를 스텝(Step)으로 나눕니다. 그리고 하나 스텝의 결과를 다음 쓰레드 처리기에 전달합니다. 각각의 쓰레드는 각 데이터 조각을 가지며 라인상에서 다음 쓰레드에 결과를 전달합니다.

이 모델은 병렬 처리를 하는 두개 이상의 다중 쓰레드를 처리하는 다중 프로세서를 가진다면 중요한 의미를 지닙니다. 이 모델은 개별적인 태스크를 작고 단순하게 유지하는 경향이 있으며 I/O나 시스템 콜과 같이 다른부분의 파이프 라인이 처리를 위해서 특정 파이프 라인를 블럭(block)하는 것을 허용하기도 합니다. 만약 다른 프로세서들에서 파이프라인의 일부를 실행한다면 각 프로세서는 그것을 캐시로 다룰 수 있는 이득을 얻을 수 있습니다.

이 모델은 또한 서브루틴 콜을 실행하기위해서 다른 쓰레드를 생성하는 것보다 재귀적 프로그래밍에서 유용합니다. 프라임과 피보나치 수혈은 파이프라인델을 잘 보여주는 폼(form)입니다.

Native Threads

시스템에서 쓰레드를 구현하는데에는 여러가지 방법이 있습니다. 쓰레드를 구현하는데에는 OS 제작사와 버전에 의존적입니다. 보통 초기버전에서는 단순하지만 버전이 향상됨에 따라 정교해(sophisticated) 집니다.

쓰레드에는 3가지의 기본 카테고리가 존재합니다. User-mode threads, kernel threads, multiprocessor kernel threads 입니다.

User-mode 쓰레드는 프로그램이나 라이브러 전체를 거쳐서 생존하는 쓰레드 입니다. 이 모델에서 OS는 쓰레드에 대해서 아무것도 알지 못합니다. 이 모델을 구현하기는 쉽고 대부분의 OS는 이를 초기에 지원합니다. 이 모델의 단점은 OS가 쓰레드를 알지 못하기 때문에 하나의 쓰레드는 모든 다른 쓰레들을 블럭할 수 있습니다. 전통적 블럭킹 활동에는 시스템 콜, 대부분의 I/O, sleep과 같은 것이 있습니다.

Kernel Threads 는 좀 더 진화한 버전입니다. OS는 커널 쓰레드에 대해서 알고 있으며 그들에대해서 통제권한을 가집니다. 커널 쓰레드와 유저 쓰레드의 주요한 차이점은 블럭킹입니다. 커널 쓰레드에서 하나의 쓰레드 블럭은 다른 쓰레드를 블럭하지 않습니다. 이것은 사용자 쓰레드는 없는 것으로 커널은 프로세스 레벨에서 블럭을 하지 쓰레드 레벨에서 블럭을 하지 않습니다.

이것은 큰 진전으로 쓰레드된 프로그램은 쓰레드되지 않은 프로그램에 비해서 큰 성능을 제공합니다. 예를들어 I/O 블럭인 쓰레드는 다른 것을 수행하는 스레드를 블럭하지 않습니다.

특정 시점에서 커널 쓰레딩이 쓰레드에 인터럽트(Interrupt)를 걸게되면 프로그램이 만들어낸 묵시적인 락킹(locking) 소비(?)를 해제시킵니다. 예를들어 단순하게 $a = $a + 2 를 생각해보면 다른 쓰레드에서 $a를 바꿨고 그 사이에 값을 가지고 오거나 새로운 값을 저장하는 사이에 $a를 다른 쓰레드에서 보여진다면 커널 쓰레드는 예상치 못한 행동을 할수 있습니다.

멀티프로세서(Multiprocessor) 커널 쓰레드는 쓰레드 지원에서 최종 종착지입니다. 멀티 CPU를 가진 시스템에서의 멀티프로세서 커널 쓰레드에서 OS는른 CPU에서 동시에 실행하기위해서 두개 혹은 그 이상의 쓰레드를 스케줄 합니다.

이것은 하나 혹은 그 이상의 쓰레드에서 동시에 실행시켰을때 쓰레드된 프로그램에서 매우 큰 성능을 제공합니다. 반면에 기본 커널 쓰레드에서 나타나지ㄶ았던 naggin synchronization 이슈가 나타납니다.

Cooperative multitasking 시스템에서 제어(control)를 하지못하도록 쓰레드를 운영해 왔습니다. 만약 쓰레드가 yield function 을 호출하면, 제어를 회수합니다. 또, 쓰레드가 I/O와 같은 블럭을 발생시키는 무언가를 실행했을때도 제어를 회수합니다. cooperative multitasking 구현상에서 하나의 쓰레드는 CPU time 에 대해서 모든 다른 것들을 소모시킵니다.

Preemptive multitasking 시스템은 다음 쓰레드를 결정하는 동안 일정한 간격으로 쓰레드를 인터럽트 합니다. preemptive multitasking 시스템에서는 보통 하나의 쓰레드가 CPU를 독점하지 않습니다.

어떤 시스템에서는 cooperative 와 preemptive 쓰레드를 동시에 운영할 수도 있습니다. (realtime 우선순위에서는 cooperative, normal 우선순위에서는 preemptive)

Thread in Perl

이제 Perl에서 쓰레드를 사용하는법을 익혀보겠습니다. 먼저 Perl 이 쓰레드를 사용할 수 있는지를 체크해야 합니다. Perl 을 컴파일 설치할때에 Thread 관련 옵션을 지정하지 않았다면 쓰레드가 사용불가 일수도 있습니다. 따라서 Perl 쓰레드를 사용할때는 코드 맨 앞에 다음과 같이 해줌으로서 Thread 지원여부를 체크할 수 있습니다.

위 코드를 실행했을때 오류 메시지가 나온다면 Perl 이 Thread를 지원하지 않는 것임으로 다시 설치해줘야 합니다.

Perl에서 Thread를 사용하는 것은 기타 언어에서와 거의 흡사합니다.

Thread를 생성할때는 new 메소드를 이용합니다. 서브 프로시져명을 첫번째 인자로 주고 그 프로시져에 인자로 전달할 파라메터를 나열하면 됩니다. 위의 예제는 사실 유용하지 못합니다. 어짜피 실행포인트가 하나인데 구지 쓰레드를 쓸일이 없기 때문입니다. 하지만 다음과 같이 쓰레드를 여려개를 만들면 효과가 있게 됩니다.

출력 결과

여기서 주목해야 할 것이 있습니다. 마지막 줄에 “A thread exited while 2 threads were running.” 쓰레드는 자원을 공유 합니다. 메모리나 cpu 도 공유를 하는데, 그야말로 프로그램의 모든것을 공유하는 것입니다. 그런데 어느 Thread가 먼저 끝날지 모릅니다. 어떤 Thread 가 프로그램을 종료한다면 다른 Thread는 실행이 중단되고 종료 되겠죠. 위의 마지막 줄은 그러한 상황으로 발생된 메시지 입니다. 해결방법은 다음과 같습니다.

join 은 다른 Thread 가 작업을 마치기를 기다립니다. 그래서 모든 Thread가 작업을 마치면 쓰레드는 소멸되죠. 이렇게되면 위의 문제는 해결이 되는거겠죠. 위 코드를 다음과 같이 바꾸면 효율적일 것입니다.

for 문과 foreach 문을 이용해서 Thread를 생성하고 join 하도록 되었습니다.

Thread 관리

Thread를 생성하고 끝내는 방법을 배웠습니다. 그런데 여기서 한가지 드는 의문이 있습니다. 예를들어서 5천대 서버에 뭔가를 할려고 한다면 Thread를 5천개를 만들어야 합니다. 그런데 Thread도 자원을 소모하게 되어 있어서 이렇게 많이 생성을 한다면 메모리를 많이 소비하게 됩니다. 더군다나 5천개의 Thread를 동시에 실행한다는 것은 CPU에게는 좀더 빠르게 왔다리 갔다리 하면서 일을 해야한다는 것을 뜻하게 되고 이는 결국 OS의 Context Switch 개수를 높이게 되고 이는 결국 서버에 부하를 유발하게 됩니다.

그래서 Thread 를 50개 정도만 생성하고 그중에서 끝나는 Thread에게 다시 일을 막기는 식으로 하면 시스템도 보호하고 원하는 작업을 모두 끝낼수가 있습니다. 쉽게말해서 Thread poll 과 같은 개념이라고 보시면 됩니다. 5천개의 서버목록을 Queue에 넣은다음에 Thread를 20개를 생성하고 Thread가 이 큐에서 가져다 쓰게 하면 효율적일 것입니다.

이와같은 개념을 Perl Thread에서는 지원하고 있습니다.

위 코드는 Thread::Queue 를 이용해 Thread 관리를 통해서 구현된 것입니다. Thread의 개수는 변수 $threadcount 에서 지정되고 각각의 Thread들은 enqueue 메소드를 통해서 queue에 넣어집니다. 그리고 dequeue 를 통해서 Thread를 사용하고 있지요. join 시에 사용되는 threads->list 는 Thread들의 객체(?)를 들고 있기 때문에 이를 활용하면 join을 쉽게 구현할 수 있습니다. 수천개의 같은 일을해야하는 경우 $threadcount 는 20으로 제한되어 있기때문에 20개의 Thread만 가지고 수천개를 처리하게 됩니다.

동시성 문제

동시성 문제는 컴퓨터 세계에서는 매우 중요하게 다루어지는 대상입니다. 대부분의 프로그램이 단일작업을 하는것이 아닌 다중 작업, 즉 Thread 라든지 Multi processing 기반으로 작성되어진다면 십중팔구 동시성문제를 해결하지 않고는 올바르게 작동할 수 없습니다. 동시성 문제를 가장 신경쓰는 분야중 하나는 아마도 데이터베이스 시스템일 것입니다.

데이터베이스에서의 동시성 문제는, 예들들면 한순간에 동시에 두명이 레코드를 접근한다고 생각하면 하나는 Select를 다른 하나는 Delete 를 했을 경우 과연 어떻게 될것인가 하는 문제입니다. 데이터베이스 시스템은 다중 접속을 허용하는 프로그램이며 동시에 두명 이상이 같은 레코드를 조회할 가능성이 항상 존재하게 됩니다. 두명 이상의 조회하는 가운데 모든 사람이 같은 명령을 내릴수도 있고 위와같이 Delete 하는 쿼리를 내릴수도 있는데 이럴경우 되돌려주는 결과물을 어떻게 할 것인가 하는 문제가 생깁니다. 이게 바로 데이터베이스 시스템에서 신경쓸수 밖에 없는 동시성 문제입니다.

Thread 에서도 이와 비슷한 동시성 문제가 발견이 됩니다. 파일을 쓰거나 변수를 조작할 경우에 주로 발생한다. 이를 해결하는 방법을 없을까? 어떤 Thread 가 자원에 접근하고 있을때에 그 자원에 대해서 다른 Thread가 못쓰게 하면 되지 않을까? 이를 위해서 Thread는 Lock, Mutex, semaphore 를 지원합니다. 여기서는 perl 에서 lock 을 이요하는 방법을 알아보겠습니다.

위 예제를 보면 lock 을 어떻게 사용하는지를 한눈에 알수 있습니다. 파일에 쓰기 작업을 하기 전에 lock 을 걸어서 동시성 문제를 해결하고 있지요.

Thread in Python

Python 에서도 Thread를 지원하고 위의 내용을 모두 적용해서 사용할 수 있습니다. queue를 이용해 Thread를 관리하고 lock 을 사용해서 동시성문제를 해결하는 것은 완전 perl 비슷합니다. 오히려 python 이 좀더 직관적이다라고 볼수 있지요. 그런데 문제가 있습니다. Thread Queue 를 사용하기 위해서는 Python 버전 2.6 이상 사용해야 합니다.

Thread에 대한 것과 Queue 를 이용한 방법은 아래의 링크에 매우 잘 설명이되어 있습니다.

http://www.ibm.com/developerworks/kr/library/au-threadingpython/index.html

Python 의 동시성 문제도 Perl 과 같이 lock 을 씁니다. 예를 들면 다음과 같습니다.

lock객체를 얻어서 lock 을 얻고 lock 을 풀어줘야 합니다.