한 걸음 두 걸음

자바 JAVA ] 스레드 thread 병행 프로그래밍 / 운영체제 연관 본문

Language/Java

자바 JAVA ] 스레드 thread 병행 프로그래밍 / 운영체제 연관

언제나 변함없이 2019. 11. 22. 13:54
반응형

프로세스와 스레드

운영체제를 공부하신 분들은 다 아시겠지만, 컴퓨터 업무 처리의 단위는 프로세스와 스레드가 있습니다. 프로세스는 실행중인 프로그램 작업단위로 독자적으로 자신만의 데이터와 프로세스ID 등을 갖고 실행됩니다. 이는 프로그램으로도 불리지만, 사실 하나의 프로그램은 여러 개의 프로세스를 가질 수도 있습니다.(예, 카카오톡 프로그램은 메세지송수신과 프로필뮤직 플레이를 동시에 제공합니다.) 이 때, 메세지 송수신과 뮤직 플레이 프로세스가 각각 있으며 두 사이의 메모리는 분리되어있습니다.

스레드는 프로세스 안에 한 개 이상 존재합니다. 프로세스 내에서 스레드들은 모두 프로세스의 자원을 공유하며 효율적으로 통신합니다. 자바로 만든 프로그램은 반드시 한 개 이상의 스레드를 갖습니다. 최초로 만들어진 스레드가 바로 메인스레드입니다. 프로세스와 마찬가지로 새로운 스레드를 만들 때 자신을 fork하여 만듭니다. 단, 프로세스보다 만드는 오버헤드가 적습니다. 동기화 문제가 까다롭긴 하지만, 한 번에 하나의 작업(마우스 쓸 때는 키보드를 못쓴다라던가..)만 해야하는 상황을 피하기 위해서는 스레드를 사용하는 것이 좋습니다.

그렇다면 이들을 어떻게 병렬적으로 처리할 수 있을까요?

병행 테스크 Concurrent Task

멀티테스킹이란 여러 개의 앱을 동시에 실행할 수 있도록 작업단위를 나눠 빠른 속도로 여러 앱의 작업을 순차적으로 처리해주는 것을 말합니다. CPU는 하나일 뿐이지만 여러 프로그램의 작은 작업단위를 꾸준히 처리해줌으로써 사용자에게는 여러 앱이 동시에 실행되는 듯한 느낌을 줍니다.

멀티스레딩이란 하나의 앱 안에서 다수개의 기능을 할 수 있도록 여러 개의 스레드를 이용하는 것을 말합니다. 그렇다면 이들을 이용해서 어떻게 병행프로그래밍을 할 수 있을까요?

1. Runnable인터페이스

자바는 Thread클래스를 지원합니다.

Thread t = new Thread();
t.start(); //스레드 객체를 활성화합니다.

스레드는 위처럼 간단하게 사용할 수 있지만, 위에서는 작업 내용이 기술되지 않았으므로, Thread클래스에 구현된 run()함수를 통해 작업 내용을 기술해주어야합니다. run함수는 Runnable인터페이스로부터 온 것으로, 재정의가 필요합니다. (Thread클래스는 Runnable인터페이스를 implements합니다.)

runnable인터페이스를 구현한 것을 task라 하고, 이를 스레드에서 실행시키는 방식으로 병행 테스크를 처리합니다.

public interface Runnable{
    void run(); //자바런타임시스템에 의해 호출됨
}

Thread클래스는 java.lang패키지에 있으므로 따로 import해줄 필요가 없습니다.

class PrintThread extends Thread{
    @Override
    public void run(){
        System.out.println("작업할 내용");
    }
}

이처럼 PrintThread클래스에 Thread를 상속받아 구현 후

public class Main {
    public static void main(String[] args) {
        Thread t = new PrintThread();
        t.start();
    }
}

해주면 System.out.println("작업할 내용");이 실행됩니다.

Runnable인터페이스를 상속하여 구현하려면

class PrintThread implements Runnable{
    @Override
    public void run(){
        System.out.println("작업할 내용");
    }
}

인터페이스를 implements한 후,

public class Main {
    public static void main(String[] args) {
        Thread t = new Thread(new PrintThread()); //Runnable객체가 Thread객체가 되도록 만들었습니다.
        t.start();
    }
}

로 실행시키면 됩니다.


보다 깔끔하게 정리!

자바 스레드를 생성하는 방법 두가지

  1. Runnable 인터페이스 사용
    커스텀 스레드 클래스

    class MyThread implements Runnable{
     @Override
     public void run(){
         System.out.println("작업할 내용");
     }
    }

    사용하기

    MyThread mt = new MyThread();
    Thread t = new Thread(t);
    t.start();
  2. Thread 상속
    커스텀 스레드 클래스

    class MyThread extends Thread{
     @Override
     public void run(){
         System.out.println("작업할 내용");
     }
    }

    사용하기

    MyThread mt = new MyThread();
    mt.start();

여러 개의 스레드 만들기

(편집중)


