brunch

You can make anything
by writing

C.S.Lewis

멀티스레드 - 스레드 제어

* 이 내용은 철저히 초심자를 위해 알기 쉽게 설명하는 것을 목적으로 하고 있습니다.

* 더 정확하고 자세한 개념은 다른 고수님들의 글들을 참고하시길 바랍니다.

* 그리고 이 글에서는 코드 최소한으로 다루고 있습니다.


이전 글에서 스레드는 어떤 녀석이며 어떤 특성을 가지고 있는지에 대해 배웠습니다.

이번엔 이 스레드들을 제어하는 여러 방법들에 대해서 알아보도록 하겠습니다.


Thread.sleep

가장 쉬운 방법은 바로 sleep, 스레드를 잠시간 재우는 것입니다.

sleep 은 밀리세컨드 단위로 재울 수 있습니다. 이것으로 어떻게 순서제어를 하느냐고요? 토끼와 거북이 우화를 생각해 보세요.


아무리 빠른 스레드라 하여도 중간에 잠을 재우게 된다면 다른 스레드가 먼저 도착할 수 있습니다.

이렇게 스레드를 강제로 휴식하게 하면서 순서를 조정할 수 있습니다.

무척 쉬운 방법입니다만 가장 비 효율 적인 방법이기도 합니다.


Thread.sleep()을 사용하면 InterruptedException이라는 일반 예외가 발생할 수 있기에 반드시 예외 처리가 필요합니다. 그리고 시간 계산을 잘하지 못하면 순서제어에 실패할 수 있거나 필요 이상의 긴 휴식을 줌으로써 효율이 떨어질 수 있습니다.


Sychronized(동기화)

갑자기 뜬금없는 이야기 일 수 있습니다만, 공용 PC 사용에 대한 이야기를 해 볼까 합니다.

A와 B는 한 공간에 PC를 사용하고 있습니다. 


A는 게임에 정말 진심인 친구였는데요, 이 친구가 오랜만에 열심히 게임을 해서 등급을 플래티넘까지 올렸습니다. 그리고 잠시 자리를 비운 사이 B 가 와서 A 가 하던 게임을 하게 됩니다. 하지만 B는 A 만큼의 실력자가 아니었기에 등급이 떨어지고 말았습니다.

이후 A 가 다시 자리에 돌아왔을 때 자신이 올려놓았던 등급이 다시 떨어져 있는 모습을 보았다며 어떤 기분이 들었을까요?


이런 상황은 두 사람이 같은 PC(공통 자원)를 사용하고 있기에 발생한 것이죠?

앞에서 스레드는 같은 메모리를 공유한다고 하였습니다. 그렇기에 하나의 메모리에 두 스레드가 동시에 사용하게 된다면 이런 불상사가 생길 수 있습니다. 그럼 이 문제를 어떻게 해결해야 할까요?

의외로 간단합니다. A의 입장에서 PC를 완전히 사용하기 전 까지는 PC를 잠가놓는 것입니다.

그럼 A 사 자리를 비웠다 하더라도 B는 PC를 사용할 수 없을 것입니다.

우리는 이 기능을 Synchronized라고 부릅니다.


그림과 같이 누군가 사용하고 있다면 추가로 PC를 사용하고 싶은 스레드는 줄을 서서 다음 차례를 기다려야 합니다. 이렇게 synchronized를 사용하면 먼저 들어간 스레드 이후에 줄을 세워 두기에 특정 스레드가 일을 처리하는 중에 다른 스레드가 먼저 처리되는 일은 절대로 없을 것입니다.


그럼 synchronized의 사용 방법은 어떻게 될까요?

이 기능은 사용하고 싶은 메서드에 추가하거나 메서드 내의 특정 영역에 블록 형태로 지정해 놓으면 됩니다.

두 방법의 차이는 무엇이냐고요?


바로 줄 서서 대기하는 곳의 차이입니다.

어떤 식당은 건물 밖에서 기다려야 하는 반면, 어떤 식당은 건물 안에서도 대기할 수 있는 곳이 있습니다. 이 두 식당의 차이가 무엇인지를 곰곰이 생각해 봅시다. 


