◎위챗 : speedseoul
http://dlucky.tistory.com/203
제1절 쓰레드의 개념
1.1 쓰레드란
쓰레드(thread)는 사전적으로 "실" 혹은 "(구슬 등을) 실에 꿰다"는 의미를 갖고 있는데, 컴퓨터에서 프로그램을 수행할 때에도 각각의 명령어를 실에 꿰듯이 순차적으로 수행하기 때문에 '쓰레드'라는 용어를 사용하게 된 것으로 생각된다. 아직까지 쓰레드라는 용어에 대한 적합한 번역이 없기에 여기서도 그대로 쓰레드라는 용어를 사용하고자 한다.
프로세스(process)는 "컴퓨터에서 실행되고 있는 프로그램"을 말하는 것으로, 쓰레드를 프로세스와 동일한 의미로 사용하는 경우가 많이 있으나 다음과 같은 약간의 차이를 갖고 있다. 즉, 프로세스는 완전한 하나의 프로그램이 실행되는 것으로서, 예를 들어 fork()를 통해 생성되는 프로세스는 원래의 프로세스와 똑같은 변수와 코드 등 여러 가지 면에서 똑같은 복사본이다. 따라서 부모와 똑같은 자식을 생성하기 위해서는 많은 자원(resoure)이 요구된다. 그러나, 쓰레드는 프로세스와는 달리 부모 프로세스 전체의 복사본을 만들지 않고, 필요한 코드 만을 생성하여 동시에 수행한다. 또한, 부모 쓰레드가 갖고 있는 모든 데이터를 완전하게 접근할 수 있기 때문에, 훨씬 더 효율적이다.
대부분의 프로그램은 시작에서부터 끝에 이르기까지 모든 명령어들이 순차적으로 수행되면서 하나의 프로세스를 생성하기 때문에 단일 쓰레드 프로그램이라고 한다. 그러나, 프로그램에 따라서는 여러 가지의 함수가 서로 독립적으로 수행되면서 여러개의 프로세스를 생성하기 때문에 다중 쓰레드 프로그램이라고 한다. 다중 쓰레드 프로그램의 예로는 채팅 프로그램을 들 수 있을 것이다. 채팅 프로그램에서는 사용자가 키보드를 통해 글을 쓰는 동안에도 상대방이 글을 썼는지를 지속적으로 감시한다. 즉, 사용자의 입력을 기다리는 프로세스와 상대방이 쓴 글을 화면에 출력해주는 두 개의 프로세스를 갖기 때문에 다중 쓰레드 프로그램이 된다.
1.2 쓰레드의 생성 및 수행
(1) 스레드의 생성과 종료
쓰레드를 생성하는 방법에는 두 가지가 있는데, 첫째로는 Thread 클래스로 부터 상속을 받아서 만들수도 있다. 자바는 다중상속을 허용하지 않기 때문에 Thread 클래스를 상속받으면 다른 클래스를 상속하여 수행할 수가 없다. 따라서 이 방법은 독립적으로 수행되는 쓰레드를 생성할 경우에 주로 사용된다. 둘째로는 둘째로는 Runnable 인터페이스를 구현(implements)해서 쓰레드를 생성하는 것으로 GUI 프로그래밍에서와 같이 이미 다른 클래스를 상속하여 그 특성을 유지한 프로그램을 작성할 필요가 있을 때 주로 사용된다.
쓰레드를 실행하기 위한 전제조건으로 run() 메소드가 필요한데, 여기에 쓰레드가 수행할 작업의 내용을 기술한다. 이와 같이 쓰레드를 이용하는 클래스를 선언하여 객체를 생성하면, 해당 객체에대해 start() 메소드를 호출하여 쓰레드를 실행시킨다. 쓰레드를 종료하기 위해서는 stop() 메소드를 호출한다.
다음은 Thread 클래스를 상속하여 생성한 클래스로부터 2개의 객체를 생성하여 실행시키는 예제 프로그램이다. 이 프로그램은 완전히 독립적인 두 개의 프로세스를 생성하여 실행하기 때문에, 실행결과로부터 count 변수도 개별적으로 소유하여 수행하고 있음을 볼 수 있다.
[예제 - Thread 상속의 예] class HelloThread extends Thread { |
위의 프로그램은 Thread 클래스를 상속받아서 작성되었는데, 이를 Runnable 인터페이스를 구현하도록 프로그램을 수정해 보면 다음과 같다. 여기서 Runnable 인터페이스는 run() 메소드만을 갖고 있기 때문에 run() 메소드만 구현하면 된다.
[예제 - Runnable 구현의 예] class HelloThread implements Runnable { |
(2) 쓰레드의 상태 전이
컴퓨터에서 하나의 프로세스(쓰레드)가 생성되면 이것은 대기상태에 있거나 혹은 CPU에서 처리 되는 등의 상태(status)를 거쳐서 결국은 종료(Dead) 상태에 이르게 되는 데, 이를 쓰레드의 상태전이라고 한다. 쓰레드의 상태가 전이되는 과정을 그림으로 그리면 다음과 같으며, 이를 상태전이도라고 한다.
먼저 각 상태에 대해 간략히 설명하면 다음과 같다.
다음으로 각 상태의 전이를 일으키는 쓰레드 메소드들에 대해 살펴보면 다음과 같다.
1.3 쓰레드의 우선순위
여러 개의 쓰레드를 구현하는 자바 프로그램에서는 각 쓰레드들이 빠르게 전환되면서 실행이 되어 쓰레드들이 동시에 실행되는 것처럼 보인다. 그러나 CPU는 대기중인 쓰레드들을 정해진 우선순이(priority)규칙에 따라 한 순간에 하나의 쓰레드만을 선택하여 수행한다.
자바에서도 각 쓰레드에 1에서 10까지의 우선순위를 부여함으로써 쓰레드의 실행 순서를 변경할 수 있다. 여기서, 1이 가장 낮은 우선 순위이고 10이 가장 높은 우선 순위로서, MIN_PRIORITY(숫자 1에 해당), NOM_PRIORITY(숫자 5에 해당), MAX_PRIORITY(숫자 10에 해당) 등의 상수를 사용할 수도 있다. 쓰레드에 우선순위를 지정하지 않으면 우선순위는 NOM_PRIORITY(5)이다. 쓰레드의 우선순위를 변경하려면, Thread 클래스의 setPriority() 메소드를 사용하며, 특정 스레드의 우선 순위를 알아보려면 Thread 클래스의 getPriority() 메소드를 사용한다.
다음은 위 프로그램을 약간 수정한 것으로, 두 개의 쓰레드에 각각 다른 우선 순위를 부여한 후에 수행하도록 하였다.
[예제 - Thread 우선순위의 예] class ThreadPriority extends Thread { |
프로그램의 실행결과를 살펴보면 MAX가 나중에 start()되었지만, 전반적으로 앞부분에서 실행되고 있음을 볼 수 있다. 또한, 두 번의 실행순서가 서로 다름을 볼 수 있는데 이는 CPU가 처리하는 다른 작업들에 영향을 받았기 때문이다. 즉, 각 쓰레드는 0.01초간의 휴식기간을 갖는데, MAX 쓰레드가 수행된 후 휴식기간중에 MIN 이 도착하면 MIN이 수행되지만, MIN이 실행되기 바로 직전에라도 0.01초가 지나서 MAX가 도착하면 우선순위가 높아서 MAX가 다시 수행된다.
1.4 동기화 (synchronized)
다중 사용자 프로그래밍 혹은 다중 쓰레드 프로그램에서 어떤 프로그램은 순차적으로 수행하지 않고 병렬적으로 처리할 경우에는 계산결과에 문제가 발생할 수 있다. 예를 들어, 은행에 10만원이 남아 있는데, 다음과 같이 두 개의 쓰레드가 중첩(overlap)되어 수행된다면 그 결과는 예상하지 못한 결과를 낳게 될 것이다.
시점 |
쓰레드 A |
쓰레드 B |
T1: |
- 현재잔고 → 10만원 |
|
T2: |
- 현금인출 → 10만원 - 5만원 |
- 현재잔고 → 10만원 |
T3: |
- 잔고기록 → 5만원 |
- 현금인출 → 10만원 - 3만원 |
T4: |
|
- 잔고기록 → 7만원 |
위에서 각 쓰레드는 정상적으로 수행되어 쓰레드A는 현금잔고 10만원에서 5만원을 인출한 후 잔고로서 5만원으로 계산하였으며, 쓰레드B도 같은 과정을 통해 잔고를 7만원으로 계산하였다. 따라서 최초의 현금잔고 10만원으로부터 각각 5만원과 3만원 (합계 8만원)을 인출한 후에 최종적으로 7만원이 남는 문제가 발생하게 되었다.
이러한 문제는 데이터베이스의 다중사용자(Multi-User)문제로서 동일한 데이터를 서로 다른 두 개의 쓰레드가 동시에 갱신(update)하는 과정에서 발생하는 것으로서, 갱신의 상실(Lost of Update)라고 한다. 이러한 문제를 제거하기 위해서는 하나의 쓰레드가 끝날 때까지 락(lock)을 걸어서 다른 쓰레드가 동일한 데이터를 갱신하지 못하도록 함으로써 해결할 수 있는데, 자바에서는 이를 동기화(synchronized)라는 문장을 이용해서 처리한다.
어떤 데이터를 공유하는 쓰레드 내부의 데이터를 동기화하기 위해서는 synchronized 키워드를 사용한다. 이 synchronized 키워드를 사용하는 방법은 두 가지가 있는데 첫번째는 메소드 선언부에서 하는 방법과 두 번째는 동기화 객체를 사용하는 코드 블록 부분만을 synchronized로 선언하는 방법이다. 첫 번째 방법은 메소드 내에서 처리할 내용이 많을 경우에는 스레드의 효율이 떨어지기 때문에, 메소드 내에서 처리할 내용이 많을 경우에는 두 번째 방법에서와 같이 동기화 객체를 사용하는 코드 블록 부분에만 synchronized로 선언한다.
public class SyncTest {
int ShareNumber =
0;
// 메소드 전체를 sychronized로 선언
public synchronized void
increaseNumber() {
ShareNumber++;
}
}
public class
SyncTest {
int ShareNumber = 0;
public void increaseNumber()
{
// 순수하게 공유문제가 발생하는 부분만을 synchronized로 선언
synchronized (this)
{
ShareNumber++
}
}
}
위에서 SyncTest 클래스는 synchronized 키워드가 붙어있는 메소드를 갖고 있기 때문에, 자바시스템은 SyncTest 클래스로부터 생성되는 모든 객체마다 락(lock)을 결합시킨다. 이후 어떤 객체의 동기화된 메소드가 호출되면 그 메소드를 호출한 쓰레드는 그 객체에 결합된 락을 잠근다. 따라서, 이 락이 풀리기 전까지는 동기화된 메소드 뿐만 아니라 이 객체를 호출하는 것 자체가 금지된다.
다음은 여러개의 쓰레드가 하나의 객체를 이용하여 작업을 수행할 때 부적합한 계산결과가 발행할 수 있음을 보여주는 예로서, A, B 두 개의 쓰레드가 각각 1~10과 1~5까지의 합을 구한다. 따라서, A 쓰레드의 합은 50, B 쓰레드의 합은 15가 되어야 한다.
[예제 - 동일한 객체에 접근하는 다중 Thread의 예] // 공유 데이터 클래스 |
이 프로그램에서는 쓰레드 A와 쓰레드 B가 교차하여 수행할 수 있도록 run() 메소드에 Thread.sleep() 메소드를 삽입하였다. 위의 프로그램을 수행하면 다음과 같은 결과를 얻게 되는데, 프로그램을 수행할 때마다 서로 다른 결과가 나올 수 있다.
쓰레드 A가 시작됨.
쓰레드 B가 시작됨.
쓰레드 A의 1~1까지 합은 1
쓰레드 B의
1~1까지 합은 1
쓰레드 A의 1~2까지 합은 3
쓰레드 A의 1~3까지 합은 6
쓰레드 B의 1~2까지 합은 8
쓰레드
B의 1~3까지 합은 11
쓰레드 A의 1~4까지 합은 15
쓰레드 A의 1~5까지 합은 20
쓰레드 B의 1~4까지 합은
24
쓰레드 B의 1~5까지 합은 29
쓰레드 A의 1~6까지 합은 35
쓰레드 A의 1~7까지 합은 42
쓰레드 A의 1~8까지 합은
50
쓰레드 B가 종료됨.
쓰레드 A의 1~9까지 합은 59
쓰레드 A의
1~10까지 합은 69
쓰레드 A가 종료됨.
위의 결과는 1~5 의 합은 29, 1~10의 합은 69로 계산되었는데, 이는 쓰레드 A와 쓰레드 B의 계산이 혼합되었기 때문에 나타난 결과이다. 따라서, 다음과 같이 run() 메소드를 수정하면 한번에 하나씩만 수행되기 때문에 정상적인 결과가 출력된다. 물론, 이럴 경우 이 프로그램은 쓰레드 프로그램으로서는 큰 의미를 갖지 못하지만, synchronized의 개념을 설명하기 위해 도입했을 뿐이다.
public void synchronized sumTo(int num) {
//
내용은 동일함.
}
위에서는 Summing 클래스내의 메소드에 동기화를 설정하였기 때문에, 특정 쓰레드가 이 메소드에서 작업을 수행하면 다른 쓰레드는 작업이 끝날 때까지 객체에 접근할 수 없다. 따라서, 다음의 실행결과에서 보는 바와 같이 쓰레드 A와 B가 각각 생성된 후에 A의 계산이 끝난 후 B가 시작되었다. 또한, 다음의 결과에서 보는 바와 같이 "쓰레드A가 종료"되었다는 메시지가 B의 계산이 시작된 이후에 나타나고 있는데, 이부분은 동기화와 관계없이 개별적으로 수행될 수 있기 때문이다.
쓰레드 A가 시작됨.
쓰레드 B가 시작됨.
쓰레드 A의 1~1까지 합은 1
쓰레드 A의
1~2까지 합은 3
쓰레드 A의 1~3까지 합은 6
쓰레드 A의 1~4까지 합은 10
쓰레드 A의 1~5까지 합은
15
쓰레드 A의 1~6까지 합은 21
쓰레드 A의 1~7까지 합은 28
쓰레드 A의 1~8까지 합은 36
쓰레드 A의
1~9까지 합은 45
쓰레드 A의 1~10까지 합은 55
쓰레드 B의 1~1까지 합은 1
쓰레드 A가 종료됨.
쓰레드 B의 1~2까지 합은 3
쓰레드
B의 1~3까지 합은 6
쓰레드 B의 1~4까지 합은 10
쓰레드 B의 1~5까지 합은
15
쓰레드 B가 종료됨.