이를 활용하면 예시로, 안드로이드 앱에서 서비스를 실행할 때 앱이 포그라운드 상태에 있을 경우, 매 10초마다 GPS값을 검사하고 상태를 업데이트 하는 등의 작업을 해줄 수 있습니다.
sleep(long millis) 스레드 수면상태를 사용하여 시간을 조절하기도 하나, 이 경우 스레드가 아예 동작하지 않게 되므로 비동기처리를 하는 것이 좋을 때가 많습니다.

스레드가 인터럽트에 반응하도록 만들고싶다면, InterruptedException 예외 처리를 해주어야합니다 (sleep()사용 시 발생할 수 있습니다)

스레드는 같은 자원을 공유하기 때문에 동시에 자원에 접근하여 문제가 발생할 수 있습니다. 이를 해결하기 위해 동기화를 사용합니다. 자원에 동시에 접근하는 것을 막고 한 번에 하나의 스레드만 사용할 수 있도록 합니다. 특히, 중요한 자원을 보호하기 위해 임계영역을 설정하고 임계영역은 스레드간섭이 일어나지 않도록 엄격히 관리합니다.

안드로이드스튜디오의 Bluetooth Chat 예제에서 데이터송수신 값이 순서대로 오지 않고, 마구잡이로 찍히는 현상을 발견할 수 있는데 데이터를 읽어와 저장해놓은 변수에 스레드가 임의로 접근하여 메모리를 수정하는 바람에 제대로 된 데이터를 받지 못합니다. 이러한 것이 바로 스레드 간섭입니다. 이를 해결하기 위해 자바에서는 동기화된 메소드를 제공합니다. synchronized 키워드를 메소드에 붙이면, 스레드가 임계영역 안에서 실행되도록 만듭니다.
하지만 이 방법은 deadlock, starvation문제를 일으킵니다. 우선순위가 낮은 스레드는 계속 임계영역에 접근하지 못해 무한정 대기하는 등 아사상태가 됩니다. 이 외에도 polling, wait, motify 등이 있으니 동기화를 사용할 분들은 참고해주세요.


Executor클래스 : 태스크와 여러 개의 스레드를 매칭해줍니다.

Executor e = Executors.newCachedTreadPool();//최적의 스레드 풀을 만들어주는 함수 
Runnable task = ... ;
e.exeute(task);

놀고있거나 사용하지 않는 스레드를 활용하거나 새로운 스레드를 만들어서 테스크 처리를 합니다.

+ 람다식 예시


public class Main {
    public static void main(String[] args) {
    //태스크 1개 정의
        Runnable r = ()->{
            System.out.println("작업할 내용");
        }
    }
}

ExecutorService service = Excutors.newCachedThreadPool();
service.execute(r);
service.shutdown();

2. Callable 인터페이스

Runnable은 결과를 반환하지 않고 자체로 실행되는데 Callable인터페이스는 call메소드를 통해 결과를 반환합니다.

public interface Callable<V> {
    V call() throws Exception;
}

이는 Executor의 서브인터페이스인 ExcecutorService클래스를 사용하여 병행프로그래밍을 합니다.

ExecutorService e = Executors.newCachedTreadPool();
Callable<V> task = ...;
Future<V> result = e.submit(task); //비동기이기 때문에 후에 결과값이 나올 때 저장됩니다. 

3. 병렬처리 Parallel Operation

Parallel Operation은 하나의 작업을 분할해서 각 코어에 할당하여 처리합니다. 이는 작업처리 시간을 줄이기 위해 사용합니다.
동시성(하나의 코어에서 멀티 스레드가 빠르게 번갈아가면서 수행하여 동시에 실행되는 것처럼 보인다.)
병렬성(다수의 코어에서 작업을 나누어 실행합니다. 단, 데이터 및 자원을 분리해서(포크) 보낸 후 합치는 과정(조인)이 필요합니다.)

4. 포크 조인 프레임워크

포크조인 프레임워크는 병렬 스트림 요소들을 병렬처리하기위한 프레임워크입니다.
Core가 4개이면 데이터를 4개로 나누어서 각 코어에 작업을 맡기고 후에 작업결과를 합쳐서 최종결과를 반환합니다.
포크할 때는 ExecutorService을 상속받는 ForkJoinPool을 사용하여 내부적으로 서브 요소를 나누고 작업스레드를 관리합니다.

5. 병렬 스트림

병렬 스트림을 이용하는 경우 개발자는 명령만 내릴 뿐 포크조인 프레임워크가 백그라운드에서 알아서 처리하여 굉장이 간편합니다.
parallelStream() 메소드를 배웠었죠 : )(https://onepinetwopine.tistory.com/496)이처럼%EC%9D%B4%EC%B2%98%EB%9F%BC) Stream에 parallel함수만 추가해주면 바로 병렬스트림 처리가 됩니다.

모든 테스크를 병렬로 처리하는 것이 빠른 것은 아니므로 적절한 테스크에만 적용시켜야합니다.

반응형