멀티 쓰레드란?

JCChoo
11 min readJan 20, 2021

--

매번 면접 단골 문제… 쓰레드를 쓰기만 했지 무엇인지 잘 모른다. 그러던 중 아래 참고 url에 좋은 글이 있길래 공부 겸 번역해봤다…

참고 : https://medium.com/swlh/java-multithreading-48a21a361791

(번역은 개발이니 이해 부탁드립니다..)

한 번에 여러 프로그램(크롬, 카톡…) 을 동시에 실행하는 것이 멀티 프로그래밍이다. 마찬가지로, 한번에 여러 작업(프로세스, 프로그램, 쓰레드 등)의 실행은 멀티태스킹이다. 그들 사이의 가장 큰 차이점은, 멀티 프로그래밍은 context switching 개념에서만 작동하는 반면에, 멀티 태스킹은 시간 공유를 기반으로 한다는 것이다.

(context swiching과 시간 공유에 대해선 따로 공부 후 추가 반영하겠습니다.)

멀티쓰레딩은 CPU를 최대한 이용하기 위해 프로그램의 두개 이상의 부분을 동시에 실행하는 프로세스이다. 또한, 멀티쓰레딩은 단일 어플리케이션 내 특정 작업을 개별 쓰레드로 세분화할 수 있는 멀티 태스킹의 확장이다. 이러한 각 쓰레드는 병렬로 실행될 수 있다. 운영 체제는 처리 시간을 다른 어플리케이션뿐만 아니라 어플리케이션 내의 각 쓰레드간에도 나눈다. 여러 activities가 동시에 진행될 수 있는 방식으로 프로그램을 작성할 수 있다.

Thread란 무엇일까?

Thread는 기본적으로 경량 프로세스이며 Java는 멀티 쓰레드 프로그래밍에 대해 내장 지원을 제공한다. 멀티 쓰레드 프로그램은 동시에 실행할 수 있는 두 개 이상의 부분을 포함하는 하나의 프로그램이다. 그러한 프로그램의 각 부분을 Thread라 부른다. 모든 쓰레드는 별도의 실행 경로를 정의된다.

Java의 Main()함수는 어플리케이션을 실행하기 위해 Java Virtual Machine에 의해 만들어진 특별한 쓰레드에서 실행된다. 어플리케이션 내부에서, 더 많은 쓰레드를 만들고 시작할 수 있으며, 이는 메인 쓰레드와 병렬로 애플리케이션 유틸리티의 일부를 실행할 수 있다. 기억해야 할 또 다른 점은, 병렬로 실행되는 쓰레드는 실제로 같은 인스턴스에서 실행된다는 의미가 아니다. 프로세서의 접근 제어는 쓰레드 사이의 전환이다. 이러한 쓰레드는 단일 프로세서에서 실행되고 공통 리소스를 공유한다.( -> 쓰레드!! 리소스 공유!! 멀티 쓰레드!!!?)

Thread 의 생명 주기

쓰레드는 쓰레드가 생성, 시작, 실행 그리고 결과적으로 죽는 것과 같은 수많은 단계를 경험한다. 쓰레드의 생명주기는 아래와 같다.

New : 쓰레드는 이 단계에서 생명주기가 시작한다. 쓰레드는 프로그램이 시작될때 까지 새로운 상태로 남아있다.

Runnable : 프로세서는 실행을 위해 시간을 쓰레드에게 줄 때, 쓰레드는 실행 상태라고 한다.

Waiting : 쓰레드는 또 다른 쓰레드가 작업을 수행할 때까지 기다릴 때, 쓰레드는 대기 상태라고 말한다. 이는 실행을 계속하기 위해 실행 중인 쓰레드로부터 신호를 수신하는 즉시, 실행 상태로 돌아간다.

Blocked : 쓰레드가 일시 중단 또는 휴면 상태로 인해 실행 중 상태 또는 실행 가능한 상태로 들어가는 것을 막을 때, 이 쓰레드는 차단된 상태로 간주한다.

Dead : 쓰레드는 작업의 실행을 완료했을 때 이 상태로 들어간다. 이 쓰레드는 이 상태에서 종료된다.

