본문 바로가기
프로그래밍 놀이터/안드로이드, Java

[Java Concurrency] 활동성 최대로 높이기 #2

by 돼지왕 왕돼지 2017. 5. 2.
반응형

 [Java Concurrency] 활동성 최대로 높이기 #2


10단계 스레드 우선 순위, CPU, gui 앱, java concurrency, jvm, livelock, LOCK, open call, OS, priority, stack trace, StackTrace, Starvation, synchronized, thread dump, thread.sleep, thread.yield, trylock, [Java Concurrency] 활동성 최대로 높이기 #2, 고려, 규칙적이지 않은 구조, 기록, 놓친 신호, 대기 상태 그래프, 데드락, 데드락 방지, 데드락 복구, 데드락 분석, 라이브 락, 라이브락, 락, 락 사용 순서, 락 순서, 락 확보 실패, 명시적 락, 명시적인 락, 문서, 문서화, 백그라운드 스레드, 병렬 앱, 병렬 프로그램, 사이클, 설계, 소모, 소스코드 분석, 스레드, 스레드 stack trace, 스레드 덤프, 스레드 우선순위, 스케줄링, 스케쥴링, 스택 프레임, 심각, 심각성 원인, 암묵적 락, 암묵적인 락, 암물적인 락, 에러, 오픈 호출, 우선 순위, 운영체제, 원인 추적, 이더넷, 임의의 시간, 자바 병렬, 자바5, 자바6, 자원 할당, 재시도, 정적 분석, 타임아웃, 플랫폼, 플랫폼 종속, 형편 없는 응답성, 활동성, 활동성 최대로 높이기, 회복 불가능한 오류, 힌트


10.2. 데드락 방지 및 원인 추적


-
한 번에 하나 이상의 락을 사용하지 않는 프로그램은 락의 순서에 의한 데드락이 발생하지 않는다.
물론 그다지 실용적이지 않은 방법일 수 있지만,
가능하다면 한 번에 하나 이상의 락을 사용하지 않도록 프로그램을 만들어 보는 것도 좋다.


-
여러 개의 락을 사용해야만 한다면 락을 사용하는 순서 역시 설계 단계부터 충분히 고려해야 한다.
설계 과정에서 여러 개의 락이 서로 함께 동작하는 부분을 최대한 줄이고, 락의 순서를 지정하는 규칙을 정해 문서로 남기고 그 규칙을 정확하게 따라서 프로그램을 작성해야 한다.


-
세세한 수준에서 락을 관리하는 프로그램에서는 두 단계의 전략으로 데드락 발생 가능성이 없는지를 확인해보자.
첫번째 단계는 여러 개의 락을 확보해야 하는 부분이 어디인지를 찾아내는 단계이고,
그 다음으로 이와 같은 부분에 대한 전반적인 분석 작업을 진행해 프로그램 어디에서건 락을 지정된 순서에 맞춰 사용하도록 해야 한다.

가능한 부분에서는 최대한 오픈 호출 방법을 사용하면 이와 같은 분석과 확인 작업이 조금 간편해 질 수 있다.
오픈 호출을 사용하지 않는 경우라면 코드 리뷰 과정을 거치거나 자동화된 방법으로 바이트 코드나 소스코드를 분석하는 방법으로 여러 개의 락을 사용하는 부분을 뽑아 낼 수 있다.



* 10.2.1. 락의 시간 제한

-
데드락 상태를 검출하고 데드락에서 복구하는 또 다른 방법으로는 synchronized 등의 구문으로 암묵적인 락을 사용하는 대신 Lock 클래스의 메소드 가운데 시간을 제한할 수 있는 tryLock 메소드를 사용하는 방법이 있다.
암묵적인 락은 락을 확보할 때까지 영원히 기다리지만, Lock 클래스 등의 명시적인 락은 일정시간을 정해두고 그 시간동안 락을 확보하지 못한다면 tryLock 메소드가 오류를 발생시키도록 할 수 있다.
락을 확보하는 데 걸릴 것이라고 예상되는 시간보다 훨씬 큰 값을 타임아웃으로 해두고 tryLock 을 호출하면,
뭔가 일반적이지 않은 상황이 발생했을 때 제어권을 되돌려 받을 수 있다.


