Skip to content

Latest commit

 

History

History
451 lines (414 loc) · 47.6 KB

11장.md

File metadata and controls

451 lines (414 loc) · 47.6 KB

11 성능, 확장성

  • 스레드를 사용하는 가장 큰 목적 = 성능을 높임
  • 스레드를 사용하면 시스템의 자원을 훨씬 효율적으로 활용할 수 있고, 애플리케이션으로 하여금 시스템이 갖고 있는 능력을 최대한 사용하게 할 수 있고 응답속도도 향상시킬 수 있다.
  • 11장에서는 병렬프로그램의 성능을 분석하고, 모니터링하고, 그 결과로 성능을 향상시킬 수 있는 방법에 대해 알아본다.
  • 대부분 애플리케이션의 내부 구조를 복잡하게 만들어야 하는 경우가 많고, 안전성과 활동성에 문제가 생길 가능성도 많다.
    • 성능을 높이기 위해 적용한 프로그래밍 기법이 역효과를 가져올 수도 있다.
    • 성능때문에 안전성을 해칠 수는 없다.
  • 일단 프로그램이 정상적으로 동작하도록 만들고 난 다음 프로그램이 빠르게 동작하는 편이 낫다.

11.1 성능에 대해

  • 성능을 높이는 것 = 더 적은 자원을 사용하면서 더 많은 일을 하도록 한다
    • 자원 -> 처리해야 할 작업이 있을 때 CPU, 메모리, 네트워크 속도, 디스크 속도, 데이터베이스 처리 속도, 디스크 용량 가운데 하나
  • 자원중에 항상 모자라는 부분이 발생할 것이다.
  • 작업을 실행할 때 충분하지 못한 특정 자원 때문ㅇ ㅔ성능이 떨어지는 현상이 나타난다면, 작업의 성능이 해당 자원에 좌우된다고 한다.
  • 적절한 작업 비용을 스레드에 효율적으로 적용한다면 성능이나 응답성이 높아지고 처리 용량도 커지는 장점이 있다.
  • 그러나 잘못 설계된 병렬 애플리케이션은 순차적으로 작업을 처리하는 프로그램보다 느리게 동작하는 경우도 간혹 생긴다.
  • 더 나은 성능을 목표로 프로그램이 병렬로 동작하게 할 경우 생각해야할 2가지
      1. 프로그램이 확보할 수 있는 모든 자원을 최대한 활용
      1. 남는 자원이 생길 때마다 그 자원 역시 최대한 활용

11.1.1 성능 대 확장성

  • 애플리케이션의 성능은 여러가지 측변에서 자료를 수집해 측정 가능
    • 서비스 시간, 대기 시간(얼마나 빠르냐), 처리량, 용량(얼마나 많은 양을 하냐), 효율성, 확장성(CPU, 메모리, 디스크, I/O 장치 등의 추가적인 정비를 사용해 처리량이나 용량을 얼마나 쉽게 키울 수 있냐) 등
  • 병렬 프로그램 환경에서 확장성을 충분히 가질 수 있도록 애플리케이션을 설계하고 튜닝하는 방법은 기존에 해오던 일반적인 성능 최적화 방법과 다른 부분이 많다.
  • 성능을 높이기 위해서 튜닝 작업을 하는 경우에 그 목적은 동일한 일을 더 적은 노력으로 하고자 하는것 (O(n^2) -> O(n log n))
  • 성능에서 얼마나 빠르게와 얼마나 많이는 완전히 다른 뜻을 가지며, 심지어 서로 화합할 수 없는 상황도 생긴다.
  • 더 높은 확장성을 확보하거나 하드웨어의 자원을 더 많이 활용하도록 하다 보면, 앞서 큰 작업 하나를 작은 여러 개의 작업으로 분할해 처리하는 것처럼 개별 작업을 처리할 때 필요한 작업의 양을 늘리는 결과를 얻을 때가 많다.
  • 단일 스레드 애플리케이션에서 사용하던 성능 개선 방안은 대부분 확장성의 측면에서 효과적이지 않다.
  • 3-티어 모델을 보면 시스템의 확장성을 높이도록 변경하려 할 때 성능의 측면에서 얼마나 많은 소해를 보는 경우가 많은지 쉽게 알 수 있다.
  • 프리젠테이션, 비지니스 로직, 스로티지의 세 가지 부분이 하나로 통합돼 있는 단일 애플리케이션을 다중 티어 애플리케이션과 비교해 보면, 다중 티어 애플리케이션이 웬만큼 잘 만들어져있다 하더라도 별다른 튜닝을 하지 않은 단일 애플리케이션의 성능이 훨씬 나을 가능성이 ㅁ낳다.
    • 단일 구조 애플리케이션은 서로 다른 티어 간에 주고받는 도중에 발생하는 네트워크 시간 지연 현상도 없을 것이고, 연산 작업을 서로 다른 추상적인 계층을 통과시켜가며 처리하는데 드는 부하가 적기 때문
  • 하지만 단일 구조의 애플리케이션이 처리할 수 있었던 최대 부하를 넘어서는 작업량을 감당해야 하는 순간이 오면 문제는 심각.
  • 서버 애플리케이션을 만들 때는 얼마나 빠르게라는 측면보다 얼마나 많이 라는 측면, 즉 확장성과 처리량과 용량이 훨씬 중요하다.
  • 11장에서는 단일 스레드 상황에서의 성능보다는 확장성을 중점적으로 다룬다.

