자바를 자바 23 (processes and thread, multithreading)

Background: processes and threads

컴퓨터를 키고 프로그램을 실행시키면 운영체제가 프로세스를 만들어준다. 이 프로세스가 프로그램을 돌리기 위한 자원을 분배하게 된다. 이때 중요한 것이 메모리로 프로그램 별로 메모리를 할당해주는 과정을 운영체제가 담당하게 된다.

그래서 작업관리자를 실행시키면 메모리를 프로그램별로 얼마나 할당해 주었는지 확인할 수 있다.

스레드 : 프로그램을 실행시키는 가장 작은 단위로 스케쥴러에 의해 독립적으로 관리된다. 각 프로세스 별로 적어도 하나의 스레드에서 여러개의 스레드를 가질 수 있다. 그래서 프로세스가 두 개이상의 스레드를 가지면 mulit-threaded process라고 부른다.

그림을 보다 싶이 하나의 프로세스를 보면 하나의 스레드에서 수십개의 스레드가 존재할 수 있다. 그리고 리소스(메모리)도 함께 프로세스 안에 들어가 있게 된다.

그래서 프로세스가 여러개인것과 멑리 스레드가 프로세스에 존재하는 것은 다른 개념이다.

Background: multitasking vs. multithreading

  • Multitasking
    OS는 여러개의 프로세스를 하번에 돌린다. : Concurrently
    현대의 OS는 multitasking을 지원한다.

  • Multithreading
    한 프로세스에서 여러 스레드를 사용해서 걔네들을 동시에 돌리는 것을 이야기 한다.


그래서 왼쪽 그림을 보면 멀티태스킹이고 오른쪽 그림은 멀티스레딩을 이야기 한다.

Background: multithreading

  • 장점
    • 리소스를 사용하는데 효율적이다. 한 프로그램이 여러가지 일을 처리해야 할때 스레드가 많다면 이 일을 여러 스레드가 한번에 처리할 수 있다.
    • 서버가 여러개의 클라이언트에게 서비스를 제공할때 스레드가 여러개라면 여러 클라이언트에게 서비스를 제공할 수 있다.
    • 기능별로 나누어 놓는 기능 modularity이라고 부르는데 하는 일 별로 스레드를 부여해서 프로그램을 modularity하게 만들 수 있다.
  • 단점
    • Synchronization : 두개의 스레드가 한 메모리를 함께 접근하면 충돌이 일어날 수 있다.
    • Dead lock : A 스레드 C 리소스를 가지고 D 리소스를 원하고 B 스레드가 D 리소스를 가지고 C 리소스를 원할때 두 스레드 모두 자신이 원하는 리소스는 가져가지 못하는 문제로 프로그램이 멈추게 된다.
    • Inefficiency : 스레드를 만들었으나 프로그래머가 아무일도 시키지 않은 경우

Implementing threads in Java

자바에서는 thread를 class로 만들어 놓았기에 이것을 상속받거나 interface runnable을 받아오는 방법이 존재한다.

class MyThread extends Thread {
	public void run() { // overriding
		/* tasks to run on a thread */
	}
}

class MyThread implements Runnable {
	public void run() {
		/* tasks to run on a thread */
	}
}

Thread 클래스나 Runnable abstract 클래스를 받아오면 위와 같이 run이라는 함수가 생기게 된다. 바로 여기에 우리가 원하는 업무들을 작성해야 한다.

Implementing threads in Java: Example


//thread를 상속받아서 getName을 반복해서 출력해 주고 있다.
class ThreadEx1_1 extends Thread {
	public void run() {
		for(int i=0; i<5; i++) System.out.println(getName());
	}
}

//여기에서는 Thread에 있는 함수를 사용할 수 없어서 Thread 클래스로 부터 currentThread를 받아와야 한다.
class ThreadEx1_2 implements Runnable {
	public void run() {
		for(int i=0; i<5; i++) System.out.println(Thread.currentThread().getName());
	}
}

그렇다면 왜 위와같이 만들었을까? 그 이유는 바로 우리가 상속을 여러개로 부터 받을 수 없기 때문에 하나의 클래스로 상속받은 우리 클래스에 스레드를 넣고 싶으면 Runnable을 받아오는 방법밖에 없다.

