[운영체제] 7. 쓰레드(Thread)의 이해

2021. 12. 29. 17:10OperatingSystem

이전글

https://yonghwankim-dev.tistory.com/200

 

[운영체제] 6. 프로세스(Process)간 통신의 실제

이전글 https://yonghwankim-dev.tistory.com/196 [운영체제] 5. 프로세스(Process)간 통신 이전글 https://yonghwankim-dev.tistory.com/192 [운영체제] 4. 프로세스(Process)의 생성 이전글 https://yonghwankim..

yonghwankim-dev.tistory.com

 

개요

이전글에서는 프로세스간 통신을 하기 위해서 IPC 시스템을 구현하기 위한 대표적인 두가지 방법인 POSIX Shared memory 방법과 Pipe를 활용한 방법을 통하여 구현하였습니다. 이번 글에서는 쓰레드(Thread)의 개념과 멀티코어 프로그래밍(Multicore Programming)의 개념에 대해서 소개하겠습니다.

 

목차

1. 쓰레드(Thread)의 개념

2. Java Thread API

3. 멀티코어 프로그래밍(Multicore Programming)

 

1. 쓰레드(Thread)의 개념

지금까지 프로세스가 단일 제어 쓰레드로 실행되는 프로그램이라고 가정하였습니다. 그러나 한개의 프로세스에는 여러개의 제어 쓰레드가 포함할 수 있습니다.

Thread란 무엇인가?

  • 경량형 프로세스(lightweight process)
  • CPU의 최소 점유 단위
  • Thread에는 thread ID, program counter, register set, stack을 가지고 있음

위 그림에서 왼쪽은 싱글 쓰레드를 가진 프로세스이고 오른쪽 그림은 3개의 쓰레드를 가진 하나의 프로세스입니다. 3개의 쓰레드는 code, data, files 자원을 공유합니다. 왼쪽의 프로세스가 여러개 존재하면 멀티프로세싱(multiprocessing)이고 오른쪽의 쓰레드가 여러개 있으면 멀티쓰레딩(multithreading)이라고 합니다.

 

멀티쓰레딩 프로그래밍의 장점

  • 반응성(Responsiveness) : 프로세스의 일부가 block되어도 계속 실행이 가능합니다.
  • 자원 공유(Resource Sharing) : 프로세스의 쓰레드들은 공유 메모리(shared-memory) 또는 메시지 전달(message-passing)보다 쉽게 자원 공유가 가능합니다.
  • 경제성(Economy) : 쓰레드 교환이 Context Switching보다 낮은 오버헤드를 가지고 프로세스 생성보다 생성비용이 낮습니다.
  • 확장성(Scalability) : 프로세스는 다중 프로세서 구조를 활용할 수 있습니다.

2. JAVA Thread API

자바는 쓰레드를 제공하기 위해 표준 API를 제공합니다. 쓰레드를 사용하기 위한 방법에는 대표적으로 3가지가 존재합니다.

  1. Thread 클래스 상속
    • Thread 클래스를 상속받기 위한 클래스를 생성하고 Thread 클래스 상속받음
    • run() 메서드를 오버라이드 구현
  2. Runnable 인터페이스 구현
    • Runnable 인터페이스를 구현할 클래스를 생성하고 Runnable 인터페이스를 implements함
    • run() 메서드를 오버라이드 구현
  3. Lambda 표현식 사용하기
    • 새로운 클래스를 생성하지 않음
    • Runnable 인터페이스의 람다 표현식을 사용함

2.1 Thread 클래스 상속

// 쓰레드 구현 방법1 : Thread 클래스 상속
public class MyThread1 extends Thread{

	@Override
	public void run() {
		try 
		{
			while(true)
			{
				System.out.println("Hello, Thread!");
				Thread.sleep(500);
			}
		}
		catch(InterruptedException ie) {
			System.out.println("I'm interruptedException");
		}
	}
	
	public static void main(String[] args)
	{
		MyThread1 thread = new MyThread1();
		thread.start();
		System.out.println("Hello, my child");
	}
	
}
Hello, my child!
Hello, Thread!
...
...
Hello, Thread!
...
...

 

2.2 Runnable 인터페이스 구현

// 쓰레드 구현 방법2 : Runnable 인터페이스 구현
public class MyThread2 implements Runnable{

	@Override
	public void run() {
		try 
		{
			while(true)
			{
				System.out.println("Hello, Runnable!");
				Thread.sleep(500);
			}
		}
		catch(InterruptedException ie) {
			System.out.println("I'm interruptedException");
		}
	}
	
	public static void main(String[] args)
	{
		Thread thread = new Thread(new MyThread2());
		thread.start();
		System.out.println("Hello, My Runnable Child");
	}
	
}
Hello, My Runnable Child
Hello, Runnable!
Hello, Runnable!
Hello, Runnable!
Hello, Runnable!
Hello, Runnable!
...

 

