본문 바로가기

JAVA/JAVA 기초

병렬 프로그래밍

반응형

병렬 프로그래밍

여러가지 일을 동시에 처리하기 위한 기법은 크게 병행(Concurrency), 병렬(Parallel) 그리고 분산(Distribute)으로 구분한다.

  • 병행은 하나의 CPU 코어에서 소프트웨어적인 기법으로 동시에 여러 작업을 교차하면서 실행하는것. (프로그램 성질)
  • 병렬은 여러 개의 코어에 작업을 배분해서 동시에 작업을 실행하는 것. (기계적인 특징)
  • 멀티코어 환경에서 병렬과 병행 작업이 동시에 일어난다.
  • 작업이 여러 코어에 배분될뿐만 아니라 하나의 코어에서 여러 작업이 병행해서 동작한다.

컨커런트 API

  • 웹 기반 개발이 일반화 되면서 멀티 스레드 프로그래밍이 강조 되었다.
  • 컨커런트 API가 멀티 스레드 모델보다 개발이 좀 더 쉽긴 하지만 멀티 스레드를 대체하기 위한 것은 아니다.
  • 데몬 프로그램을 작성하고 멀티 스레드 환경으로 동작하도록 서비스를 구성하기 위해서는 저수준 API를 사용한 스레드 프로그래밍이 필요하다.
  • 여러 데이터를 동시에 처리해서 성능을 확보하기 위해 병렬 프로그래밍을 하려 한다면, 과거에는 스레드 기반으로 작성해야 했다.
  • 지금은 컨커런트 API로 해결한다.

컨커런트 API 특징

  • 병렬 애플리케이션에서 데이터의 동기화와 정합성을 확보하기 위해 Lock 객체를 제공하며 이를 통해 잠금 기능을 사용할 수 있다.
  • 스레드를 실행하고 관리하는 고수준 API를 사용한 Executor 클래스를 제공한다.
  • Executor 인터페이스를 구현한 것으로 java.util.concurrent 패키지에 포함되어 있으며, 대량 데이터를 병렬 처리하기에 적합하다.
  • 병렬 프로그램에서 대량 데이터의 정합성을 유지한 채 사용하기 위한 컬렉션 프레임워크의 확장판인 컨커런트 컬렉션 클래스를 제공한다.
  • 원자적 변수는 동기화를 위한 synchronized 키워드를 사용을 최소화하여 성능을 확보하면서 메모리 정합성(Memory consistency) 에러를 방지하는 기능을 제공한다.
  • ThreadLocalRandom 클래스를 이용해서 멀티 스레드 환경에서 효율적인 난수를 생성하는 기능을 제공한다.

컨커런트 API의 특징은 스레드에서 데이터 정합성을 확보하고 멀티 스레드 환경에서 프로그래밍하기 위한 필요한 내용이다.

  • 패키지 구성
  • java.util.concurrent
  • java.util.concurrent.atomic
  • java.util.concurrent.locks

컨커런트 API 패키지의 인터페이스와 클래스를 6가지 분류

실행자
  • 컨커런트 API에서 작업을 실행하는 역할을 하며 인터페이스와 인터페이스를 구현한 클래스로 구성되어 있다.
  • Executor 인터페이스를 사용하면 비동기 처리, 스레드 풀, 태스크 프레임워크 등을 쉽게 구현할 수 있다.
  • 컬렉션 프레임워크에서 제공하는 선입선출방식의 데이터 처리 흐름이며, 컨커런트 API에서 추가로 제공하는 큐는 멀티 스레드 환경에서 안정성을 보장한다.
타이밍
  • 멀티 스레드 프로그래밍에서 소프트웨어의 안정성을 확보하기 위해서는 스레드를 잘 실행하고 종료해서 한정된 자원을 최대한 효율적으로 활용해야 한다.
  • 대부분의 서버 사이드 소프트웨어는 멀티 스레드에 타임아웃 기능을 제공한다.
  • 타임아웃 기능을 통해 불필요한 스레드, 좀비나 데드락 스레드를 관리하고 소프트웨어를 안정적으로 동작하도록 한다.
동기화
  • 서버 사이드 소프트웨어는 동시에 실행할 수 있는 스레드 크기를 제한하는 기능을 제공한다.
  • 스레드를 많이 생성하다보면 어느 순간에 오히려 성능이 떨어지고 처리 시간이 느려진다.
  • 이를 방지하기 위해 Semaphore 클래스를 이용하면 쉽게 구현할 수 있다.
컨커런트 컬렉션
  • 컨커런트 API 환경에서 List, Map형 데이터를 다루기 위해 제공하는 인터페이스와 클래스들이다.
  • 컬렉션 데이터의 업데이트 혹은 삭제 작업이 많다면 HashMap, ArrayList 보다 컨커런트 컬렉션에서 제공하는 것을 사용하는 것이 좋다.
메모리 정합성 관련 속성
  • 자바 언어 스펙을 보면 공유되는 변수의 값에 대해서 멀티 스레드가 읽고 쓰기를 할때 데이터의 정합성을 보장하기 위해서 synchronized나 volatile 키워드로 보호해야 한다고 기술되어 있다.

Executors 클래스

  • 스레드 관리와 비즈니스 구현을 분리할수 있고 사전에 만들어둔 기능을 이용할 수 있다.
  • Executor 인터페이스 : 컨커런트 API의 핵심 인터페이스이다.
  • 스레드 풀 : 스레드를 관리하기 위한 풀이다. 병렬 프로그래밍에서 스레드를 관리하기 위한 기능을 제공한다.
  • 포크/조인 프레임워크 : JDK7에서 새롭게 포크/조인 프레임워크를 이용하면 스레드 간의 대기와 연관관계 등을 정의할 수 있다.

 