11.1.2 성능 트레이드 오프 측정

  • 공학적인 모든 선택의 순간에는 항상 트레이드 오프가 존재하기 마련.
  • 강 위에 다리를 건설할 때 돔더 두꺼운 강판을 사용하면 다리를 수용할 수 있는 용량이 늘어나고 아전성도 높아지겠지만 건설 비용 역시 크게 증가.
  • 소프트웨어 공학에서는 트레이트 오프에서 어떤 부분을 선택해야 할지를 결정하는데 필요한 정보가 그다지 충분하지 않다.
    • ex: 퀵소트 알고리즘은 대량의 자료를 정렬할 때 효율이 높지만, 자료의 양이 많지 않을 경우 버블 정렬 알고리즘이 훨씬 효율적이다.
  • 프로그램을 작성하는 도중에 효율적인 정렬 기능을 구현해야 할 필요가 있따면, 먼저 정렬할 대상 데이터의 규모가 어느 정도인지 먼저 알아낼 필요가 있고, 평균적인 처리 시간을 중점적으로 최적화할지, 최악의 경우에 중점을 둬야 할지, 아니면 예측성에 중점을 둬야 하는지에 대한 결정을 내릴 수 있도록 추가적인 자료를 뽑아내는 것도 좋다.
  • 하지만 일반적으로 정렬 기능을 라이브러리로 구현하는 입장에서 알 수 있는 정보는 굉장히 제한적이다.
  • 그러므로 최적화 기법을 너무 이른 시점에 적용하지 말아야 한다. 일단 제대로 동작하게 만들고 난 다음에 빠르게 동작하도록 최적화해야 하며, 예상한 것보다 심각하게 성능이 떨어지는 경우에만 최적화 기법을 적용하는 것으로도 충분하다.
  • 공학적인 결정을 내려야 하는 시저에는 어떤 효과를 얻고자 할 때 다른 비용을 지출해야만 할 수 있고, 또 어떤 경우에는 안전성을 확보하기 위해 비용을 지불해야 할 수도 있다.
  • 성능을 높이기 위한 대부분의 결정 사항에는 다양한 변수가 관여하곤 하고 처한 상황에 따라 결정 사항이 크게 달라진다. 특정 방법이 다른 방법보다 빠르다 라고 말하기 전에 고려해야 할 사항
      1. 빠르다란 단어가 무엇을 의마하는가?
      1. 어떤 조건을 갖춰야 할 때 이 방법이 실제로 빠르게 동작할 것인가? 부하가 적을 때? 부하가 걸릴 대? 데이터가 많을 때? 데이터가 적을 때? 이런 질문에 대한 대답에 명확한 수치를 보여줄 수 있는가?
      1. 위의 조건에 해당하는 경우가 얼마나 많이 발생하는가? 이런 질문에 대한 대답에 명확한 수치를 보여줄 수 있는가?
      1. 조건이 달라지는 다른 상황에서도 같은 코드를 사용할 수 있는가?
      1. 이 방법으로 성능을 개선하고자 할 때, 숨겨진 비용, 즉 개발 비용이나 유지 보수 비용이 증가하는 부분이 어느 정도인지? 그런 부분을 감수하면서까지 성능 개선 작업을 해야 하는가?
  • 이와 같은 판단 기준은 성능과 관련된 설계와 개발에 대한 결정사항이라면 어디든 적용해 볼 수 있지다.
  • 병렬 프로그램에서 발생하는 오류의 가장 큰 원인 = 성능을 높이려는 여러 가지 기법
    • 단순한 동기화 방법이 너무 느리다고 전제하고, 직접적인 동기화 구문을 덜 사용할 수 있게 해주고 아주 훌륭한 모습을 갖추긴 했지만 위험성을 많이 내포하고 있는 여러가지 방법이 공개되어 있으며 이런 방법이 동기화 구문과 관련된 여러가지 규칙을 사용하지 않아도 된다는 핑곗거리로 자주 소개되곤 한다.
  • 버그의 원인이 될 가능성이 조금이라도 있는 위험도 높은 코드는 매우 주의 깊게 살펴봐야 한다.
  • 성능을 높이기 위해 안전성을 떨어뜨리는 것은 최악의 상황, 결국 안전성과 성능 둘 다 놓치게 된다.
  • 성능을 튜닝하는 모든 과정에서 항상 성능 목표에 대한 명확한 요구사항이 있어야 하며 그래야 어느 부분을 튜닝하고 어느 시점에서 튜닝을 그만둬야 하는지 판단할 수 있다.
  • 성능 튜닝 작업을 한 다음에는 반드시 원하는 목표치를 달성했는지 다시 측정 값을 뽑아내야 한다.
    • 추측하지 말고 실제로 측정해보라.
  • 시장에 나온 성능 측정용 제품을 보면 소프트웨어의 성능을 세밀하게 측정해주고, 병목이 어디에 있는지 눈으로 직접 볼 수 있다.

11.2 암달의 법칙

  • 일부 작업은 자원을 많이 투입하면 더 빨리 처리할 수 있다.
    • ex : 곡식을 추수할 경우 사람 많으면 추수 작업 빠릴 끝남.
  • 어떤 작업은 기본적으로 순차적으로 처리해야 한다.
    • ex : 곡식이 자라는 과장은 작업인력이 많다고 빠르게 할 수 있는게 아님
  • 프로그램을 작성할 때 스레드를 사용하려는 주된 이유가 멀티 프로세서의 성능을 최대한 활용하는 것이라면, 프로그램에서 처리하는 내용이 병렬화를 할 수 있는 일인지를 확실히 해둬야 하고, 작업을 병렬화 했을 때 그 가능성을 최대한 활용할 수 있어야 한다.
  • 대부분의 병렬프로그램에는 병렬화 할 수 있는 작업과 순차적으로 처리해야 하는 작업이 뒤섞인 단위 작업의 덩어리를 갖고 있다. 암달의 법칙을 사용하면 예측값 얻을 수 있다.
  • 암달의 법칙 : 순차적으로 실행해야 하는 작업의 비율이 F, 하드웨어에 꽂혀있는 프로세서의 개수 N일 경우
    • 속도증가량 <= 1 / F + ((1 -F) / N)
  • N이 무한대까지 증가할 수록 속도 즈가량은 1/F까지 증가. 1/F 라는 속도 증가량은 순차적으로 진행돼야 하는 부분에이 전체 작업의 50%를 차지한다고 할 때 프로세서를 아무리 많이 꽂는다 해도 겨우 두배 빨리진다는 결과
  • 순차적으로 실행해야 하는 부분이 전체의 10%에 해당한다면 최고 10배까지 속도를 증가시킬수 있다고 예측할 수 있다.
  • 암달의 법칙을 활용하면 작업을 순차적으로 처리하는 부분이 많아질 때 느려지는 정도가 얼마만큼인지를 수치화할 수 있다.
    • 하드웨어에 CPu가 10개 꽂혀 있을 때 10%의 순차 작업을 갖고 있는 프로그램은 최고 5.3배 만큼의 속도를 증가시킬 수 있다.
    • 같은 상황에서 CPu를 100개를 꽂는다면 최대 9.2배까지 속도가 증가할 것이라고 예상할 수 있다.
    • 그러나보니 속도를 10배까지 증가시키려면 CPu의 활용도가 비효율적으로 떨어질 수 밖에 없다.
  • 암달의 법칙에 따르면 프로세서의 개수가 증가하면 증가할수록 순차적으로 실행해야 하는 부분이 아주 조금이라도 늘어나면 프로세서 개수에 비해 얻을 수 있는 속도 증가량이 크게 떨어진다.
  • 멀티프로세서 시스템에서 애플리케이션의 속도를 예측해보려면 순차적으로 처리해야 하는 작업이 얼마나 되는지를 살펴봐야 한다.
  • 예제 11.1 작업 큐에 대한 순차적인 접근