2.3 Runnable 람다 표현식 사용하기

// 쓰레드 구현 방법3 : Runnable 람다 포현식 사용하기
public class MyThread3{
	
	public static void main(String[] args)
	{
		Runnable task = ()->{
			try 
			{
				while(true) 
				{
					System.out.println("Hello, Lambda Runnable!");
					Thread.sleep(500);
				}
			}
			catch(InterruptedException ie)
			{
				System.out.println("I'm interrupted");
			}
		};
		
		Thread thread = new Thread(task);
		thread.start();
		System.out.println("Hello, My Lambda Child!");
	}
	
}
Hello, My Lambda Child!
Hello, Lambda Runnable!
Hello, Lambda Runnable!
Hello, Lambda Runnable!
Hello, Lambda Runnable!
Hello, Lambda Runnable!
Hello, Lambda Runnable!

 

2.4 부모 쓰레드의 대기 : join

자식 쓰레드가 종료될때까지 대기했다가 부모 쓰레드의 나머지 명령어를 수행합니다.

// 부모 쓰레드의 대기 : join
public class MyThread4{
	
	public static void main(String[] args)
	{
		Runnable task = ()->{
			for(int i=0;i <5; i++)
			{
				System.out.println("Hello, Lambda Runnable!");
			}
		};
		
		Thread thread = new Thread(task);
		thread.start();
		try
		{
			thread.join();	// 쓰레드가 종료 될 때가지 wait
		}
		catch(InterruptedException ie)
		{
			System.out.println("Parent thread is interruped");
		}
		System.out.println("Hello, My Joined Child!");
	}
	
}
Hello, Lambda Runnable!
Hello, Lambda Runnable!
Hello, Lambda Runnable!
Hello, Lambda Runnable!
Hello, Lambda Runnable!
Hello, My Joined Child!

 

2.5 쓰레드의 종료

쓰레드를 명시적으로 종료시킵니다. 그러나 사용하지 않는것을 권고합니다.

// 쓰레드의 종료 : interrupt
public class MyThread5{
	
	public static void main(String[] args) throws InterruptedException
	{
		Runnable task = ()->{
			try
			{
				while(true)
				{
					System.out.println("Hello, Lambda Runnable!");
					Thread.sleep(100);
				}
			}
			catch(InterruptedException ie)
			{
				System.out.println("I'm Interruped");
			}
		};
		
		Thread thread = new Thread(task);
		thread.start();
		Thread.sleep(500);
		thread.interrupt();
		System.out.println("Hello, My Interruped Child!");
	}	
}
Hello, Lambda Runnable!
Hello, Lambda Runnable!
Hello, Lambda Runnable!
Hello, Lambda Runnable!
Hello, Lambda Runnable!
Hello, My Interruped Child!
I'm Interruped

 

3. 멀티코어 프로그래밍(Multicore Programming)

멀티코어 시스템(Multicore System)안의 멀티쓰레딩(Multithreading)

  • 다중 코어를 보다 효율적으로 사용하여 동시성(concurrency)를 개선할 수 있습니다.
  • 4개의 쓰레드가 있는 애플리케이션을 가정
    • single-core : 쓰레드들은 시간이 지남에 따라 인터리브(interleaved)하여 쓰레드들 중간에 공백의 시간이 존재할 것입니다.
    • multiple-cores : 여러개의 쓰레드들이 병렬적으로 수행이 가능하게 함

아래의 그림은 single-core 시스템에서 동시에 실행하는 그림입니다.

아래의 그림은 multiple-cores 시스템에서 병렬적으로 실행하는 그림입니다.

 

멀티코어 시스템(Multicore systems)에서 프로그래밍 도전

  • 테스크를 식별하는 것(Identifying Tasks) : 테스크를 나누어서 수행할 영역을 찾을 수 있는 것
  • 균형(Balance) : 동일한 가치의 작업을 수행할 수 있도록 보장하는 것
  • 데이터 분할(Data Splitting) : 데이터 또한 여러개의 코어가 놀지 않도록 잘 나누어야 하는 것
  • 데이터 의존성(Data Dependency) : 데이터 의존성을 수용하도록 테스크의 실행이  동기화되는 것을 보장하는 것
  • 테스트와 디버깅(Testing and debugging) : 싱글 쓰레드보다 더 어려움

병렬성의 타입들

병렬성의 타입들에는 첫번째로 데이터를 동등하게 쪼개서 코어가 나누어서 수행하는 것과 테스크를 쪼개서 코어가 나누어서 수행하는 타입이 존재합니다. 그러나 요즘은 분산 처리 시스템으로 인하여 구분하지는 않습니다.

 

 

References

source code : https://github.com/yonghwankim-dev/OperatingSystem_Study
Operating System Concepts, 10th Ed. feat. by Silberschatz et al.
[인프런] 운영체제 공룡책 강의