인터페이스  설명
Executor  새로운 태스크를 생성하는데 가장 기본이 되는 인터페이스
ExecutorService  Executor 인터페이스의 하위 인터페이스. Executor 인터페이스에서 제공하는 기능 외에 작업의 생명주기를 관리하는 기능을 제공
ScheduledExecutorService  ExecutorService 인터페이스의 하위 인터페이스. ExecutorService 인터페이스에서 제공하는 기능 외에 주기적으로 실행되거나 일정 시간 후에 실행할 수 있는 기능을 제공한다.

 

Executor 인터페이스

  • 태스크를 실행하는 데 가장 기본이 되는 인터페이스이다.
  • Executor 인터페이스는 execute 메서드 하나만 제공된다.

스레드 실행시

Thread thread = new Thread(new Runnable() {
    public void run() {
        System.out.println("실행");
    }
});

thread.start();

컨커런트 API의 Executor 인터페이스 사용시

public class ExcutorExample implements Executor {

    @Override
    public void execute(Runnable task) {
        // 방법1 : Runnable 인터페이스를 직접 실행
        task.run();

        // 방법2 : Thread를 생성해서 실행한다.
        //new Thread(task).start();
    }

    public static void main(String[] args) {
        Executor executor = new ExcutorExample();
        executor.execute(() -> System.out.println("Hello, Excutor!!"));
    }
}
  • execute 메서드에서 입력 파라미터로 전달받은 Runnable 객체를 어떻게 처리할 것인지 정의하는 부분으로 Runnable 객체를 run 메서드를 호출하거나, 새로운 스레드로 생성한 다음 실행하거나 둘중에 하나를 선택할 수 있도록 했다.
  • Excutor 인터페이스가 컨커런트 API의 최상위 인터페이스여서 가장 기본이 되는 메서드만 정의하였기 때문이다.
  • 실제로 컨커런트 API를 사용할때 Excutor 인터페이스를 직접 상속하고 정의할 일은 없다. 스레드를 생성하는 것으로 끝나기 때문이다.

ExecutorService 인터페이스

  • Excutor 인터페이스를 상속하였고, 기본 제공하는 execute 메서드 외에 스레드를 생성하고 이를 관리하기 위한 메서드를 추가로 정의해 놓았다.
  • ExecutorSerivce 인터페이스를 사용하는 것이 가장 보편적이고 일반적인 사용방법이다.
public class ExecutorServiceExample {

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        executorService.execute(new MyTask("TODO 1"));
        executorService.execute(new MyTask("TODO 2"));
        executorService.execute(new MyTask("TODO 3"));
        executorService.shutdown();
    }
}




@AllArgsConstructor
@NoArgsConstructor
public class MyTask implements Runnable {
    private String id;

    @Override
    public void run() {
        for (int i = 0; i <5; i++) {
            System.out.println("Task ID : " + id + ", running ... " + i);
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

결과

Task ID : TODO 1, running ... 0
Task ID : TODO 1, running ... 1
Task ID : TODO 1, running ... 2
Task ID : TODO 1, running ... 3
Task ID : TODO 1, running ... 4
Task ID : TODO 2, running ... 0
Task ID : TODO 2, running ... 1
Task ID : TODO 2, running ... 2
Task ID : TODO 2, running ... 3
Task ID : TODO 2, running ... 4
Task ID : TODO 3, running ... 0
Task ID : TODO 3, running ... 1
Task ID : TODO 3, running ... 2
Task ID : TODO 3, running ... 3
Task ID : TODO 3, running ... 4
  • newSingleThreadExecutor의 경우 오직 하나의 스레드만 처리될수 있도록 스레드 풀을 생성한다.
  • ExecutorService에 여러 개의 태스크를 등록하면 병렬 처리되지 않고 순차처리가 된다.
  • newFixedThreadPool는 스레드를 여러개 동시에 실행되도록 스레드 풀을 만든다.
  • 스레드 풀이 적으면 스레드가 종료한 다음 실행한다. 스레드 개수를 조절 가능
  • newCachedThreadPool는 여러 스레드를 병렬처리한다는 점에서 newFixedThreadPool과 유사하지만, 실행하는 스레드 수에 제한없이 등록한 모든 스레드를 동시에 처리한다는 점에서 다르다.
  • 컨커런트 API의 시작은 태스크를 만들고 이 태스크를 어떤 정책으로 실행할지 결정하는 것이다.
    결정할 것은 어떤 스레드 풀을 이용해서 병렬처리 할지, 싱글 스레드 풀처럼 순차적으로 태스크를 처리할지, 고정된 스레드 풀을 이용해서 최대 실행 가능한 태스크 수를 제어할것인지 캐시풀을 이용해서 크기에 제한을 가하지 않지만 생성된 스레드를 재사용하는 방법을 사용할지 결정해야 한다.

 

출처 - Practical 모던자바
저자 - 장윤기  

반응형

'JAVA > JAVA 기초' 카테고리의 다른 글

자바 코딩의 기술 - #7 객체 디자인  (0) 2021.12.07
자바 코딩의 기술 - #2 코딩 스타일  (0) 2020.11.26
자바 코딩의 기술 - #1 코드 정리  (0) 2020.11.26
자바 코딩 규약  (0) 2019.11.26
Object의 clone() 복사  (0) 2019.03.12