public class WorkerThread extends Thread {
    private final BlockingQueue<Runnable> queue;
    
    public WorkerThread(BlockingQueue<Runnable> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        while (true) {
            try {
                Runnable task = queue.take();
                task.run();
            } catch (InterruptedException e) {
                break; /* 스레드를 종료시킨다. */
            }
        }
    }
}
  • 예제 11.1의 코드에서 doWork 메소드를(? 어디?) N개의 스레드가 동시에 실행한다고 해보자. doWork 메소드는 공유돼 있는 작업 큐에 쌓인 작업을 가져와서 처리하게 되어있다. 그리고 각 작업은 다른 스레드나 다른 작업과 아무런 고나련 없이 독립적으로 동작한다고 가정하자.
  • 내부를 잘 살펴보면 순차적으로 처리해야만 하는 부분이 있다. 바로 작업 큐에서 작업을 하나씩 뽑아 내는 부분이다.
  • 여러 스레드가 동시 다발적으로 큐를 사용하려 할 때 안전성을 잃지 않도록 적당한 양의 동기화 작업이 선행돼야 한다.
    • ex : 큐의 상태를 안정적으로 유지하고자 락을 사용했다면 특정 스레드가 큐에서 작업을 하나 뽑아내는 그 시점에, 역시 큐에서 작업을 가져가고자 하는 다른 모든 스레드는 큐를 독점적으로 사용할 수 있을때까지 대기해야만 한다. 따라서 작업 큐와 관련된 부분에서는 프로그램이 순차적으로 처리될 수 밖에 없다.
  • 단일 작업 하나가 실행되는 시간에는 Runnable을 실행하는데 드는 시간뿐만 아니라 공유돼 있는 작업 큐에서 작업을 뽑아내는데 필요한 시간도 포함돼 있다.
  • 작업 큐로 LinkedBlockingQueue를 사용하고 있다면, 큐에서 작업을 뽑아낼 때 대기하는 시간이 동기화된 LinkedList를 사용할 때보다 훨씬 적게 든다. 데이터를 한 군데에 공유해두고 사용하는 모든 부분은 항상 순차적으로 처리해야 한다.
  • 작업의 처리 결과를 취합하는 부분도 마찬가지이다.
  • 모든 병렬 프로그램에는 항상 순차적으로 실행돼야만 하는 부분이 존재한다. 만약 그런 부분이 없다고 생각한다면 프로그램 코드를 다시 한번 들여다보라.

11.2.1 예제 : 프레임워크 내부에 감쳐줘있는 순차적 실행 구조

  • 애플리케이션의 내부 구조에 순차적으로 처리해야 하는 구조가 어떻게 숨겨져 있는지를 알아보려면 스레드 개수를 증가시킬 때마다 성능이 얼마나 빨라지는지를 기록해두고 추측 가능하다.
  • 작업을 처리하는 단계에는 단순하게 스레드 내부에서만 동작하는 연산 과정이 진행된다. 큐가 비었다는 사실을 스레드가 알게 되면, 해당 스레드는 큐에 일정 개수의 작업을 추가해서 작업을 가져가려는 다른 스레드가 계쏙해서 실행할 수 있도록 했다.
  • ConcurrentLinkedQueue 클래스의 처리량은 계속해서 증가하다가 프로세서의 개수에 해당하는 수치에 다다르면 유지
  • 동기화된 LinkedList는 3개까지는 증가하다가 그 이후에는 동기화 관련 부하가 늘어나서 성능이 떨어진다.
  • 동기화된 LinkedList 클래스는 전체 큐의 상태를 하나의 락으로 동기화하며, 따라서 offer나 remove메소드를 호출하는 동안 전체 큐가 모두 락에 걸린다.
  • 하지만 ConcurrentLinkedQueue 클래스는 정교한 큐 알고리즘사용. 즉 개별 포인터에 대한 업데이트 연산만 순차적으로 처리하면 된다.

11.2.2 정상적인 암달의 법칙 적용 방법

  • 암달의 법칙을 사용하면 프로그램 내부에서 순차적으로 처리돼야만 하는 부분의 비율을 알고 있을 때, 하드웨어를 추가함에 따라 얼마만큼 처리 속도가 증가할 것인지를 수치화해서 추측 가능
  • 대부분의 경우 멀티프로세서 시스템이라 하면 두 개나 네 개의 프로세서가 달린 경우를 생각하고, 자금의 여유가 있다면 잘해야 열 몇개의 프로세서를 장착하는 정도밖에 생각하지 못하낟.
  • 하지만 멀티코어 CPU가 대중적으로 많이 보급되면서 이제는 수백개에서 수천개의 프로세서를 장착한 시스템을 어렵지 않게 생각하게 됐다.
  • 수 백개 또는 수 천개의 프로세서가 동작하는 상황까지 가정한 상태에서 프로그램의 알고리즘을 평가한다면, 어느 시점쯤에서 확장성의 한계가 나타날것인지를 예측해 볼 수 있다.
    • 락의 적용 범위를 줄이는 방법, 즉 락분할 과 락 스트라이핑에 대해서 알아볼 것이다.
  • 암달의 법칙에서 바라보면 락을 두 개로 분할하는 정도로는 다수의 프로세서를 충분히 활용하기 어렵다는 결론을 얻을 수 있다.
  • 하지만 락 스트라이핑방법을 사용할 때는 프로세서의 수가 늘어남에 따라 분할 개수를 같이 증가시킬 수 있기 때문에 확장성을 얻을 수 있는 훨씬 믿을만한 방법이라고 할 수 있다.

11.3 스레드와 비용

  • 스레드를 사용하는 경우 병렬로 실행함으로써 얻을 수 있는 이득이 병렬로 실행하느라 드는 비용을 넘어서야 성능을 향상시킬 수 있다.

11.3.1 컨텍스트 스위칭

  • 메인 스레드 하나만 스케쥴링 한다고 하면, 메인스레드는 항상 실행될 것이다.
  • 반대로 CPU 개수보다 실행중인 스레드의 개수가 많다고 하면, 운영체제가 특정 스레드의 실행 스케쥴을 선점하고 다른 스레드가 실행될 수 있도록 스케쥴을 잡는다.
  • 이처럼 하나의 스레드가 실행되다가 다른 스레드가 실행되는 순간 컨텍스트 스위칭이 일어난다.
    • 먼저 실행중인 스레드의 실행 상태를 보관해두고, 다음 번에 실행되기로 스케쥴된 다른 스레드의 실행 상태를 다시 읽어드린다.
  • 컨텍스트 스위칭은 단숨에 공짜로 일어나는 일이 아니다. 스레드 스케줄링을 하려면 운영체제와 JVM 내부의 공용 자료 구조를 다뤄야 한다는 문제가 있다.
  • 운영체제와 JVM 역시 프로그램 스레드가 사용하는 것과 같은 CPU를 함께 사용하고 있다. 따라서 운영체제나 JVM이 CPU를 많이 사용하면 할수록 실제 프로그램 스레드가 사용할 수 있는 CPU의 양이 줄어든다.
  • 컨텍스트가 변경되면서 다른 스레드를 실행하려면 해당 스레드가 사용하던 데이터가 프로세서의 캐시 메모리에 들어 있지 않을 확률도 높다.
    • 그러면 캐시에서 찾이 못한 내용을 다른 저장소에서 찾아와야 하므로 느리게 실행된다
  • 이런 경우에 대비하고자 대부분의 스레드 스케줄러는 실행 대기 중인 스레드가 밀려 있다고 해도, 현재 실행중인 스레드에게 최소한의 실행 시간을 보장해주는 정책을 취하고 있다.
  • 그러면 컨텍스트 스위칭에 들어가는 시간과 비용을 나누는 효과를 볼 수 있고, 그 결과 인터럽트 받지 않고 실행할 수 있는 최소한의 시간을 보장받기 때문에 전체적으로 성능이 향상되는 효과를 볼 수 있다.
  • 스레드가 실행하다가 락을 확보하기 위해 대기하기 시작하면, 일반적으로 JVM은 해당 스레드를 일시적으로 정지시키고 다른 스레드가 실행되도록 한다.
  • 특정 스레드가 빈번하게 대기 상태에 들어간다고 하면 스레드별로 할당된 최소 실행 시간조차 사용하지 못한 경우도 있다.
  • 대기상태에 들어가는 연산을 많이 사용하는 프로그램은 CPu를 주로 활용하는 프로그램보다 컨텍스트 스위칭 횟수가 훨씬 많아지고, 따라서 스케줄링 부하가 늘어나면서 전체적인 처리량이 줄어든다.
  • 컨텍스트 스위칭에 필요한 부하와 비용은 플랫폼마다 다르지만 대략 살펴본 바에 따르면 최근 사용되는 프로세서상에서 5000~10000 클럭 사이클 또는 수 마이크로 초 동안의 시간을 소모한다고 알려져 있다.
  • 유닉스 시스템의 vmstat 명령이나, 윈도우 시스템의 perform 유틸리티를 사용하면 컨텍스트 스위칭이 일어난 횟수를 확인할 수 있으며 커널 수준에서 얼마만큼의 시간을 소모했는지 알 수 있다.
  • 커널 활용도가 10%가 넘는 높은 값을 갖고 있다면 스케줄링에 부하가 걸린다는 의미이며, 아마도 애플리케이션 내부의 I/O 작업이나 락 관련 동기화 부분 때문에 대기 상태에 들어가는 부분이 원인일 가능성이 높다.

11.3.2 메모리 동기화

  • 동기화에 필요한 비용은 여러 곳에서 발생.
  • synchronized, volatile 키워드를 사용해 얻을 수 있는 가시성을 통해 메모리 배리어라는 특별한 명령어를 사용할 수 있다.
  • 메모리 배리어는 캐시를 플러스하거나 무효화하고, 하드웨어와 관련된 쓰기 버퍼를 플러시하고, 실행 파이프 라인을 늦출 수 있다.
  • 메모리 배리어를 사용하면 컴파일러가 제공하는 여러 가지 최적화 기법을 제대로 사용할 수 없게 돼 간접적인 성능 문제를 가져올 수 있다. (명령어 재배치를 할 수 없기 때문)
  • 동기화가 성능에 미치는 영향을 파악하려면 경쟁적인지, 비경쟁적인지 확인해야 한다.
  • synchronized 키워드가 동작하는 방법은 비경쟁적인 경우에 최적화돼 있기 때문에 빠른경로의 비경쟁적인 동기화 방법은 대부분의 시스템에서 20~15- 클럭 사이클을 사용한다고 알려져 있다.
  • 비경쟁적이면서 꼭 필요한 동기화 방법은 성능에 큰 영향이 없다고 할 수 있다.
  • 최근에 사용하는 JVM은 대부분 다른 스레드와 경쟁할 가능성이 없다고 판단되는 부분에 락이 걸려 있다면 최적화 과정에서 해당 락을 사용하지 않도록 방지하는 기능을 제공하기도 한다.
    • ex : 락을 거는 객체가 특정 스레드 내부에 한정돼 있다면 해당 락을 다른 스레드에서 사용하며 경쟁 조건에 들어갈 수 없기 때문에 JVM은 자동으로 해당 락은 무시하고 넘어간다
    • 즉 예제 11.2와 같은 코드를 실행할 때 JVM은 락을 사용하지 않는다.
  • 예제 11.2 아무런 의미가 없는 동기화 구문. 이런 코드는 금물
synchronized(new Object()) {
    //작업 진행
}
  • 훨씬 정교하게 만들어진 JVM의 경우(????) 유출 분석을 통해 로컬 변수가 외부로 공개된 적이 없는지, 다시 말해 해당 변수가 스레드 내부에서만 사용되는지를 판단하기도 한다.
  • 예제 11.3 락 제거 대상
public class Example11_3 {
    public String getStoogeNames() {
        List<String> stooges = new Vector<String>(); // 스레드 내부에 종속
        stooges.add("Moe");
        stooges.add("Larry");
        stooges.add("Curly");
        return stooges.toString();
    }
}
  • 허술한 JVM(???) 에서 예제 11.3을 실행시키면 Vector 객체에 add 하는 부분과 toString을 호출하는 부분을 더해 4번 잡았다 놓았다 반복하게 된다.
  • 정교한 고급 컴파일러와 JVM은 stooges 변수가 메소드 외부에 노출된 적이 없다는 것을 판단하고 락 생략해서 빠르게 진행시킨다.
  • 유출 분석을 사용하지 않는 경우라면, 락 확장, 즉 연달아 부어있는 여러개의 synchronized 블록을 하나의 락으로 묶는 방법을 사용하기도 한다.
  • 락 확장 기능을 갖고 있는 JVM에서 11.3 실행 시 4개의 호출 부분을 하나로 묶어 락을 한번만 확보하고 해제한다.
  • 물론 락 확장 기능을 사용하는 JVM은 락을 확보하고 헤재하는 데 걸리는 시간과 synchronized 블록 내부의 작업에 걸리는 시간을 살펴보고 락을 확장하는 것이 효율적이라고 판단되는 경우에만 확장하기도 한다.
  • 락 확장 밥법을 사용하면 동기화 관련 부하를 줄이는데 도움을 줄 뿐만 아니라 최적화 모듈이 좀더 큰 단위의 블록을 대상으로 추가적인 최적화 작업을 진행할 기회가 생기기도 한다.
  • 경쟁조건에 들어가지 않는 동기화 블록에 대해서는 그다지 걱정하지 않아도 좋다. 동기화 블록의 기본적인 구조가 상당히 빠르게 동작할 뿐만 아니라, JVM 수준에서 동기화와 관련한 추가적인 최적화 작업을 진행하기 때문에 동기화 관련 부하를 줄이거나 아예 없애주기도 한다. 대신 경쟁 조건이 발생하는 동기화 블록을 어떻게 최적화할지에 대해서 고민하자.
  • 특정 스레드에 진행되는 동기화 작업으로 인해 다른 스레드의 성능이 영향을 받을 수도 있다. 동기화 작업은 공유돼 있는 메모리를 통하는 버스에 많은 트래픽을 유발하기 때문.

