멀티스레딩

Kyung yup Lee·2021년 2월 17일
0

운영체제

목록 보기
4/5

멀티스레딩

스레드

스레딩은 두가지 종류에서 실행될 수 있다. 스레딩이란 스레드를 통해 작업을 처리하는 방식을 말한다. 스레드에는 운영체제의 커널 프로세스에서 만들어지는 커널 스레드가 있고, 사용자 라이브러리를 통해 만들어지는 사용자 스레드가 있다. 한 마디로 자바 프로그램 안에서 스레드를 생성해서 작업 처리를 진행한다면 사용자 스레드를 생성해서 처리한 것이다.

멀티스레드 모델

커널 스레드(1:1)

운영체제의 커널을 통해 만들어지는 스레드이다. 커널 스레드 모델이라 함은 커널 스레드가 사용자 스레드와 1 vs 1로 매칭되는 스레딩 모델이다.

흔히들 CPU 를 구입할때 보는 2코어 4스레드 라는 명칭에서의 스레드가 커널 스레드이다. 즉 CPU 코어 하나에 2스레드가 있는 것으로, 하이퍼스레딩이라고 부른다. 하이퍼 스레딩은 기술적으로 운영체제가 관리하는 모든 프로세스를 병렬적으로 처리해야 하고, 이를 통한 부작용을 모두 컨트롤 할 수 있어야 하기 때문에 구현해내기 어려운 기술이었다.

그래서 기존에는 1코어 1스레드가 보편적인 CPU 사양이었고, 사용자 스레드를 멀티 스레딩으로 구현하는 것을 통해 성능을 높혔다.

사용자 스레드(1:N)

커널 스레드를 싱글 스레드로 밖에 사용할 수 없었기 때문에, 병렬처리를 하기 위해서는 프로세스 내부에 다중 스레드를 만들어내는 방법을 사용해야 했다. 사용자 스레드는 라이브러리나 개발자가 직접 병렬처리 및 부작용 관리를 하기 때문에, 운영체제의 개입이 필요없다. 사실 커널은 이 프로세스 내부가 멀티스레딩으로 돌고있는지 아닌지 관심이 없다. 커널은 프로세스를 프로세스 단위로 보기 때문에, 커널 스레드를 꽂아 놓기만 하면(CPU 할당) 알아서 프로세스가 멀티스레딩을 실행한다.

그렇기 때문에 사용자 스레드는 커널 스레드보다 오버헤드가 적다. 커널 스레드는 컨텍스트 스위칭이 일어날 때 반드시 커널 스케쥴러를 호출하고 PCB에 상태를 기록해야 하는 등 해야할 것이 많기 때문에 사용자 스레드보다 오버헤드가 많다. 그만큼 안정적인게 장점이다.

사용자 멀티스레딩 방식에 중대한 단점이 있는데, 바로 I/O 가 발생할 경우 모든 사용자 스레드가 중지되는 것이다. 이 이유는 커널 스레드는 I/O 가 발생할 경우 컨텍스트 스위칭이 되어 다른 프로세스로 옮겨가게 된다. 기존에 프로세스가 인터럽트가 발생하면 스케쥴링 방식에 따라 컨텍스트 스위칭이 된다고 공부했었다. 이 프로세스를 처리하고 있는 실질적인 CPU 단위가 커널 스레드였던 것이고, 마찬가지로 인터럽트가 발생하면 커널 스레드는 컨텍스트 스위칭 된다. 그렇게 되면 프로세스는 중지 상태에 들어가기 때문에 사용자 스레드는 모두 중지상태로 변환된다.

이런 단점이 있어서, 디스크 I/O 를 자주 사용하는 프로그램의 경우 최대한 디스크 I/O를 제한하거나 멀티스레딩을 커널 스레드를 통해 구현해야 한다.

멀티 레벨 스레드(N : M)

위의 커널 스레드 모델과 사용자 스레드 모델의 단점을 극복하기 위해 도입된 것이 멀티 레벨 스레드 모델이다. N to M 모델이라고 하는데, 다중 커널 스레드와 다중 사용자 스레드를 같이 사용하는 방식이다.

멀티 레벨 스레드 모델에서는 경량 프로세스(LWP : Light Weight Process) 라는 프로세스가 각 커널 스레드에 할당되게 된다. 또한 이 경량 프로세스는 여러 개의 사용자 스레드를 생성함으로서 멀티스레딩을 구현한다.

I/O 처리 등 시스템 콜을 통해 인터럽트가 발생하게 되더라도 다른 커널 스레드가 계속 작동하고 있으므로 프로세스가 중단 처리 되지 않고, 돌아가게 된다.

하지만 멀티 커널스레딩 만큼 LWP 를 관리하는데 드는 오버헤드가 크고, LWP에서 사용자 스레드가 한 개 이상 실행되지 않으면 오히려 더 비효율적일 수 있다는 것이 단점이다.

자바에서의 멀티스레딩

위에서 언급했듯이 프로세스 단위에서 멀티스레딩을 구현하는 것은 사용자 레벨의 스레딩이다. 때문에 운영체제는 이 스레드에 전혀 개입을 안하고, 이를 통해 발생하는 부작용들은 모두 프로그래머의 책임이다. 때문에 프로그램 수준에서의 멀티스레딩을 제어하는 것은 프로그래머의 수준을 측량하는 중요한 기준이 된다.

괜히 양날의 검이 아니다. 잘 사용하면 엄청난 성능 향상을 볼 수 있지만, 잘 못 사용하게 되면 엄청난 버그를 양산하게 된다.