Thread 만들기

쓰레드는 두 가지 매커니즘을 사용해 만들 수 있다.

  1. Runnable interface 구현
  2. Thread class 확장

Runnable interface 구현

이 매커니즘을 위해, 우리는 Runnable interface를 구현하는 클래스를 만들 필요가 있다. 그 이후로, 클래스는 Runnable interface를 구현할 것이므로, 인터페이스에 선언된 모든 메소드들을 정의해야만 한다. 운 좋게도, Runnable 인터페이스는 오직 한 가지 메소드만을 가지며, 그것은 run() 함수이다. 그래서 우리는 클래스에 run함수를 정의해야 한다.

이게 끝나면, 클래스의 인스턴스화를 할 필요가 있고, Thread의 객체를 만들고 Thread 클래스의 생성자에 대한 클래스 객체 참조를 전달하고, Thread 클래스의 start() 함수를 호출하기만 하면 된다. start 함수는 생성자를 통해 참조를 받은 클래스의 run() 함수를 로드할 것이다. 따라서, 클래스의 run 함수는 별도의 쓰레드에서 실행될 것이다.

class MyRunnable: Runnable {
override fun run() {
try {
// Displaying the running thread
println("Thread ${Thread.currentThread().id} is running...")
} catch (e: Exception) {
println(e.message) // Exception handling
}
}
}
// 테스트
class MultiThreading {
@Test
fun testRunnable() {
for (i in 1..5) {
// Instantiating Thread
val thread = Thread(MyRunnable())
// loads the run method of MyThread class
thread.start()
}
}
}

위 코드를 여러 번 컴파일하고 실행하면 실행할 때마다 다른 출력을 볼 수 있다. 하나의 답의 예는 아래와 같다.

// 결과
Thread 13 is running...
Thread 11 is running...
Thread 15 is running...
Thread 14 is running...
Thread 12 is running...
Process finished with exit code 0

Thread class 확장

이 매커니즘을 위해, java.lang.Thread 클래스를 확장하는 클래스를 만들 필요가 있다. Thread 클래스는 Runnable 인터페이스를 구현하고 그 후, 그 안의 run 함수를 정의했을 것이다. 그래서 이번엔, 클래스에 run 함수를 override(재정의)할 것이다.

run 함수와 함께 클래스를 설계했으면, 그것을 인스턴스화하고 start() 함수를 호출할 것이다. start 함수가 쓰여있지 않는다면, Java 규칙에 따라, Thread 클래스의 start함수는 실행되어, 우리 클래스의 run 함수를 쌓여서 결과적으로 run 함수의 어플리케이션 일부가 실행될 것이다.