Thread.yield()

우리는 양보의 미덕을 잘 알고 있습니다.

그리고 대중교통에서도 자리 양보는 기본으로 여기고 있죠? 우선 우리가 아는 양보를 보겠습니다.

우리가 일반적으로 보는 양보의 일반적인 모습입니다.

스레드에도 마찬가지 양보라는 개념이 있습니다만, 우리가 생각하는 양보와는 조금 다릅니다.


스레드 에서의 양보에 개념은 자리에 앉을 수 있는 기회를 한번 주는 것에 불과합니다.

그래서 스레드에서 양보를 받기 위해서는 기회를 줄 때 재빠르게 실행해야 합니다.


그렇기에 한쪽 스레드에서 아무리 Thread.yield()를 통해 지속적으로 양보를 한다 해도 Thread-1 이 즉각적인 응답을 하지 못한다면 우리가 생각한 대로 실행이 이루어지지 않습니다.

우리들 기준의 양보라면 Thread-0 이 양보를 했으니 Thread-1 이 계속해서 수행해야 하니까요.

그래서 이 양보는 ‘너도 할 수 있는 기회를 주마’ 정도로 생각하면 됩니다.


join()

Join 은 합류를 의미합니다. 스레드에서도 합류하는 의미로 사용이 되는데 그게 어떤 상황인지 한번 살펴보겠습니다.

스레드 0과 1에게 각각 다른 업무를 맡길까 합니다.

스레드 0에게는 1부터 100까지의 합을 계산하여 result라는 변수에 담으라고 시키고 스레드 1에게는 result의 값을 출력하라고 시킬 예정입니다.

동시에 둘이 이 내용을 수행했을 때 result는 어떤 값을 출력할까요? 어떤 업무가 더 빨리 끝날까 생각해 보면 답은 뻔히 나오죠?


그래서 join()을 사용하여 특정 스레드와 합류(join) 할 때까지 기다리게 합니다.

그럼, Thread-1 은 Thread-0 이 계산하여 result 변수에 값을 넣은 다음 출력을 하게 됩니다.


이렇게 먼저 도달한 스레드도 다른 스레드가 합류할 때까지 진행하지 못하도록 막는다 하여, 이러한 기능을 blocking이라고 부릅니다.


wait(), notity(), notifyAll()

불침번이라고 들어 보셨나요? 군대에서 밤중에 잠을 자지 않고 특정 시간 동안 경계를 하고 다음사람을 깨운 다음에 본인이 잠을 자는 것을 말하죠.

이때 중요한 것은 내 다음 차례의 사람을 깨운 다음 내가 자야 한다는 것입니다.

만약 누군가를 깨우지 않고 내가 먼저 잠들었다면? 큰일 나겠죠?

그래서 반드시 notify()를 통해 다른 스레드를 깨우고, 자기 자신이 wait()을 통해 대기를 해야 합니다.

그렇지 않으면 모든 스레드가 대기 중인 상태를 유지할 테니까요.


여기서 notity()와 notifyAll() 두 가지 메서드가 나오게 됩니다.

notifyAll() 은 wait 상태에 있는 모든 스레드를 깨우는 것입니다. 하지만 notify()는 하나의 스레드 만을 깨우는 메서드인데, 주의할 점은 특정한 스레드가 아니라 임의의 스레드 한 개를 깨운다는 것입니다.


Thread 상태

앞에서 스레드의 wait 상태에 대해서 여러 번 언급이 되었습니다. 이처럼 Thread는 여러 상태가 존재하며, 이 상태 값을 통해 현재 Thread의 상태를 확인해 볼 수 있습니다.

스레드의 상태는 크게 New, Runnable, Terminated의 상태로 나눌 수 있으며 자동차의 상태와 비교하자면 위와 같습니다.

NEW는 새로 생성된 상태입니다. RUNNABLE 은 언제든지 실행할 수 있도록 준비된 상태이며, 막상 실행은 아무런 상태 값을 가지고 있지 않습니다. 마지막으로 TERMINATED는 다 사용한 스레드를 종료하여 폐기한 상태를 의미합니다. 또한 쉬는 상태는 앞에서 배운 wait()을 통해 쉬는 상태는 waiting으로, sleep()을 통해 쉬는 상태는 timed waiting으로 표기가 됩니다.