문제

버그를 양산하는 대표적인 문제가 동기화(synchronize) 문제이다. 멀티스레딩의 성능향상의 비법은 비동기적으로 코드를 수행하는 데 있다. 하지만 이런 비동기성은 스레드가 공유자원에 동시 접근할 때 문제를 야기한다.

ex) 변수 a=1 의 값을 불러와서 1을 더하고 변수 a에 다시 그 값을 저장하는 코드가 있다. 두 스레드가 동시에 접근해서 a의 값을 불러와서 1을 더하고 저장했다고 가정하면, 코드는 두 번 작동했기 때문에 값은 3이 되어야 프로그래머의 예상과 맞다. 하지만 실제로는 a에 저장되어있는 값은 2이다. 왜냐하면 스레드가 동시에 실행 됐기 때문에, 같은 변수의 값을 불러와 같은 처리를 하고 같은 값을 저장했기 때문이다. 즉 순차 처리를 해야 하는데, 병렬처리를 해버림으로써 문제가 발생한 것이다.

이렇게 발생한 버그는 디버깅을 하기가 힘들다. 왜냐하면 스레드가 항상 같은 방식과 같은 타이밍에 해당 코드에 접근하지 않기 때문이다. 그러므로 결과가 실행할 때마다 다르게 나올 수 있다.

synchronized

멀티스레딩의 동시성 문제는 synchronized 키워드로 해결할 수 있다.

private Integer count = 0; 

synchronized(count) { 
	count ++; 
}

이렇게 synchronized 를 사용해서 코드를 작성하면 동시성 문제는 해결되지만 성능이 지나치게 감소해버린다.

왜냐하면 해당 코드에 접근하는 모든 쓰레드에 락을 걸어버리기 때문에 실제로 동시성 문제가 발생하지 않는다고 하더라도 락에 걸려서 다른 스레드가 코드를 처리하는 것을 대기해야 하기 때문이다.

이 문제를 해결하기 위해 적용할 수 있는 방법은 volatile 키워드와 atomic 변수를 사용하는 것이다. 위의 해결책들은 메모리 가시성, 메모리 장벽에 대한 개념의 이해가 필요하다.

메모리 가시성
메모리 가시성이란 cpu의 캐시 메모리와 메인 메모리의 값이 다른 경우를 말한다. 보통 메인메모리에서 값을 불러오면 시간적 지역성으로 인해 캐시 메모리에 저장되어있게 된다. 이 값을 참조할 경우 캐시 메모리에서 값을 불러 오게 되는데, 캐시 메모리와 메인 메모리의 값이 다른 경우가 있다. 이에 대한 이슈를 메모리 가시성이라고 부른다.

메모리 장벽
위의 메모리 가시성 이슈를 해결하기 위해 만드는 것이 메모리 장벽이다. 해당 메모리 장벽을 지나면서 cpu의 캐시메모리와 메인 메모리가 동기화 되게 된다. synchronized 키워드는 이 원리로 멀티스레딩 문제를 해결한다. 해당 synchronized 블록을 만나면 캐시메모리와 메인 메모리를 동기화 시키게 된다.

위의 내용을 보면 결국 멀티스레딩 문제의 원인은 메모리 가시성에 있다고 볼 수 있다. 내가 참조하는 캐시 메모리의 값이 메인 메모리의 값과 달라, 문제가 발생하는 것이다.

volatile, atomic 을 통해 어느정도 해결이 가능하다.

volatile

volatile 키워드는 이런 이슈를 해소하기 위해 캐시를 참조하지 않고 직접 메인메모리를 참조한다. 이를 통해 메모리 가시성 문제를 해결할 수 있지만, 캐시 메모리를 참조하지 않는 만큼 당연히 걸리는 시간은 느려질 것이고, 또한 volatile 키워드의 치명적인 단점은 오직 한개의 스레드에서 작업을 할 때 안정성을 보장한다는 것이다. 즉 두 스레드에서 읽기와 쓰기를 진행 할 경우 안정성이 깨진다는 것이다.
https://jusungpark.tistory.com/4 참고

atomic

synchronized 와 volatile 키워드의 단점을 모두 해결하려고 한 것이 atomic 변수이다. synchronized 는 블록 단위로 실행되기 때문에, 해당 블록에 접근하는 모든 스레드를 막아버리는 문제가 있었다. volatile은 다중 스레드의 경우 안정성이 깨지는 문제가 있었다.
atomic 변수는 해당 변수에 값을 쓰거나 읽으려고 할 때, 캐시 메모리와 메인 메모리를 동기화 해준다. 이를 통해 멀티스레딩 문제를 해결한다.

결론

긴 내용이었다. OS 내의 커널 스레드와 사용자 스레드의 차이점 부터 멀티스레딩의 내용까지 모두 정리했다. 깊이는 아직 부족한게 많지만 넓은 범위에서 멀티스레딩에 대한 내용은 모두 담으려고 했다. 나누면 간략해지지만 흐름이 깨진다.

출처
https://www.crocus.co.kr/1404
https://ko.wikipedia.org/wiki/%EC%8A%A4%EB%A0%88%EB%93%9C_(%EC%BB%B4%ED%93%A8%ED%8C%85)
https://sanghyunj.tistory.com/17
https://velog.io/@agugu95/%EC%9E%90%EB%B0%94-%EB%A9%80%ED%8B%B0%EC%8A%A4%EB%A0%88%EB%93%9C-%EB%AC%B8%EC%A0%9C
https://dingue.tistory.com/8

profile
성장하는 개발자

0개의 댓글