-
명시적인 락을 사용하면 락을 확보하려고 했지만 실패했다는 사실을 기록해 둘 기회를 갖는 셈이고,
그동안 발생했던 내용을 로그 파일로 남길 수도 있다.


-
여러 개이 락을 확보할 떄 이와 같이 타임아웃을 지정하는 방법을 적용하면, 
프로그램 전체에서 모두 타임아웃을 사용하지 않는다 해도 데드락을 방지하는 데 효과를 볼 수 있다.
락을 확보하려는 시점에서 시간 제한이 걸리면 이미 확보했던 락을 풀어주고 잠시 기다리다가 다시 작업을 시도해 볼 수 있다.



* 10.2.2 스레드 덤프를 활용한 데드락 분석

-
데드락을 방지하는 것이 프로그램을 작성할 때 최우선 목표겠지만,
일단 데드락이 발생했을 때는 JVM 이 만들어 내는 스레드 덤프(thread dump)를 활용해 데드락이 발생한 위치를 확인하는 데 도움을 얻을 수 있다.
스레드 덤프에는 실행 중인 모든 스레드의 stack trace 가 담겨 있다.
스레드 덤프에는 락과 관련된 정보도 담겨 있는데,
각 스레드마다 어떤 락을 확보하고 있는지, 스택의 어느 부분에서 락을 확보했는지,
그리고 대기 중인 스레드가 어느 락을 확보하려고 대기 중이었는지 등에 대한 정보를 갖고 있다.


-
JVM 은 스레드 덤프를 생성하기 전에 락 대기 상태 그래프에서 사이클이 발생했는지,
즉 데드락이 발생한 부분이 있는지 확인한다.
만약 데드락이 있었다고 판단되면 어느 락과 어느 스레드가 데드락에 관련하고 있는지,
프로그램 내부의 어느 부분에서락 확보 규칙을 깨고 있는지에 대한 정보도 스레드 덤프에 포함시킨다.


-
암묵적인 락 대신 명시적으로 Lock 클래스를 사용하고 있을 때,
자바 5.0 버전에서는 해당 Lock 에 지정된 정보는 스레드 덤프에 포함시키지 않는다.

하지만 자바 6에서는 명시적인 Lock 을 사용해도 스레드 덤프에 포함될 뿐 아니라
데드락을 검출할 때 명시적인 락을 포함하는 데드락도 검출해준다.

하지만 락을 어디에서 확보했는지에 대해 출력되는 정보는 암묵적인 락에 대한 내용만큼 정확하지는 않다.
암묵적인 락은 락을 확보하는 시점의 스택 프레임에 연결돼 있지만,
명시적인 락은 락을 확보한 스레드와 연결돼 있기 때문이다.



10.3. 그 밖의 활동성 문제점


-
프로그램이 동작하는 데 활동성을 떨어뜨리는 주된 원인은 역시 데드락이지만,
병렬 프로그램을 작성하다 보면 소모(starvation), 놓친 신호, 라이브락( livelock ) 등과 같이 다양한 원인을 만나게 된다.


* 10.3.1. 소모

-
소모(starvation) 상태는 스레드가 작업을 진행하는 데 꼭 필요한 자원을 영영 할당받지 못하는 경우에 발생한다.
소모 상태를 일으키는 가장 흔한 원인은 바로 CPU 이다.
자바 앱에서 소모 상황이 발생하는 원인은 대부분 스레드의 우선 순위(priority) 를 적절치 못하게 올리거나 내리는 부분에 있다.


-
자바의 스레드 관련 API 에서 제공하는 우선 순위 개념은 단지 스레드 스케줄링과 관련된 약간의 힌트를 제공하는 것뿐이다.
자바의 스레드 API 는 총 10단계의 스레드 우선 순위를 지정할 수 있도록 돼 있으며,
JVM 은 지정된 10단계의 우선 순위를 하위 운영체제의 스케줄링 우선 순위에 적절히 대응시키는 정도로만 사용한다.
이처럼 자바 스레드 우선 순위를 운영체제의 우선 순위에 대응시키는 기능은 플랫폼마다 다르게 적용되기 때문에
특정 시스템에서는 두 개의 스레드 우선 순위가 같은 값으로 지정될 수도 있고,
다른 운영체제에서는 또 다른 값으로 지정될 수 있다.
일부운영체제는 10보다 작은 개수의 스레드 우선 순위를 제공하는 경우도 있으며,
이럴 때는 어쩔 수 없이 두 개 이상의 자바 스레드 우선 순위가 같은 값의 운영체제 스레드 우선 순위로 대응되는 수밖에 없다.