11.3.3 블로킹

  • 경쟁하지 않는 상태에서의 동기화 작업은 전적으로 JVM내부에서 처리할 수 있다.
  • 하지만 경쟁 조건이 발생할 때에는 동기화 작업에 운영체제가 관여해야 할 수 있는데, 운영체제가 관여하는 부분은 모두 일정량의 자원을 소모한다.
  • JVM은 스레드를 대기 상태에 둘 때 두가지 방법을 사용할 수 있다
      1. 스핀 대기 : 락을 확보할 때까지 계속해서 재시도
      1. 운영체제가 제공하는 기능을 사용해 스레드를 실제 대기 상태로 두는 방법
  • 컨텍스트 스위칭에 필요한 자원의 양과 락을 확보할 때까지 걸리는 시간이 크게 좌우한다.
  • 대기시간이 짧을 경우 스핀대기, 긴 경우 운영체제의 기능을 호출
  • 대부분의 경우 운영체제가 제공하는 기능을 호출

11.4 락 경쟁 줄이기

  • 작업을 순차적으로 처리하면 확장성을 놓치고, 작업을 병렬로 처리하면 컨택스트 스위칭에서 성능에 악영향을 준다.
  • 락을 놓고 경쟁할 경우에는 순차적으로 처리함과 동시에 컨텍스트 스위칭도 같이 발생하므로 확장성과 성능을 동시에 떨어뜨리는 원인이 된다.
  • 락 경쟁을 줄여야 한다.
  • 병렬 애플리케이션에서 확장성에 가장 큰 위협이 되는 존재는 바로 특정 자원을 독점적으로 사용하도록 제한하는 락이다
  • 락을 두고 발생하는 경쟁 상황
      1. 락을 얼마나 빈번하게 확보하려고 하는지
      1. 한 번 확보하고 나면 해제할 때까지 얼마나 오래 사용하는지
  • 이 두가지를 곱한 값이 충분히 작다면 상고나없지만 클 경우 작업할 내용이 쌓여 있어도 CPU는 놀 가능성이 있다.
  • 락 경쟁 족너을 줄일 수 있는 방법
      1. 락을 확보한 채로 유지되는 시간 줄이기
      1. 락을 확보하고자 요청하는 횟수 줄이기
      1. 독점적인 락 대신 병렬성을 크게 높여주는 여러 가지 조율 방법 사용

11.4.1 락 구역 좁히기

  • 락 경쟁 발생 가능성 좁히기 -> 락을 유지하는 시간 줄이기
  • 락이 꼭 필요하지 않은 코드를 synchronized 블록 밖으로 뽑아내어 락이 영향을 미치는 구역을 좁히면 락을 유지하는 시간을 줄일 수 있다.
  • 예제 11.4 필요 이상으로 락을잡고 있는 모습
@ThreadSafe
public class AttributeStore {
    @GuardedBy("this") private final Map<String, String> attribute = new HashMap<String, String>();
    
    public synchronized boolean userLocationMatches(String name, String regexp) {
        String key = "users." + name + ".location";
        String location = attribute.get(key); // <- 유일하게 synchronized로 막아야 할 부분.
        if(location == null) {
            return false;
        }
        
        return Pattern.matches(regexp, location);
    }
}
  • 예제 11.5 락 점유 시간 단축
@ThreadSafe
public class BetterAttributeStore {
    @GuardedBy("this") private final Map<String, String> attribute = new HashMap<String, String>();

    public boolean userLocationMatches(String name, String regexp) {
        String key = "users." + name + ".location";
        String location;
        synchronized (this) {
            location = attribute.get(key);
        }
        if(location == null) {
            return false;
        }

        return Pattern.matches(regexp, location);
    }
}
  • 암달의 법칙을 통해 보면 순차적으로 처리해야 되는 코드의 양이 줄어드는 효과가 있으므로 확장성을 저해하는 요소를 줄이는 결과도 기대할 수 있다.
  • AttributeStore 클래스에는 공유된 상태 변수가 attribute 하나이기 때문에 스레드 안전성 위임방법을 사용해 좀 더 개선해 볼 여지가 있다.
  • 즉 일반 attribute 변수를 hashMap 대신 안전성이 확본된 클래스를 사용하면 스레드의 안전성을 모두 변수에 위임할 수 있다.
  • synchronized 블록을 줄이면 줄일 수록 애플리케이션의 확장성을 늘릴수 있다고 하지만, 단일연산으로 실행해야 될 명령어꺼지 빼내서는 안된다.
  • 또한 동기화를 맞추는 데도 자원이 필요하기 때문에 두 개 이상으로 쪼개는 일도 어느 한도를 넘어서면 성능의 측면에서 오히려 악영향을 끼칠 수 있다.

11.4.2 락 정밀도 높이기

  • 스레드에서 해당 락을 덜 사용하도록 변경하는 방법도 있다
  • 락 분할과 락 스트라이핑 방법이 있는데 두가지 모두 하나의 락으로 여러 개의 상 태 변수를 한번ㅇ ㅔ묶어두지 않고, 서로 다른 락을 사용해 여러 개의 독립적인 상태 변수를 각자 묶어두는 방법이다.
  • 두 가지 기법을 활용하면 락으로 묶이는 프로그램의 범위를 조밀하게 나누는 효과가 있으며, 따라서 결국 애플리케잇녀의 확장성이 높아지는 결과를 기대할 수 있다.
  • 반대로 락의 개수가 많아질수록 데드라기 발생할 위험도 높아지니 주의
  • 락이 두 개 이상의 독립적인 상태 변수를 한번에 묶어서 동기화해주고 있다면 해당하는 코드 블록을 상태 변수에 맞춰 두 개 이상의 락으로 동기화하도록 분할해 확장성을 높일 수 있다.
  • 예제 11.6 락 분할 대상