class MyThread: Thread() {
// Java는 run 메소드를 그냥 가져다 쓰던데 얘는 왜 override해야하지? Java는 내부적으로 어떻게 진행이 되는건가...?
override fun run() {
try {
// Displaying the running thread
println("Thread ${currentThread().id} is running...")
} catch (e: Exception) {
println(e.message) // Exception handling
}
}
}
public class MyThreadJava extends Thread{
public void run() {
try {
System.out.println("Thread "+Thread.currentThread().getId()+" is running...");
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
class MultiThreading {
@Test
fun testThread() {
for (i in 1..5) {
// Instantiating Thread
val myThread = MyThread()
// load the run method of MyThread class
myThread.start()
}
}
}

이를 이전에 실행한 대로 컴파일하고 실행하면 실행할 때마다 무작위 출력을 다시 보게 된다. 그리고 이건 멀티쓰레딩 개념의 증명이다.

유용한 Thread 메소드

우리의 행동을 수행하기 위해 여러 쓰레드 메서드를 사용할 수 있지만, 몇 가지 가장 좋아할만한 메소드들을 아래 설명해놨다. 이러한 메서드는 static 메서드이며, 이를 호출하면 현재 실행 중인 쓰레드에서 작업이 수행된다.

  1. public static void getAllStackTrace() :
  2. public static boolean holdsLock(Object)
  3. public static Thread currenThread() : 현재 실행 중인 쓰레드의 참조를 반환한다.
  4. public static int activeCount()
  5. public static void sleep(millis) : 현재 실행 중인 쓰레드가 지정된 밀리초 동안 일시적으로 실행을 중지하도록 한다.

몇 가지 유용한 non-static 함수들

  1. public long getId() : Thread의 식별자를 반환
  2. public void start() : 이 Thread는 해당 클래스의 run 함수를 쌓음으로써 실행을 시작한다.
  3. public final void join() : 이 쓰레드가 종료될때까지 기다린다.
  4. publid void run() : Thread가 Thread 클래스를 확장하거나 runnable 인터페이스를 구현함으로써 인스턴스화되면, 그때 이 함수를 호출한다. 만약 정의되어 있다면, 어플리케이션 코드 일부분을 포함한다.
  5. publid final void setPriority(int) : 이 쓰레드의 우선순위를 설정한다. 우선순위는 1에서 10이고 1이 가장 높은 우선순위이며 10이 가장 늦은 우선순위이다.

…(중간 생략)

Thread Safety

멀티쓰레딩에서 일부 쓰레드는 동시에 실행되므로, 동일한 리소스에 접근할 수도 있다. 이 작업은 동시에 여러 쓰레드에 의해 자원을 공유하게 되는데, 그 데이터는 일관성없는 변경을 초래할 수 있다. 이건 어떤 프로젝트에서든 구현할 때 큰 문제를 만들 수 있다. 그래서 이 혼란은 Thread-safety 프로세스를 사용하여 해결된다. 이 프로세스에서, 객체에서 작업하는 쓰레드는 다른 쓰레드가 동일한 객체에서 작업하는 것을 막아, Thread-safe한 코드를 제공한다.

Synchronization

Synchronization은 Thread safety를 수행하는 방법이다. 특별한 작업을 완성하기 위한 하나의 쓰레드 만을 허락한다. critical section(중요한 분단?섹션?) 으로 알려진 코드 블럭을 만드는 수정자인 synchronized 키워드를 사용해 구현할 수 있다.

class Sync {
@Synchronized
fun sum(n: Int) {
// Creating a thread instance
val thread = Thread.currentThread()
for (i in 1..5) {
println("Processing " + thread.name + " : " + (n + i))
}
}
}
class MyThread: Thread() {
var sync = Sync()
override fun run() {
sync.sum(10)
}
}
class MultiThreading {
@Test
fun testSync() {
// Instantiating MyThread class
val myThread = MyThread()
// Instantiating thread instances
val thread1 = Thread(myThread)
val thread2 = Thread(myThread)
thread1.name = "Thread A"
thread2.name = "Thread B"
// Starting thread instance thread1 and thread2
thread1.start()
thread2.start()
}
}
}

위 코드를 실행하면 아래와 같은 결과가 나온다.

Processing Thread A : 11
Processing Thread A : 12
Processing Thread A : 13
Processing Thread A : 14
Processing Thread A : 15
Processing Thread B : 11
Processing Thread B : 12
Processing Thread B : 13
Processing Thread B : 14
Processing Thread B : 15
Process finished with exit code 0

하지만 @Synchronized를 삭제하면 아래와 같은 결과가 나온다

Processing Thread A : 11
Processing Thread B : 11
Processing Thread A : 12
Processing Thread B : 12
Processing Thread A : 13
Processing Thread B : 13
Processing Thread A : 14
Processing Thread B : 14
Processing Thread A : 15
Processing Thread B : 15

위의 코드는 아래를 참조하면 된다.

쓰레드가 무엇이며, 멀티 쓰레드일 때 어떻게 막는지, 테스트를 해볼 수 있는 방법까지 소개했다. 어떻게 동작하고 막는지에 대해까지 알아 보았고, 점점 깊게 공부를 해봐야겠다…

--

--

JCChoo
JCChoo

Written by JCChoo

“기본부터”라는 말을 좋아하지만 정작 기본이 없는… 기본을 쌓아나가려고 합니다. 안드로이드 개발자로 성장하기 위해 열심히 책상앞에 앉아 두들겨보겠습니다.