[Java] 멀티스레딩, 병행성 및 성능 최적화 - 1

나르·2024년 3월 3일
0

JAVA

목록 보기
17/18
post-thumbnail

본 강의는 글또 X 유데미 콜라보 이벤트로 지원되었습니다.

Why we need Threads?

멀티스레드가 필요한 이유

  • 응답성(Responsivceness)
  • 성능(Performance)

병렬성(concurrency) = 멀티스레드를 이용해 멀티태스킹을 하면 모든 작업이 동시에 실행되는 것 처럼 보인다.

Thread 란

컴퓨터를 켜면

  • OS가 디스크에서 메모리로 로드된다.
  • 프로그램(APP)은 디스크에 파일 형태로 저장되어 있다가, 실행되면 OS에 의해 메모리로 올라와 인스턴스(프로세스, application context)로 생성된다.
  • 프로세스 구성 : 프로세스ID(PID), 코드, heap, MainThread(stack+instruction pointer)
    • Stack = 메모리영역, 지역변수가 저장되고 함수가 실행됨
    • 명령어 포린터 = 스레드가 실행할 다음 명령어의 주소를 가지는 그냥 포인터

Context Switching

여러 프로세스가 실행되고, 프로세스는 하나 이상의 스레드를 가진다. 그리고 이 스레드들은 CPU 실행을 두고 경쟁하게 된다.

  • Stop Thread 1
  • Schedule Thread 1 out
  • Schedule Thread 2 in
  • Start Thread 2

CPU에서 실행되는 스레드는 CPU 내 레지스터, 캐시, 메모리의 커널 리소스등을 사용한다.
그래서 컨텍스트 스위칭이 일어나면 작업중이던 스레드의 리소스를 저장하고, 새 스레드의 리소스를 CPU와 메모리에 올리는 작업이 필요하다.
특히 같은 프로세스 내의 스레드들은 공유하는 리소스가 많지만, 다른 프로세스 스레드 간의 컨텍스트 스위칭은 비용이 더 크다.

Thread scheduling

다음과 같은 작업 스레드들이 있을 때, 어떤 순서로 실행해야할까?

  1. First come

이 경우 긴 요청이 먼저 도착하면 다른 스레드에는 기아현상이 발생한다.
UI 스레드는 실행시간이 짧음에도 응답성이 늦어져 사용자 경험을 저해한다.

  1. Shortest Job

이 경우 상대적으로 짧은 UI 스레드만 계속해서 실행되고 작업이 있는 긴 스레드는 영원히 실행되지 않을 수 있다.

  1. Epochs

OS는 에포크에 맞춰 시간을 적당한 크기로 나눈다.
스레드의 타임 슬라이스를 종류별로 에포크에 할당한다. (모든 스레드가 각 에포크에서 완료되진 않음)

  1. Dynamic priority

Static priority(개발자가 미리 설정) + Bonus(OS가 각 epoch마다 조절)
OS 는 실시간/인터렉티브 스레드에 우선권을 주고, 기아현상을 막기 위해 이전 에포크에서 완료되지않은 스레드도 확인한다.

Thread

스레드는 아무것도 하지 않아도 메모리와 일부 커널 리소스를 사용한다.
실행 중인 경우에는 CPU 시간과 CPU 캐시도 사용한다.

때문에 스레드가 오래 걸리거나, 작업을 끝낸 후에도 종료되지 않으면 interrupt 등을 이용해 종료 처리 해야한다.

interrupt

public static void main(String [] args) {
    Thread thread = new Thread(new BlockingTask());
    thread.start();
    thread.interrupt();
}
// use InterruptedException
private static class BlockingTask implements Runnable {
    @Override
    public void run() {
        //do things
        try {
            Thread.sleep(500000);
        } catch (InterruptedException e) {
            System.out.println("Existing blocking thread");
        }
    }
}

// use isInterrupted()
private static class BlockingTask implements Runnable {
    @Override
    public void run() {
        //do things
        if (Thread.currentThread().isInterrupted()) {
            System.out.println("Prematurely interrupted computation");
            return;
        }
        System.out.println("Existing blocking thread");
    }
}

인터럽트 신호에 명시적으로 반응하는 메서드가 없을 경우, InterruptedExecption 을 발생시켜 처리해야 한다.

    public static void main(String [] args) {
        Thread thread = new Thread(new WaitingForUserInput());
        thread.start();
        thread.interrupt();
    }
 
    private static class WaitingForUserInput implements Runnable {
        @Override
        public void run() {
            try {
                while (true) {
                    char input = (char) System.in.read();
                    if(input == 'q') {
                        return;
                    }
                }
            } catch (IOException | InterruptedException e) {
                System.out.println("An exception was caught " + e);
            };
        }
    }

System.in.read() 는 interrupt 에 의해 방해되지 않기 때문에 이건 또 종료가 안된다..

Daemon

background 에서 앱을 돌리는 것으로, 스레드의 완료 여부와 상관없이 리턴한다.

 public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new LongComputationTask(new BigInteger("200000"), new BigInteger("100000000")));

        thread.setDaemon(true);
        thread.start();
        Thread.sleep(100);
        thread.interrupt();
    }

join

Thread1과 Thread2의 실행 순서를 통제해야하는 경우 Thread.join() 을 쓸 수 있다.

// 가능은 하나 비효율적
while(thread.isFinished()) {
	// burn CPU cycles
}

// join 이용
public static void main(String[] args) throws InterruptedException {
        List<Long> inputNumbers = Arrays.asList(100000000L, 3435L, 35435L, 2324L, 4656L, 23L, 5556L);

        List<SomeThread> threads = new ArrayList<>();

        for (long inputNumber : inputNumbers) {
            threads.add(new SomeThread(inputNumber));
        }

        for (Thread thread : threads) {
            thread.setDaemon(true);
            thread.start();
        }

        for (Thread thread : threads) {
            thread.join(2000);
        }

        for (int i = 0; i < inputNumbers.size(); i++) {
            SomeThread thread = threads.get(i);
            if (thread.isFinished()) {
                System.out.println(thread.getResult());
            } else {
                System.out.println("still in progress");
            }
        }
    }
profile
💻 + ☕ = </>

0개의 댓글