@ThreadSafe
public class ServerStatus {
    @GuardedBy("this") public final Set<String> users; // 로그인된 사용자 목록
    @GuardedBy("this") public final Set<String> queries; //현재 실행중인 데이터베이스 쿼리
    //두개의 변수는 완전히 독립적.

    public ServerStatus(Set<String> users, Set<String> queries) {
        this.users = users;
        this.queries = queries;
    }
    
    public synchronized void addUser(String u) {users.add(u);} ///로그인
    public synchronized void addQuery(String q) {queries.add(q);} //쿼리실행
    public synchronized void removeUser(String u) {users.remove(u);} //로그아웃
    public synchronized void removeQuery(String q) {users.remove(q);} //쿼리 끝
}
  • 예제 11.7 락이 분할된 ServerStatus 클래스
@ThreadSafe
public class ServerStatus {
    @GuardedBy("this")
    public final Set<String> users;
    @GuardedBy("this")
    public final Set<String> queries;

    public ServerStatus(Set<String> users, Set<String> queries) {
        this.users = users;
        this.queries = queries;
    }

    public void addUser(String u) {
        synchronized (users) { //락의 정밀도가 높아짐, 대기 상태에 들어가는 경우가 크게 줄어든다.
            users.add(u);
        }
    }

    public synchronized void addQuery(String q) {
        synchronized (queries) {
            queries.add(q);
        }
    }
    
    // remove 메소드 역시 분할 가능
}
  • 락을 하나에서 둘로 분할하는 방법은 경쟁 조건이 아주 심하지는 않지만 그래도 어느 정도 경쟁이 발생하고 있는 경우에 가장 큰 효과를 볼 수 있다.
  • 그러나 경쟁상황이 거의 발생하지 않는 경우에는 큰 효과를 보지는 못하지만, 부하가 걸리면서 경쟁이 발생하기 시작했을 때 성능이 떨어지는 시점을 늦출 수도 있다.
  • 대부분의 동기화 블록에서 락 경쟁이 일어나지 않도록 할 수 있으며, 따라서 처리량과 확장성의 측면에서 큰 이득을 얻을 수 있다.

11.4.3 락 스트라이핑

  • 경쟁 조건이 굉장히 심한 락을 두 개로 분할하고 나면, 경쟁이 심한 락이 두 개가 생긴다.
  • 락 분할 방법은 때에 다라 독립적인 객체를 여러 가지 크기로 묶어내고, 묶인 블록을 단위로 락을 나누는 방법이 락 스트라이핑
    • ex : ConcurrentHashMap 클래스가 구현된 소스코드를 보면 16개의 락을 배열로 마련해두고, 16개의 락 각자가 전체 해시 범위의 1/16에 대한 락을 담당한다. 따라서 N번째 해시 값은 락 배열에서 N mod 16의 값에 해당하는 락으로 동기화된다.
    • ConcurrentHashMap에서 사용하는 해시 함수가 적당한 수준 이상으로 맵에 들어있는 항목을 분산시켜 준다는 가정하에 락 경쟁이 발생할 확률을 1/16으로 낮추는 효과가 있다.
    • 결국 ConcurrentHashMap은 최대 16개의 스레드에서 경쟁 없이 동시에 맵에 들어 있는 데이터를 사용할 수 있도록 구현돼 있는 셈이다.
  • 락 스트라이핑을 사용하다 보면 여러 개의 락을 사용하도록 쪼개 놓은 컬렉션 전체를 한꺼번에 독점적으로 사용해야 할 필요가 있을 수도 있는데, 이런 경우에는 단일 락을 사용할때보다 동기화 시키기가 어렵고 자원 소모도 많다.
  • 대부분 작업을 처리할 때는 쪼개진 락 하나만 확보하는 것으로도 충분하지만, 간혹 전체 컬렉션을 독점해야 하는 경우가 생긴다.
    • 이런 경우에는 보통 쪼개진 락을 전부 확보한 이후에 처리하도록 구현한다.
  • 예제 11.8 락 스트라이핑을 사용하는 해시 기반의 맵
@ThreadSafe
public class StripeMap {
    // 동기화 정책 : bucket[n]은 locks[n%N_LOCKS]락으로 동기화된다.
    private static final int N_LOCKS = 16;
    private final Node[] buckets;
    private Object[] locks;
    
    private class Node{}
    
    public StripeMap(int numBuckets) {
        buckets = new Node[numBuckets];
        locks = new Object[N_LOCKS];
        IntStream.range(0, N_LOCKS).mapToObj(i -> locks[i]).forEach(lock -> new Object());
    }
    
    private final int hash(Object key) {
        return Math.abs(key.hashCode() % buckets.length);
    }
    
    public Object get(Object key) {
        int hash = hash(key);
        synchronized (locks[hash % N_LOCKS]) {
            for(Node m = buckets[hash]; m != null ; m = m.next) {
                if(m.key.equals(key)) {
                    return m.value;
                }
            }
        }
        return null;
    }
    
    public void clear() {
        for(int i = 0; i < buckets.length; i++) {
            synchronized (locks[i % N_LOCKS]) {
                buckets[i] = null;
            }
        }
    }
}
  • N_LOCKS 만큼의 락 생성, N_LOCKS 개의 락이 각자의 범위에 해당하는 해시 공간에 대한 동기화를 담당한다.
  • get메소드와 같은 대부분의 메소드는 N_LOCKS개의 락 가운데 하나만 확보하는 것으로 충분하다.
  • 물론 N_LOCKS개의 락을 한꺼번에 모두 확보해야 하는 경우가 있지만, clear 메소드에 구현된 것과 같이 N_LOCKS개의 락을 확보하지 않고 처리할 수 있는 방법이 있을 수도 있다.