그래서 이렇게 다른 클래스를 받은 각 클래스는 아래와 같이 작성하여 실행시키면 된다.

public class Lecture {
	public static void main(String[] args) {
    	//Thread를 상속 받았다면 그냥 선언하면 된다.
		ThreadEx1_1 t1 = new ThreadEx1_1();
        
        //Runnable을 상속 받았다면 우선 객체를 만든다음 스래드 객체로 넘기어 주어야 한다.
		Runnable r = new ThreadEx1_2();
		Thread t2 = new Thread(r);
        
        //최종적으로 start함수를 부르면 스레드가 시작된다.
		t1.start();
		t2.start();
	}
}


위의 출력결과를 통해서 스케쥴러가 정하는 순서가 정해져 있지 않다 고정되어 있지 않다는 것을 확인할 수 있다.

최종적으로 스레드가 스케쥴러에 올라가고 프로그램을 실행시키기 위해 스케쥴러가 어떤 스레드를 올릴지를 결정하여서 프로그램이 최종적으로 순차적으로 실행되게 된다. 이것을 보고 스케쥴러가 스레드를 dispatch한다고 부른다.

Threads: start and run

왜 run에 정의하고 start를 부를까? 이것은 바로 call stack 때문이다.

이제 call stack이 위와 같이 구성된다. 만약 프로그램이 실행되면 main이 먼저 stack에 들어가고 run을 호출하면 run이 스택에 들어가서 run이 실행되고 main이 실행될 것이다. 그래서 call stack의 top이 현재 실행되고 있는 프로그램이다.

이와 반대로 start를 호출하면 아래와 같이 된다.

start가 하는 일은 call stack하나 더 만든 다음에 새롭게 만들어진 스택에 run을 삽입하고 멀티 스레드에서 실행이 된다. 위에서 보았던 그림과 다른점은 위의 그림은 최종적으로 Single Thread형태로 프로그램이 작동하지만 start릏 호출하게 될경우 우리가 원했던 multi thread기능을 사용하게 된다.

그래서 우리는 multi core CPU를 사용하기 때문에 멀티 스레드를 사용할 수 있게 되는 거이다.

이때 스레드 두개가 concurrently하게 돌고 있을 때 어떤 스레드가 먼저 끝날지를 예측할 수 없다. 만약 main이 먼저 끝나게 된다면 call stack이 반환되게 되는데 이와 다르게 run이 들어가 있는 call stack은 아직 운영중이다. 이런 경우에는 모든 thread가 업무를 마치었을때 프로그램도 함께 종료되게 된다.

Threads: Example 2

public class Lecture {
	public static void main(String args[]) throws Exception {
		ThreadEx2_1 t1 = new ThreadEx2_1();
		t1.start();
	}
}

class ThreadEx2_1 extends Thread {
	public void run() {
		throwException();
	}
    
    //일부러 Exception을 발생시키어서 call stack이 어떻게 동작하는지 확인하는 부분이다.
	public void throwException() {
		try {
			throw new Exception();
		} catch(Exception e) {
        //여기에서 call stack을 확인할 수 있다.
			e.printStackTrace();
		}
	}
}

public class Lecture {
	public static void main(String args[]) throws Exception {
		ThreadEx3_1 t1 = new ThreadEx3_1();
		t1.run();
	}
}

class ThreadEx3_1 extends Thread {
	public void run() {
		throwException();
	}
    
	public void throwException() {
		try {
			throw new Exception();
		} catch(Exception e) {
			e.printStackTrace();
		}
	}
}


출력결과를 보니 throwException이 가장 위에 있고 run이 바닦에 있는 것을 확인할 수 있다. 그런데 왜 main이 보이지 않을까? 이것은 Thread를 사용하면 call stack이 서로 다르기 때문에 보이지 않는 것이다.


반대로 run을 호출하니 위와 같이 구성되는 것을 확인할 수 있다.

Single thread vs. multiple threads: experiment

