Virtual Threads 알아보기(2)

이월(0216tw)·2024년 5월 14일

가상스레드를 생성하고 실행해보자

가상스레드를 생성하는 방법은 2가지가 있다.

(1) Thread 클래스와 Thread.Builder 인터페이스를 사용하는 방법
(2) Executor.newVirtualThreadPerTaskExecutor 메소드를 사용하는 법

아직 공식적으로 지원은 하지 않고 준비중이라고 합니다 ㅠㅠ
그냥 레퍼런스 해석만 하는 걸로 하겠습니다.

(1) 의 방법

Thread 클래스와 Thread.Builder 인터페이스를 사용하는 방법

<코드>

Thread thread = Thread.ofVirtual().start(() -> System.out.println("Hello"));

가상 스레드를 만들기 위해 Thread.Builder의 인스턴스를 생성하는 메서드 Thread.ofVirtual()을 생성한다.

Thread.Builder 인터페이스는 스레드 이름(name) 과 같은 일반 Thread의 프로퍼티와 함께 구현이 가능하다.

<name 속성을 추가해본 가상스레드>

Thread.Builder builder = Thread.ofVirtual().name("MyThread"); //기존 스레드처럼 이름속성 사용 가능

Runnable task = () -> {
    System.out.println("Running thread");
};
Thread t = builder.start(task); //신기하게 start에 인자로 Runnable 을 준다.  
                                //기존 Thread는 생성자로 Runnable 객체를 부여했는데

System.out.println("Thread t name: " + t.getName()); 

< 두 개의 가상 스레드 만들기 >


Thread.Builder builder = Thread.ofVirtual().name("worker-", 0);

Runnable task = () -> {
    System.out.println("Thread ID: " + Thread.currentThread().threadId());
};

Thread t1 = builder.start(task);  //위에서 Thread ID 랜덤값 출력 
t1.join();
System.out.println(t1.getName() + " terminated"); //worker-0 terminated 

Thread t2 = builder.start(task); //위에서 Thread ID 랜덤값 출력 
t2.join();  
System.out.println(t2.getName() + " terminated"); //worker-1 terminated

참고로 name의 인자는 prefix 와 start이다.
즉 생성되는 가상 스레드의 접두사와 시작할 숫자를 의미하며 여기서는 worker-0 , worker-1 .. 같이 생성된다.

(2) 의 방법

Executor.newVirtualThreadPerTaskExecutor 메소드를 사용하는 법

Executor는 스레드 관리와 생성을 어플리케이션에서 분리해서 관리할 수 있다.
(예전에 테스트를 할 때 fixedThreadPool 처럼 스레드 개수를 한정하는 등, 스레드 관련 처리를 전체적인 앱에서 분리해서 쓸 수 있다는 의미같다)

<코드>

try (ExecutorService myExecutor = Executors.newVirtualThreadPerTaskExecutor()) {
    Future<?> future = myExecutor.submit(() -> System.out.println("Running thread"));
    future.get();
    System.out.println("Task completed");
    // ...

코드 설명
(1) Executors.newVirtualThreadPerTaskExecutor() 메서드로 ExecutorService를 생성했다.
(2) ExecutorService의 submit 메서드는 입력받은 Runnable을 실행시킨다. 리턴은 Future객체
(3) Future객체는 get() 을 이용해서 가상스레드가 완료되기를 기다린다. 왜냐면 get()을 통해 가상스레드에서 리턴하려는 값을 받을 수 있기 때문이다. 👉예시코드-사용자정보가져오기
(4) 위에서는 따로 get() 한 데이터를 처리하지 않고 가상메서드 완료를 기다리는 용도이므로
마지막에 Task Completed가 실행되고 종료된다. ( future -> Main 쓰레드 순서로 종료)


클라이언트-서버 멀티스레드 예시 코드 분석

이 예시는 EchoServer , EchoClient 두개의 클래스로 구성이 되어있다.
EchoServer는 각 연결마다 포트 생성 및 새로운 가상스레드를 생성한다.
EchoClient는 위 서버에 연결해서 메세지를 보내는 프로그램이다.

EchoClient는 EchoServer와 소켓을 이용해 커넥션을 획득하고,
C->S 에게 메세지를 보내면 S->C에게 다시 메세지를 반환해주는 간단한 구조이다.

서버는 각 클라이언트 요청마다 가상의 스레드를 통해 동시에 서비스를 할 수 있다

EchoServer코드

public class EchoServer {
    public static void main(String[] args) throws IOException {
         
         //전달된 인수 개수가 1이 아니면 사용법을 리턴하고 종료 (포트 입력하세요 등) 
        if (args.length != 1) {
            System.err.println("Usage: java EchoServer <port>");
            System.exit(1);
        }
        int portNumber = Integer.parseInt(args[0]);

	  try (
            ServerSocket serverSocket =
                new ServerSocket(Integer.parseInt(args[0])); //해당 포트로 소켓 생성
        ) {                
            while (true) {
                Socket clientSocket = serverSocket.accept(); //클라이언트 요청 대기
             
               //가상 스레드 시작! 
               Thread.ofVirtual().start(() -> {
                    try (
                        PrintWriter out =
                            new PrintWriter(clientSocket.getOutputStream(), true);
                        BufferedReader in = new BufferedReader(
                            new InputStreamReader(clientSocket.getInputStream()));
                    ) {
                        String inputLine;
                        while ((inputLine = in.readLine()) != null) {
                            System.out.println(inputLine); //서버 측 콘솔에 출력
                            out.println(inputLine); //클라이언트에게 보낼 메세지 세팅
                        }
                    
                    } catch (IOException e) { 
                        e.printStackTrace();
                    }
                });
            }
        } catch (IOException e) {
            System.out.println("Exception caught when trying to listen on port "
                + portNumber + " or listening for a connection");
            System.out.println(e.getMessage());
        }
    }
}

echoClient코드

public class EchoClient {
    public static void main(String[] args) throws IOException {
    
        if (args.length != 2) {
            System.err.println(
                "Usage: java EchoClient <hostname> <port>");
            System.exit(1);
        }
        String hostName = args[0];
        int portNumber = Integer.parseInt(args[1]);
        
        try (
            Socket echoSocket = new Socket(hostName, portNumber); //서버 연결
            
            PrintWriter out =
                new PrintWriter(echoSocket.getOutputStream(), true);
            BufferedReader in =
                new BufferedReader(
                    new InputStreamReader(echoSocket.getInputStream()));
        ) {
            BufferedReader stdIn =
                new BufferedReader(
                    new InputStreamReader(System.in));
            String userInput;
            while ((userInput = stdIn.readLine()) != null) {
                out.println(userInput); //서버로 메세지 전달 
                System.out.println("echo: " + in.readLine()); //서버로부터 오는 메세지 콘솔 출력
                if (userInput.equals("bye")) break;
            }
        } catch (UnknownHostException e) {
            System.err.println("Don't know about host " + hostName);
            System.exit(1);
        } catch (IOException e) {
            System.err.println("Couldn't get I/O for the connection to " +
                hostName);
            System.exit(1);
        } 
    }
}

뭔가 코드가 길어보이지만 결론을 말하면
Server가 다수의 Client에게 서비스하기 위해 가상의 쓰레드를 활용할 수 있고,
특히 I/O 작업처럼 대기가 많이 발생하는 환경에서 가상의 쓰레드는 보다 효율적으로
자원을 사용하며 많은 Client에게 서비스를 할 수 있게 된다.

profile
#SQLD강사 #AI개발 #AI강사 #개발자 개발도 하고 강의도 하지만 고민을 제일 많이 합니다

0개의 댓글