11.4.4 핫 필드 최소화

  • 락 분할 방법과 락 스트라이핑은 여러 개의 스레드가 각자 방해받지 않으면서 독립적인 데이터를 사용할 수 있도록 해주기 때문에 애플리케이션의 확장성을 높여준다.
  • 애플리케이션 내부를 살펴봤을 때 락으로 동기화시킨 데이터에 대한 경쟁보다 락 자체에 대한 경쟁이 더심한 경우에 락 분할 방법으로 확장성에 이득을 얻을 수 있다.
  • 하나의 락으로 두개의 독립적인 변수 X와 Y를 동기화하고 있고, 스레드 A는 변수 X를 사용하려고 하고 스레드 B는 Y를 사용하려고 할 경우 스레드 A와 스레드 B는 서로 독립적인 데이터를 사용하기 때문에 데이터를 두고 경쟁하지는 않지만, 하나의 락으로 동기화되어 있기 때문에 락을 확보하기 위해 경쟁하게 된다.
  • 모든 연산에 꼭 필요한 변수가 있다면 락의 정밀도를 세밀하게 쪼개는 방법을 적용할 수 없다.
  • 이 부분은 성능과 확장성이 서로 공존하기 어렵게 만드는 또다른 요인
  • 자주 계산하고 사용하는 값을 캐시에 저장해두도록 최적화 한다면 확장성을 떨어뜨릴 수밖에 없는 핫필드가 나타난다.
  • ex : HashMap클래스를 구현시 맴 내부에 들어 있는 항목의 개수를 세는 size 메소드를 구현할 경우
    • size를 호출할 때마다 항목의 수를 매번 계산
    • 약간 더 최적화하는 방법은 개수 카운터를 두고 항목이 추가되거나 제거될 때 카운트 증거나 감소
    • 이 방법을 사용하면 O(n)에서 O(1)로 크게 줄일 수 있다.
  • 항목의 개수를 따로 관리하는 방법으로 최적화해 size 메소드나 isEmpty 메소드등의 처리 속도를 높이면 단일 스레드 애플리케이션이나 완전히 동기화된 애플리케이션에서 잘 작동
  • 하지만 맵 내부에 항목을 변경하는 모든 기능을 호출할 때, 공유된 변수인 개수 카운터의 값을 변경해야 하기 때문에 멀티 스레드 애플리케이션에서는 확장성을 높이는 일이 굉장히 어려워진다.
  • 결국 성능의 측면에서 최적화라고 생각했던 기법, 즉 맵 내부에 항목을 캐시해둔는 방법이 확장성의 발목을 ㅏㅈㅂ는 셈.
  • 이와 같이 모든 연산을 수행할 때마다 한 번씩 사용해야 하는 카운터 변수와 같은 부분이 핫필드
  • ConcurrentHashMap의 경우 전체 카운트를 하나의 변수에 두지 않고, 락으로 분배된 각 부분마다 카운터 변수를 따로 두고 관리하면서 size메소드 호출 시 각 카운터 변수의 합을 알려주는 방법 사용.

11.4.5 독점적인 락을 최소화하는 다른 방법

  • 락 경쟁 때문에 발생하는 문제점을 줄일 수 있는 또 다른 방법 = 좀 더 높은 병렬성으로 공유된 변수를 관리하는 방법 도입(이로인해 락 사용 줄이기)
  • ex : 병렬 컬렉션 클래스를 사용하거나 read-write락을 사용하거나 불변 객체를 사용하고 단일 연산 변수를 사용하는 등의 방법
  • ReadWriteLock 클래스를 사용하면 여러 개의 reader가 있고 하나의 writer가 있는 상황으로 문제를 압축할 수 있다.
    • 여러 개의 스레드에서 공유된 변수의 내용을 읽어가려고 하고 대신 값을 변경하지는 못한다.
    • 값을 변경할 수 있는 단 하나의 스레드는 값을 쓸 때 락을 독점적으로 확보한다.
    • 읽기 연산이 대부분을 차지하는 데이터 구조에 적용하기가 알맞으며 전체적으로 독점적인 락을 사용하는 경우보다 병렬성 측면에서 확장성을 크게 높여준다.
    • 읽기 전용의 데이터 구조라면 불변 클래스의 형태를 유지하는 것만으로도 동기화 코드를 완전히 제거해버릴수 있다.
  • 단일 연산 변수를 사용하면 통계 값을 위한 카운터 변수나 일련번호 생성 모듈, 링크로 구성된 데이터 구조에서 첫번째 항목을 가리키는 링크와 같은 '핫필드' 가 존재할 때 핫 필드의 값을 손쉽게 변경할 수 있게 해주낟.
    • 단일연산클래스는 숫자나 객체에 대한 참조 등을 대상으로 정밀도가 높은 단일 연산 기능을 제공하며, 그 내부적으로는 CPU 프로세서에서 제공하는 저수준의 병렬처리 기능을 활용하고 있다.
    • 작정중인 클래스 내부에 다른 변수와의 불변조건에 관여하지 않는 핫 필드가 몇 개 정도 있다면 해당하는 핫 필드를 단일 연산 변수로 변경하는 것만으로도 확장성에 이득을 볼 수 있다.

11.4.6 CPU활용도 모니터링

  • 확장성 테스트의 목적은 CPU를 최대한 활용하는데 있다.
  • 유닉스 환경의 vmstat이나 mpstat과 같은 유틸리티, 또는 윈도우 환경의 perform과 같은 유틸리티를 사용하면 CPU확인 가능
  • 두 개 이상의 CPU가 장착된 시스템에서 일부 CPU만 열심히 일하고 나머지는 놀고 있다면 가장 먼저 해야할 일은 프로그램의 병렬성을 높이는 방법을 찾아 정용하는 일
  • CPU를 충분히 활용하지 못하는 원인
      1. 부하가 부족하다 : 부하 상황이 안되어 있다면 부하를 점점 늘려가면서 CPU사용률이나 응답 시간이나 서비스 시간 드으이 항목이 어떻게 증가하는지 측정, 애플리케이션이 허덕거릴 만큼의 부하를 만들어 내는게 쉽지 않고, 클라이언트 시스템 문제일 수도 있다.
      1. I/O 제약 : iostat이나 perform 등의 유틸리티를 사용하면 성능가운데 디스크 관련 부분이 얼마나 되는지를 살펴볼 수 있다. 네트워크 트래픽 수준을 모니터링 하면 대역폴을 얼마나 사용하고 있는지도 쉽게 파악 가능
      1. 외부 제약 사항 : 외부 데이터베이스 또는 웹 서비스 등 사용시 성능에 발목을 잡는 부분이 외부에 있을 가능성도 있다.
      1. 락 경쟁 : 프로파일링 도구 활용시 락 경쟁 조건이 얼마나 발생했는지 알아 볼 수 있으며, 어느 락이 가장 빈번하게 경쟁의 목표가 되는지도 알 수 있다.
  • 애플리케이션이 CPU를 적절한 수준 이상으로 충분히 사용하고 있다면, 위에 소개했던 여러가지 모니터링 방법을 사용해서 CPU를 추가했을 때 얼마나 이득을 볼 수 있을 것인지 예측해 볼 수 있다.