-
스레드 우선순위라는 것이 약간 무뎌 보일 뿐더러 우선 순위를 변경했다고 해서 그 효과가 뚜렷하게 나타나지 않는 경우도 많다.
스레드의 우선 순위를 위로 올린다고 해도 아무런 변화가 없거나,
아니면 우선 순위가 높은 스레드만 우선적으로 실행시켜 다른 스레드가 제대로 실행되지 못하게 될 수도 있다.


-
일반적인 상황에서는 스레드 우선 순위를 변경하지 않고 그대로 사용하는 방법이 가장 현명하다고 할 수 있다.


-
약간 이해할 수 없는 위치에서 Thread.sleep 메소드나 Thread.yield 메소드를 호출해 우선 순위가 낮은 스레드에게 실행할 기회를 주려는 부분이 있는지를 찾아보면 우선 순위를 원상 복귀시키거나 기타 다른 응답성 문제를 해소해야 할 프로그램인지를 쉽게 가려낼 수 있다.


-
스레드 우선 순위를 변경하고 싶다 해도 꾹 참아라.
우선 순위를 변경하고 나면 플랫폼에 종속적인 부분이 많아지며,
따라서 활동성 문제를 일으키기 쉽다.
대부분의 병렬 앱은 모든 스레드의 우선 순위에 기본값을 사용하고 있다.



* 10.3.2. 형편 없는 응답성

-
응답성이 떨어지는 경우는 백그라운드 스레드를 사용하는 GUI 앱에서 굉장히 일상적이다.


-
앱의 응답성이 떨어진다면 락을 제대로 관리하지 못하는 것이 원인일 수 있다.



* 10.3.3. 라이브락

-
라이브락(livelock) 도 일종의 활동성 문제 가운데 하나인데,
대기 중인 상태가 아니었다 해도 특정 작업의 결과를 받아와야 다음 단계로 넘어갈 수 있는 작업이 실패할 수밖에 없는 기능을 계속해서 재시도하는 경우에 쉽게 찾아 볼 수 있다.

에러를 너무 완벽하게 처리하고자 회복 불가능한 오류를 회복 가능하다고 판단해 계속해서 재시도하는 과정에 나타난다.


-
라이브락은 여러 스레드가 함께 동작하는 환경에서 각 스레드가 다른 스레드의 응답에 따라 각자의 상태를 계속해서 변경하느라 실제 작업은 전혀 진전시키지 못하는 경우에 발생하기도 한다.
( 길에서 마주친 두 사람이 서로 길을 비켜줌 )

이 형태의 라이브락을 해결하려면 작업을 재시도하는 부분에 약간의 규칙적이지 않은 구조를 넣어두면 된다.

임의의 시간 동안 기다리다가 재시도하는 방법은 이더넷뿐 아니라 일반적인 병렬 프로그램에서도 라이브락을 방지하고자 할 때 사용할 수 있는 훌륭한 해결방법이다.



Summary


활동성과 관련된 문제는 심각한 경우가 많은데,
활동성 문제를 해결하려면 일반적으로 앱을 종료하는 것 외에는 별다른 방법이 없다는 데 심각성의 원인이 있다.
가장 흔한 형태의 활동성 문제는 바로 락 순서에 의한 데드락이다.

락 순서에 의한 데드락을 방지하려면 앱을 설계하는 단계부터 여러 개의 락을 사용하는 부분에 대해 충분히 고려해야 한다.
앱 내부의 스레드에서 두 개 이상의 락을 한꺼번에 사용해야 하는 부분이 있다면,
항상 일정한 순서를 두고 여러 개의 락을 확보해야만 한다.
이런 문제에 대한 가장 효과적인 해결 방법은 항상 오픈 호출 방법을 사용해 메소드를 호출하는 것이다.
오픈 호출을 사용하면 한 번에 여러 개의 락을 사용하는 경우를 엄청나게 줄일 수 있고,
따라서 여러 개의 락을 사용하는 부분이 어디인지 쉽게 찾아낼 수 있다.





반응형

댓글