//아래와 같이 같은 String을 출력하는 프로그램의 시간을 측정해 보았다.
public class Lecture {
	public static void main(String[] args) {
		long startTime = System.currentTimeMillis();
        
		for(int i=0; i<300; i++) {
			System.out.printf("%s", new String("-"));
		}
        
		System.out.print("elapsed time: " + (System.currentTimeMillis() - startTime));
        
		for(int i=0; i<300; i++) {
			System.out.printf("%s", new String("|"));
		}
        
		System.out.print("elapsed time: " + (System.currentTimeMillis() - startTime));
	}
}

// 위와 유사하지만 스레드를 사용해서 같은 String을 여러번 찍도로 하였다.
public class Lecture {
	static long startTime = 0;
	public static void main(String[] args) {
    	//우선 스레드를 만들고 작동시킨다.
		ThreadEx5_1 th1 = new ThreadEx5_1();
		th1.start();
        
		startTime = System.currentTimeMillis();

		//main에서도 어떤일을 실행시키었다.
		for(int i=0; i<300; i++) System.out.printf("%s", new String("-"));
		System.out.print("elapsed time 1: " + (System.currentTimeMillis() - Lecture.startTime));
	}
}

// 이 부분이 concurrently 하게 작동한다.
class ThreadEx5_1 extends Thread {
	public void run() {
		for(int i=0; i<300; i++) System.out.printf("%s", new String("|"));
		System.out.print("elapsed time 2: " + (System.currentTimeMillis() - Lecture.startTime));
	}
}

concurrent와 simultaneous가 존재한다. CPU가 하나밖에 없으면 스레드가 스케쥴러에 따라서 한번에 하나의 스레드만 CPU에 올라가서 작동하는 거지 두개가 동시에 실시간으로 작동하는 것이 아니다. 그렇기 때문에 simultaneous하게 작동하는 것이 아닌 concurrent하게 CPU에 오르고 내리고를 반복하는 것이다.


Single Thread로 작동했을때의 시간측정인데 어째서인지 첫번째 작업이 더 오래걸리었다. 이것은 스케쥴러에 이미 있던 작업들로 인하여 첫번째 작업이 수행되기까지 시간이 걸린다. 또하나 확인할 수 있는 것은 이들이 연속적으로 이루어졌다는 점이다.


반면 multi Thread를 사용했을때는 측정된 시간이 유사한 것을 확인할 수 있다.

Single thread vs. multiple threads

Single thread라면 두개의 일을 연쇄적으로 처리한다.

하지만 A와 B가 서로 다른 스레드에서 작동한다면 아래와 같은 작업을 보여주게 된다.

왼쪽은 single core인 경우로 스케쥴러에 의해 A와 B가 concurrent하게 작동한다. 오른쪽은 multi core인경우에는 실재로 동시에 동작하게 된다.

Benefits of multithreading

public class Lecture {
	public static void main(String[] args) throws Exception {
    // java에서 Dialog 박스가 나와서 유저에게 입력을 받게 된다.
		String input = JOptionPane.showInputDialog("Enter any string.");
		System.out.println("You have entered: " + input);
        
        // sleep은 1000 = 1초로 1초동안 스레드가 잠시 멈추게 된다.
		for(int i=10; i>0; i--) {
			System.out.println(i);
			try {
				Thread.sleep(1000);
			} catch(Exception e) { /* do nothing */ }
		}
	}
}

위와 다르게 스레드를 사용하여서 제작하면 어떻게 될까? 바로 아래쪽이 스레드를 활용한 코드이다.

public class Lecture {
	public static void main(String[] args) throws Exception {
		ThreadEx7_1 th1 = new ThreadEx7_1();
		th1.start();
		String input = JOptionPane.showInputDialog("Enter any string.");
		System.out.println("You have entered: " + input);
	}
}

class ThreadEx7_1 extends Thread {
	public void run() {
		for(int i=10; i>0; i--) {
			System.out.println(i);
			try {
				sleep(1000);
			} catch(Exception e) { }
		}
	}
}


결국 Single-threaded에서는 위와 같이 A가 끝날때까지 기다리게 된다.


이와 다르게 multi-threaded라면 위와 같이 A와 B가 돌아가면서 작업을 하게 된다. 결과적으로 두 작업이 함께 작동하는 장점이 생기게 된다.

좋은 웹페이지 즐겨찾기