11.4.7 객체 폴링은 하지 말자

  • 최근에는 JVM에서 객체를 새로 메모리를 할당하는 작업과 가비지 컬렉션 작업이 크게 개선됐다.
  • 예전에 객체 관련 할당과 제거 작업이 느렸을 때믄 객체를 더 이상 사용하지 않는다 해도 가비지 컬렉터에 넘기는 대신 재사용할 수 있게 보관해두고, 꼭 필요한 경우에만 새로운 객체를 생성하는 객체 풀을 ㅁ낳이 활용했었다.
  • 이런 방법을 사용해 가비지 컬렉션에 소모되는 시간을 줄일 수 있다고 하지만 그렇다 해도 단일 스레드 애플리케이션에서 아주 무겁고 큰 객체를 제외하고는 일반적으로 성능에 좋이 않은 영향
  • 게다가 크기가 작거나 중간 크기인 객체를 풀로 관리하는 일은 오히려 상당한 자원 소모
  • 병렬 애플리케이션에서는 객체 풀링을 사용했을 때 훨신 많은 비용을 지불해야 할 수도 있다.
  • 객체 풀 역시 성능을 최적화할 수 있는 방법가운데 하나라고 생각하기도 하지만 반대로 확장성에는 심각한 문제를 일으킬 수 있다.
  • 객체 풀의 성능 최적화는 적절한 방법이 아니다.

11.5 예제 : Map 객체의 성능 분석

  • 단일 스레드 환경에서 ConcurrentHashMap은 동기화된 HashMap보다 성능이 약간 빠르다. 하지만 병렬 처리 환경에서는 ConcurrentHashMap의 성능이 빛을 발한다.
  • ConcurrentHashMap의 구현 내용을 살펴보면 가장 많이 사용하는 기능이 현재 맵 내부에 갖고 있는 값을 찾아내 가져가는 연산이라고 가정하고 있으며, 따라서 ConcurrentHashMap은 여러 개의 스레드세어 get 메소드를 연달하 오출하는 경우에 가장 빠른 속도를 낸다.
  • 동기화된 HashMap 클래스가 속도가 떨어지는 가장 큰 이유는 물론 맵 전체가 하나의 락으로 동기화돼 있다는 점이고, 따라서 한번에 단 하나의 스레드만이 맵을 사용할 수 있다.
  • 또한 ConcurrentHashMap은 대부분의 읽기 연산에는 락을 걸지 않고 있으며 쓰기 연산과 일부 읽기 연산에는 락 스트라이핑을 활용하고 있다.
  • 이런 기법에 힘입어 대부분의 경우 대기 상태에 들어가지 않고도 다수의 스레드가 동시에 ConcurrentHashMap의 기능 사용 가능
  • ConcurrentHashMap과 ConcurrentSkipListMap은 애초에 설계부터 멀티 스레드 환경에서 사용하는 것을 목표로 만들어졌고, synchronizedMap을 활용해 동기화시킨 HashMap와 TreeMap은 아주 단순하게 동기화를 맞춘 것
  • ConcurrentHashMap와 ConcurrentSkipListMap은 결과를 보면 스레드 수가 늘어남에 따라 성능이 잘 따라와 준다.
  • 두개가 아닌 경우 성능이 떠어지는데 이런 성능 저하는 락 경쟁을 제대로 막지 못하는 경우에 발생
  • 경쟁이 많이 발생하면 연산에 필요한 시간의 대부분이 컨텍스트 스위칭과 스케줄링에 필요한 대기 시간으로 소모되며 스레드를 추가한다 해도 성능을 못끌어올린다.
  • ConcurrentHashMap나 ConcurrentSkipListMap을 사용하자

11.6 컨텍스트 스위치 부하 줄이기

  • 다양한 종류의 연산이 대기 상태에 들어갈 수 밖에 없는 특성이 있다.
  • 실행과 대기의 두 가지 상태에 옮겨 다니는 것 = 컨텍스트 스위치
  • ex: 요청을 처리하는 가운데 출력할 로그 메시지를 생성하는 작업
  • 큰텍스트 스위치 횟수를 줄이면 서버의 처리량에 어떤 변화가 있는지를 확인하기 위해 속도 비교하기
  • 방법 1 .대부분의 로그 출력 프레임워크는 sout 문장을 적당히 감싸고 있을 뿐이다.
  • 방법 2. LogWriter(예제 7.15) 사용
  • 두 개는 출력되는 로그 메시지의 양이나 로그 메시지를 몇 개의 스레드에서 출력하는지, 컨텍스트 스위치를 하는데 얼마만큼의 자우너이 필요한지에 따라 차이가 발생한다.
  • 로그 출력 기능에 걸리는 시간은 항상 I/O 스트림 클래스와 관련된 모든 작업을 포함한다.
  • 즉 I/O 연산이 대기 상태에 들어가면 해당 스레드가 대기중인 시간까지 전체 작업 시간에 포함된다.
  • 아니면 다수의 스레드가 동시다발적으로 로그 메시지를 출력하고자 한다면 메시지를 출력하고자 한다면 메시지를 출력하는 출력 스트림 객체에 대한 경쟁이 발생할 수 있다.
  • 이럴 경우 컨텍스트 스위치가 발생
  • 정리해보면 로그 메시지를 그 즉시 출력하는 방법은 I/O연산과 스트림에 대한 락에 직접적으로 연결되어 있으며, 따라서 컨텍스트 스위치가 빈번하게 발생할 가능성이 높고 서비스 시간은 늘어난다.
  • 요청을 처리하는 스레드의 외모로 I/O 작업을 뽑아내는 방법은 요청을 처리하는 평균 시간을 줄여주는 좋은 방법.
  • 작업이 실제로 처리되는 위치를 옮기고 있고, 사용자가 직접 그 속도를 느끼기 어려운 위치로 I/O작업을 이동시키면 대기상태에 들어갈 수 있는 원인 방지 가능

요약

  • 멀티 스레드를 사용하는 가장 큰 이유중에 하나가 다중 CPU 하드웨어를 충분히 활용하고자 하는것
  • 병렬 처리 애플리케이션의 성능에 대해 논의하면서 실제적인 서비스 시간보다는 애플리케이션의 데이터 처리량이나 확장성을 살펴봤다.
  • 암달의 법칙에 따르면 애플리케이션의 확장성은 반드시 순차적으로 실행돼야만 하는 코드가 전체에서 얼마만큼 차지하느냐에 달렸다.
  • 순차적처리 = 독점적 락 사용
  • 락으로 동기화하는 벙위를 세분화해 정밀도를 높이거나, 락을 확보하는 시간을 최소한으로 줄이는 기법을 사용해 락을 최소한만 사용해야 하낟.
  • 독점적인 락 대신 독점적이지 않은 방법을 사용하거나 대기 상태에 들어가지 않는 방법 사용ㅎ