|
| 1 | +# 동시성 |
| 2 | +##### 객체는 처리의 추상화. 스레드는 일정의 추상화 |
| 3 | + |
| 4 | +## 동시성이 필요한 이유? |
| 5 | +#### 동시성은 결합을 없애는 전략. 즉, what와 when을 분리하는 전략 |
| 6 | +### 미신과 오해 |
| 7 | + - 동시성은 항상 성능을 높여준다? |
| 8 | + > 동시성은 '때로' 성능을 높여준다. 대기시간이 아주 길어 여러 스레드가 프로세서를 공유할 수 있거나, 여러 프로세서가 동시에 처리할 독립적인 계산이 충분히 많은 경우에만 성능이 높아진다. |
| 9 | + - 동시성을 구현해도 설계는 변하지 않는다? |
| 10 | + > 크게 달라진다. |
| 11 | + - 웹 또는 EJB컨테이너를 사용하면 동시성을 이해할 필요가 없다? |
| 12 | + > 알아야한다. |
| 13 | + |
| 14 | + ### 타당한 생각 몇가지 |
| 15 | + - 동시성은 다소 부하를 유발한다. |
| 16 | + - 동시성은 복잡하다 |
| 17 | + - 일반적으로 동시성 버그는 재현하기 어렵다. |
| 18 | + - 동시성을 구현하려면 흔히 근본적인 설계 전략을 재고해야 한다. |
| 19 | + |
| 20 | + ## 난관 |
| 21 | + ``` |
| 22 | + public class X { |
| 23 | + private int lastIdUsed; |
| 24 | + |
| 25 | + public int getNextId() { |
| 26 | + return ++lastIdUsed; |
| 27 | + } |
| 28 | + } |
| 29 | + ``` |
| 30 | + > 인스턴스 X를 생성, lastIdUsed = 42; 두 스레드가 해당 인스턴스를 공유, 두 스레드가 getNextId(); 호출 |
| 31 | + 결과는? -> |
| 32 | + - case1) A 스레드는 43을 받는다. B 스레드는 44를 받는다. lastIdUsed는 44가 된다. |
| 33 | + - case2) A 스레드는 44를 받는다. B 스레드는 43을 받는다. lastIdUsed는 44가 된다. |
| 34 | + - case3) 한 스레드는 43을 받는다. 다른스레드는 43을 받는다. lastIdUsed는 43이 된다. |
| 35 | + #### -> 일부 잘못된 결과를 내놓는다. 경로의 갯수는? JIT(Just-In-TIme) 컴파일러가 바이트 코드를 처리하는 방식과 자바 메모리 모델이 원자로 간주하는 최소단위를 알아야함. 간략하게는 잠재적인 경로는 최대 12,870개. 데이터 타입을 int에서 long으로 바꾼다면? 경로는 2,704,156개 |
| 36 | + |
| 37 | +## 동시성 방어 원칙 |
| 38 | +### 단일 책임 원칙 SRP ( Single Responsibility Principle ) |
| 39 | +> SRP? ( "주어진 메서드/클래스/컴포넌트를 변경할 이유가 하나여야한다" 는 원칙) |
| 40 | +동시성은 복잡성 하나만으로도 따로 분리할 이유가 충분함. |
| 41 | +동시성을 구현할 때는 다름 몇 가지를 고려하자 |
| 42 | + - 동시성 코드는 독자적인 개발, 변경, 조율 주기가 있다. |
| 43 | + - 동시성 코드에는 독자적인 난관이 있다. 다른 코드에서 겪는 난관과 다르며 훨씬 어렵다. |
| 44 | + - 잘못 구현한 동시성 코드는 별의별 방식으로 실패한다. 주변에 있는 다른 코드가 발목을 잡지 않더라도 동시성 하나만으로도 충분히 어렵다. |
| 45 | + > 권장사항: 동시성 코드는 다른 코드와 분리하라. |
| 46 | + |
| 47 | + ### 따름 정리(corollary): 자료 범위를 제한하라. |
| 48 | + > 앞에서의 예제와 같은일이 벌어지지 않게 하기위해서는 임계영역(critical section)을 만들어야함. 공유 자료를 수정하는 위치가 많을수록 다음 가능성도 커진다. |
| 49 | + - 보호할 임계영역을 빼먹는다. 그래서 공유 자료를 수정하는 모든 코드를 망가뜨린다. |
| 50 | + - 모든 임계영역을 올바로 보호했는지(DRY 위반) 확인하느라 똑같은 노력과 수고를 반복한다. |
| 51 | + - 그렇지 않아도 찾아내기 어려운 버그가 더욱 찾기 어려워진다. |
| 52 | + |
| 53 | + ### 따름정리: 자료 사본을 사용하라 |
| 54 | + > 공유 자료를 줄이려변 처음부터 공유하지 않는 방법이 가장 좋다. 예를들면 데이터를 복사해서 쓴다던가.. 만약 데이터를 복사함으로 생기는 리소스가 걱정된다면? 정말 복사함으로써 잃는 리소스와 공유자료를 사용하면서 생길 수 있는 리스크중 리소스가 더 아까운지 한번 더 생각을 해보자. |
| 55 | + |
| 56 | + ### 따름정리: 스레드는 가능한 독립적으로 구현하라. |
| 57 | + > 자신만의 세상에 존재하는 스레드를 구현한다. 즉, 다른 스레드와 자료를 공유하지 않는다. |
| 58 | + > 권장사항: 독자적인 스레드로, 가능하면 다른 프로세서에서, 돌려도 괜찮도록 자료를 독립적인 단위로 분할하라. |
| 59 | + |
| 60 | + |
| 61 | + ## 라이브러리를 이해하라 |
| 62 | + ##### 자바 5로 스레드 코드를 구현한다면 다음을 고려하라 |
| 63 | + - 스레드 환경에 안전한 컬렉션을 사용한다. (자바 5부터 제공) |
| 64 | + - 서로 무관한 작업을 수행할 때는 executor 프레임워크를 사용한다. |
| 65 | + - 가능하다면 스레드가 차단(blocking)되지 않는 방법을 사용한다. |
| 66 | + - 일부 클래스 라이브러리는 스레드에 안전하지 못하다. |
| 67 | + |
| 68 | + ### 스레드 환경에서 안전한 컬렉션 |
| 69 | + -> 현기에 알아서 찾아라~! 여기에 기입해놓고 나중에 읽을것같으면 수정하삼 |
| 70 | + |
| 71 | +## 실행 모델을 이해하라 |
| 72 | + ##### 다중 스레드 어플리케이션을 분류하는 방식은 여러 가지이다. |
| 73 | + - 한정된 자원(Bound Resource): 다중 스레드 환경에서 사용하는 자원으로, 크기나 숫자가 제한적이다. 데이터베이스 연결, 길이가 일정한 읽기/쓰기 버퍼등이 대표적 |
| 74 | + - 상호 배제(Mutual Exclusion): 한 번에 한 스레드만 공유 자료나 공유 자원을 사용할 수 있는 경우를 가리킨다. |
| 75 | + - 기아(Stavation): 한 스레드나 여러 스레드가 괸장히 오랫동안 혹은 영원히 자원을 기다린다. 예를들어, 항상 짧은 스레드에게 우선순위를 준다면, 짧은 스레드가 지속적으로 이어질 경우, 긴 스레드가 기아상태에 빠진다. |
| 76 | + - 데드락(Deadlock): 여러 스레드가 서로가 끝나기를 기다린다. 모든 스레드가 각기 필요한 자원을 다른 스레드가 점유하는 바람에 어느 쪽도 더 이상 진핸하지 못한다. |
| 77 | + - 라이브락(Livelock): 락을 거는 단계에서 각 스레드가 서로를 방해한다. 스레드는 계속해서 진행하려 하지만, 공명(resonance)으로 인해, 굉장히 오랫동안 혹은 영원히 진행하지 못한다. |
| 78 | + |
| 79 | + ### 생상자-소비자(Producer-Consumer) |
| 80 | + > 생산자가 정보를 생산하면 버퍼(buffer)나, 대기열(queue)에 넣는다. 그러면 소비자가 버퍼or 대기열(이하 버퍼)을 확인하고 정보가 있으면 사용한다. |
| 81 | + > 생산자는 버퍼에 빈공간이 있어야 정보를 생산, 주입(?)하고 소비자는 버퍼에 정보가 있어야 해당 버퍼에 접근을 해 정보를 사용한다. |
| 82 | + > 생산자는 정보를 생성하면 소비자에게 버퍼에 정보가 있다는것을 알리고, 소비자는 정보를 가져와 버퍼에 공간이 있다면 생산자에게 빈공간이 있다는것을 알린다(시그널). |
| 83 | + > 따라서 잘못하면 생산자 스레드와 소비자 스레드가 둘 다 진행 가능함에도 불구하고 동시에 서로에게서 시그널을 기다릴 가능성이 존재한다. |
| 84 | + |
| 85 | + ### 읽기-쓰기(Readers-Wirters) |
| 86 | + > 쓰기 스레드가 공유자원을 갱신하는 경우 -> 처리율(throughput)이 문제의 핵심, 처리율을 강조하면 기아(starvation)현상이 생기거나 오래된 정보가 쌓임 |
| 87 | + > 쓰기 스레드가 자료를 갱신할때 읽기스레드가 접근하지 못하게 하거나, 읽기스레드가 읽을때 쓰기스레드가 접근하지 못하게 한다면 복잡한 균형잡기가 필요함. 처리율이 떨어짐. |
| 88 | + |
| 89 | + ### 식사하는 철학자들(Dining Philosophers) |
| 90 | + > 원탁에 철학자들이 앉아있음. 철학자들 왼쪽에는 포크가 있고, 원탁 중앙에는 스파게티가 있음.철학자들은 배가고프지 않다면 생각을하고, 배가고프면 양손에 포크를 들고 스파게티를 먹는다. 양손에 포크가 없다면 스파게티를 먹을 수 없다. |
| 91 | + > 자신의 왼쪽이나 오른쪽 철학자가 포크를 들고 스파게티를 먹을때는 양손에 포크를 들 수 없기때문에 배고파도 포크를 기다려야한다. |
| 92 | + > 여기서 철학자를 스레드로, 포크를 자원으로 바꿔서 생각해보자. |
| 93 | + |
| 94 | + ### 권장사항: 위에서 설명한 기본 알고리즘과 각 해법을 이해하라. |
| 95 | + |
| 96 | +## 동기화하는 메서드 사이에 존재하는 의존성을 이해하라 |
| 97 | + 동기화하는 메서드 사이에 의존성이 존재하면 동시성 코드에 찾아내기 어려운 버그가 생긴다. |
| 98 | + > 권장사항: 공유 객체 하나에는 메서드 하나만 사용하라. |
| 99 | + 공유 객체 하나에 여러 메서드가 필요한 상황도 생긴다. 그럴 때는 다음 세 가지 방법을 고려한다. |
| 100 | + - 클라이언트에서 잠금 - 클라이언트에서 첫 번째 메서드를 호출하기 전에 서버를 잠근다. 마지막 메서드를 호출할 때까지 잠금을 유지한다. |
| 101 | + - 서버에서 잠금 - 서버에다 "서버를 잠그고 모든 메서드를 호출한 후 잠금을 해제하는" 메서드를 구현한다. 클라이언트는 이 메서드를 호출한다. |
| 102 | + - 연결(Adapted 서버) - 잠금을 수행하는 중간 단계를 생성한다. '서버에서 잠금' 방식과 유사하지만 원래 서버는 변경하지 않는다. |
| 103 | + |
| 104 | + ## 동기화 하는 부분을 작게 만들어라 |
| 105 | + 자바에서 synchronized키워드를 사용하면 락을 설정한다. 같은 락으로 감싼 모든 코드 영역은 한 번에 한 스레드만 실행이 가능하다. 락은 스레드를 지연시키고 부하를 가중시킨다. -> synchronized를 남발하지 말자. 하지만 임계영역은 반드시 보호해야함으로, 코드를 짤 때 임계영역 수를 최대한 줄여야 한다. |
| 106 | + > 권장사항: 동기화하는 부분을 최대한 작게 만들어라. |
| 107 | + |
| 108 | + ## 올바른 종료 코드는 구현하기 어렵다 |
| 109 | + 깔끔하게 종료하는 코드는 올바로 구현하기 어렵다. 가장 흔한것은 데드락. 깔끔하게 종료하는 다중 스레드 코드를 짜야한다면 시간을 투자해 올바로 구현하기를 바란다. |
| 110 | + > 권장사항: 종료 코드를 개발 초기부터 고민하고 동작하게 초기부터 구현하라. 생각보다 오래 걸린다. 생각보다 어려우므로 이미 나온 알고리즘을 검토하라. |
| 111 | + |
| 112 | + ## 스레드 코드 테스트하기 |
| 113 | + 코드가 올바르다고 증명하기는 현실적으로 불가능하다. 그럼에도 충분한 테스트는 위험을 낮춘다. |
| 114 | + > 권장사항: 문제를 노출하는 테스트 케이스를 작성하라. 프로그램 설정과 시스템 설정과 부하를 바꿔가며 자주 돌려라. 테스트가 실패하면 원인을 추적하라. 다시 돌렸더니 통과하더라는 이유로 그냥 넘어가면 절대로 안 된다. |
| 115 | + |
| 116 | + - 말이 안 되는 실패는 잠정적인 스레드 문제로 취급하라. |
| 117 | + > 재현하기 매우 어렵다. 하지만 일회성 문제를 계속 무시한다면 잘못된 코드위에 코드가 계속 쌓인다. |
| 118 | + > 권장사항: 시스템 실패를 일회성이라 치부하지 마라. |
| 119 | + |
| 120 | + - 다중 스레드를 고려하지 않은 순차 코드부터 제대로 돌게 만들자. |
| 121 | + > 스레드 환경 밖에서 코드가 제대로 도는지 반드시 확인한다. 일반적인 방법으로, 스레드가 호출하는 POJO를 만듬. |
| 122 | + > 권장사항: 스레드 환경 밖에서 생기는 버그와 스레드 환경에서 생기는 버그를 동시에 디버깅하지 마라. 먼저 스레드 환경 밖에서 코드를 올바로 돌려라. |
| 123 | + |
| 124 | + - 다중 스레드를 쓰는 코드 부분을 다양한 환경에 쉽게 끼워 넣을 수 있게 스레드 코드를 구현하라 |
| 125 | + > 다중 스레드를 쓰는 코드를 다양한 설정으로 실행하기 쉽게 구현하라 |
| 126 | + > 한 스레드로 실행하거나, 여러 스레드로 실행하거나, 실행 중 스레드 수를 바꿔본다. |
| 127 | + > 스레드 코드를 실제 환경이나 테스트 환경에서 돌려본다. |
| 128 | + > 테스트 코드를 빨리, 천천히, 다양한 속도로 돌려본다. |
| 129 | + > 반복 테스트가 가능하도록 테스트 케이스를 작성한다. |
| 130 | + > 권장사항: 다양한 설정에서 실행할 목적으로 다른 환경에 쉽게 끼워넣을 수 있게 코드를 작성하라. |
| 131 | + |
| 132 | + - 다중 스레드를 쓰는 코드 부분을 상황에 맞춰 조정할 수 있게 작성하라. |
| 133 | + |
| 134 | + - 프로세서 수보다 많은 스레드를 돌려보라. |
| 135 | + > 스와핑(swapping)할 때도 문제가 발생한다. 스와핑이 잦을 수록 임계영역을 빼먹은 코드나 데드락을 일으키는 코드를 찾기 쉬워진다. |
| 136 | + |
| 137 | + - 다른 플랫폼에서 돌려보라. |
| 138 | + |
| 139 | + - 코드에 보조 코드를 넣어 볼려라. 강제로 실패를 일으키게 해보라. |
| 140 | + > 보조코드를 추가해 코드가 실행되는 순서를 바꿔준다 ex) Object.wait(), Object.sleep() ~ |
| 141 | + > 코드에 보조코드를 추가하는 방법은 두가지. 1) 직접 구현하기 2) 자동화 |
| 142 | + > 보조코드를 직접 구현하기는 문제가 많으니 자동화를 사용하는걸 추천. (AOF, CGLIB, ASM~~) |
| 143 | + > 권장사항: 흔들기 기법을 사용해 오류를 찾아내라. |
| 144 | + |
| 145 | + |
| 146 | + |
| 147 | + |
| 148 | + |
| 149 | + |
| 150 | + |
| 151 | + |
| 152 | + |
| 153 | + |
| 154 | + |
0 commit comments