Demon Thread

메인 스레드가 자신을 위해서 일해줄 스레드를 복사하는데 이것을 워크 스레드라고 부른다고 했습니다. 그리고 이 워크스레드 중에서는 데몬스레드라는 녀석이 존재합니다.

이 워크스레드와 데몬 스레드의 차이는 무엇일까요?


메인스레드가 워크스레드와 데몬스레드에게 일을 시켰다고 가정해 보겠습니다.

이때 메인스레드는 해야 할 일을 다 마치고 집에 돌아간다고 했을 때 워크스레드는 본인의 일이 끝나지 않았으면 메인스레드와는 별개로 움직입니다.

하지만 데몬 스레드는 자신의 일이 끝나지 않았음에도 메인스레드를 따라 집에 들어갑니다.


이런 데몬스레드를 만들어 줄 때는 한 가지만 추가하면 됩니다.

스레드를 생성하고 star() 하기 전에 setDaemon(true)를 실행해 주는 순간 해당 스레드는 데몬 스레드로 변화되게 됩니다. 다시 워크 스레드로 되돌리고 싶다면 setDaemon(false)를 사용하면 됩니다. 


Thread 정지

시작된 스레드는 언젠가는 정지해야 합니다.

기본적으로 스레드는 본인이 해야 할 일이 다 끝나면 자동으로 종료됩니다.


하지만 무한루프와 같이 끝나지 않는 상황에서는  스레드를 강제로 정지시켜줘야 합니다.

이때 우리는 스레드를 start() 메서드를 사용하여 시작했기 때문에 stop() 메서드로 정지하는 것을 떠 올릴 수 있습니다. 하지만 stop() 메서드는 사용을 권장하고 있지 않습니다.


그 이유는 stop() 메서드는 마치 PC 방에서 예고 없이 전원을 내리는 것과 비슷하기 때문입니다.

만약 여러분들이 PC에 중요작업을 하는데 예고 없이 PC 가 꺼졌다면 어떨까요?

아마도 작업 중이던 내용이 저장되지 못할 것입니다. 그래서 PC 방에서는 사용시간이 종료되기 5분 전, 1분 전 미리 알려주는 것입니다. 

스레드도 마찬가지입니다. 그럼 스레드는 어떤 식으로 종료를 해야 할까요?


첫 번째는 stop이라는 변수에 true/false 값을 넣어 종료 여부를 변경가능 하도록 하는 것입니다.

이는 마치 정지/출발을 깃발신호를 보내듯이 변경할 수 있다 하여 stop flag라고 부릅니다.

만약 stop() 메서드를 사용한다면 while 문 안에서 그대로 멈춰버리게 되지만, stop flag를 사용하면 while을 빠져나와 아래 내용들을 수행할 수 있습니다.


두 번째는 강제 인터럽트라 하여 특정 스레드에 interrupt()라는 메서드를 사용하여 해당 스레드에 인터럽트를 일으키는 것입니다.

인터럽트란 긴급한 처리를 위해 CPU의 현재 작업을 멈추게 하고 먼저 끼어드는 것을 의미합니다.

그리고 While 문에서는 실행 중 인터럽트가 발생하는 것을 감지하면 즉시 break를 통해 while 문을 빠져나오는 것이죠. 그리고 이후의 일을 수행하도록 합니다.


이상으로 스레드의 제어에 대해서 알아보았습니다.

스레드를 제어할 방법은 이렇게 많습니다. 하지만 그래서인지 뭔가 복잡해 보이는 것도 사실이죠.

하지만 스레드의 본래 속성을 이해하고, '이 친구들을 제어할 여러 방법들이 있다.' 정도만 이해하셔도 좋습니다. 이 정도만 이해해도 정식으로 프로그래밍을 배울 때 스레드 이해에 많은 도움이 될 것입니다.

매거진의 이전글 멀티스레드 - 스레드의 개념
브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari