레이블이 multithreading인 게시물을 표시합니다. 모든 게시물 표시
레이블이 multithreading인 게시물을 표시합니다. 모든 게시물 표시

2018-12-27

read-writers lock - 공유 자원 접근하기

Multithreaded programming에서 공유 자원에 접근할 때는 동시에 두 개 이상의 스레드가 자원을 변경시키지 않기 위해서 mutex를 사용한다. Mutex를 사용하면 공유자원에 접근하는 스레드를 한 개로 제한하기 때문에 안전하지만, 어떤 경우는 비효율적이다. 예를 들어 여러 스레드가 공유 자원에 동시에 접근해야 하지만 그중 일부 스레드만 값을 변경하는 경우는 어떨까?

이런 경우 값을 읽기만 하는 스레드는 동시에 접근해도 상관없다. 하지만 어떤 스레드가 값을 변경하고 있으면, 다른 스레드는 공유 자원에 접근해서는 안 된다. 반대로 다른 스레드가 공유 자원에 접근하고 있는 중에는 값을 변경하는 스레드는 접근해서는 안 된다.

이때 사용하는 것이 shared-exclusive lock이라고도 하는 readers-writer lock이다. Readers-writer lock은 여러 개의 reader와 한 개의 writer를 허용한다. 그래서 multiple-readers/single-writer lock(MRSW lock)이라고도 불린다. 즉, 이미 read lock이 잡혀있는 readers-writer lock에 read lock을 잡으면 바로 lock이 잡히고 다음 코드를 실행할 수 있지만, write lock을 잡으면, lock을 잡지 못하고 read lock이 풀릴 때까지 기다린다.

그렇다면 A thread가 read lock을 잡고, B thread가 write lock을 잡은 뒤 C thread가 read lock을 잡으면 어떻게 될까? 단순하게 생각해보면 A thread가 read lock을 잡고 있으니 B thread의 write lock은 잡히지 못하고 기다리고, C thread의 read lock은 잡힐 수 있기 때문에, A threadC thread의 코드가 실행될 것이다. 하지만 이런 구현의 경우 read lock이 빈번하게 잡히는 코드라면, write lock이 영원히 실행되지 못할 수도 있다. 이 현상을 write-starvation이라고 부른다.

그래서 readers-write lock의 구현 중에서는 이미 read lock이 잡혀서 read lock을 잡을 수 있는 상황에서도 write lock이 기다리고 있으면 새 read lock은 잡지 않고, write lock이 잡히는 것을 기다리는 구현도 있다. 이 경우 불필요하게 read lock을 못 잡고 기다리는 경우도 있기 때문에 효율성은 떨어진다. 하지만 먼저 기다리기 시작한 lock이 먼저 잡히기 때문에 공정성은 올라간다.

Write-starvation이 발생할 수 있지만, read lock을 효율적으로 잡을 수 있는 구현을 read-preferring이라고 부른다. 반대로 공정하지만, 효율성이 약간 떨어지는 구현을 write-preferring이라고 부른다. 이 둘 중 어느 쪽이 좋은지는 상황에 따라 다르다. read lock을 빈번하게 잡았다 풀어 write-lock starvation이 발생할 수 있는 경우는 write-preferring lock이 좋다. 하지만 write-starvation이 발생할 걱정이 없는 경우는 read-preferring의 구현이 더 가볍고, 이미 read lock이 잡혀있는 MRSW lock에 대해서 같은 스레드에서 다시 한번 read lock을 잡을 수 있다.

어떤 구현은 이 둘 중 한 가지 정책을 사용한다고 명시하지만, 구현에 따라서는 어떤 정책을 사용하는지 명시하지 않는 경우도 있다. 어떤 정책인지 명시하지 않은 경우는 대부분 추후에 최적화할 여지를 남겨두기 위해서이기 때문에 이 경우 read-preferring이든 write-preferring이든 안전하도록 코드를 짜야 한다.

2018-03-11

2018년 10번째 주

 이 포스팅은 그냥 지난 한 주간 읽었던 것들을 정리하는 포스트입니다. 그냥 예전에 봤던 글 중 나중에 필요한데 뭐였는지 기억 안 나는 글들이 있어서 쓰기 시작했습니다.
 보통 하는 일과 관련된 글들이 올라오겠지만 딱히 정해둔 주제는 없고, 그때그때 관심 있었던 것을 읽었기 때문에 지난주에 쓰인 글일 수도 있고 몇 년 전에 쓰인 글일 수도 있습니다.

Slack Is Shutting Down Its IRC Gateway

 채팅 프로그램들은 누구나 겪는 문제 중 하나가 일부 사용자가 IRC 클라이언트를 포기하지 않는다는 것이다. Slack도 마찬가지였고, 이에 대해서 Slack은 IRC Gateway를 지원하여 IRC 사용자들도 Slack에 포함시키려는 노력을 하였다.

https://xkcd.com/1782/

 하지만 이제는 Slack을 IRC 클라이언트에서 사용할 수 없다. Slack이 IRC 서포트를 중지한다고 발표했다.
 모든 플랫폼에서 같은 사용자 경험을 주고 싶은데 IRC에서 지원하지 않는 기능들이 방해되기 때문이라고 한다. 하지만 이미 Slack에서 IRC Gateway를 사용하는 사람들은 그 한정된 기능에 적응하고 사용하고 있는 사람들일 텐데, 이 사람들을 포기하면서 같은 사용자 경험을 주는 것이 그리 중요한 일일까 싶다. 그보다는 IRC Gateway를 이용하는 사람들을 Slack이 유료로 파는 기능들을 사용하지 않기 때문이 아닐까 싶다.

Why RSS Still Beats Facebook and Twitter for Tracking News

 Facebook이나 Twitter 같은 SNS가 생긴 뒤로 많은 사람이 자신의 포스팅을 SNS에 올리기 시작했고, RSS(Rich Site Summary)는 이제 사용되지 않을 것으로 생각했다. 대표적인 RSS 구독 서비스였던 Google Reader가 2013년 서비스를 종료했던 것이 RSS의 죽음을 나타내는 가장 상징적인 사건이었다.
 하지만 사람들은 여전히 RSS를 포기하지 않았다. 5년이 지난 지금까지 사람들은 여전히 RSS를 사용하고 있다. RSS는 SNS가 가지지 못 하는 장점이 여러 개 있다.
 일단 RSS를 사용하면 내가 구독하고 있는 것을 못 보고 넘어갈 일이 없다. SNS는 기본적으로 최근 포스트를 우선으로 보여준다. 내가 매일 접속하여 꾸준히 보지 않으면, 놓치는 정보가 있을 수 있다.
 게다가 SNS에서 보여주는 정보는 내가 원하는 정보라고 확신할 수 없다. RSS는 내가 RSS Reader에 구독을 원하는 주소를 등록하는 방식이지만, SNS는 자신들이 추천하는 포스트를 나에게 보여준다. 나름대로의 추천 알고리즘을 통해 정보를 뿌려주지만 내가 원하는 글이라고 확신할 수 없다.
 마지막으로 RSS는 하나의 서비스만 사용하면 되는데, SNS는 종류별로 가입해야 한다. 글을 올리는 사람이 모든 SNS에 다 올리면 좋겠지만, 현실은 그렇지 않다. 모든 글을 구독하기 위해서는 모든 SNS에 가입해야 하고, 내가 원하는 글만 보는 것이 아니라 각 SNS가 추천하는 글도 다 보게 된다.
 이런 문제들을 해결할 방법이 나오기 전에는 RSS가 죽을 일은 없을 것 같다.

static local variable


 일단 C++11 이후. 즉, 모던 C++에서는 맞는 말이다. 그 이전에서는 멀티 스레드 프로그램에서는 싱글턴을 사용해야 했는데, 그런 경우가 있으면 2018년에 모던 C++이 아닌 상황에서 이미 망한 프로젝트니까 빠르게 도망치자.
 과거에, 그러니까 C++의 abstract machine이 스레드에 대해 고려를 하지 않았던 시절에는 여러 스레드에서 동시에 static local variable을 사용하는 것이 문제가 될 수 있었다. 하지만 모던 C++에서는 abstract machine에 스레드 개념이 들어갔고, 여러 스레드가 동시에 접근하더라도 static local variable이 단 한 번만 초기화돼야 한다고 명시됐으니 그냥 컴파일러를 믿고 사용하면 된다.

2016-03-07

[Android] AsyncTask - UI 스레드에서는 시간이 오래 걸리는 일을 하면 안 된다

 안드로이드는 메인 스레드에서 UI와 관련된 일을 처리한다. 그래서 메인 스레드를 UI 스레드라고 부르기도 한다. 그런데 UI 스레드에서 오랫동안 CPU를 점유하는 일을 하게 되면, "Application Not Responding"이라는 메시지가 나온다. 이러면 앱에 대한 컨트롤을 잃게 되며, 응답할 때까지 기다리거나 강제종료할 수밖에 없다.
 이런 일을 방지하기 위해서는 새로운 스레드를 생성하여 다른 스레드에 시켜야 한다. 하지만 로우레벨의 스레드를 바로 사용하면 동기화하는 과정에서 쉽게 실수할 수 있으므로 실수 없이 쉽게 스레드를 사용할 수 있게 하려고 안드로이드는 AsyncTask라는 클래스를 지원한다.

 AsyncTask는 UI 스레드에서 잠시 동안 백그라운드로 일을 호출하고 싶을 때 사용된다. 잠시 동안이라는 것을 문서상으로는 should ideally be used for short operations (a few seconds at the most.)1)라고 표현하고 있다. 만약 길게 실행되는 일을 백그라운드에서 실행하고 싶으면 스레드를 직접 사용하거나 다른 방법을 이용해야 한다. 그 이유는 몇 가지 있다.
 대표적인 문제는, AsyncTaskActivity에 종속되지 않기 때문에, AsyncTask를 호출했던 Activity가 먼저 죽었을 경우 처리가 복잡해진다.
 또 다른 문제는 백그라운드 스레드를 점유해버린다는 것이다. AsyncTaskAsyncThread가 새로 만들어질 때마다 스레드를 생성하지 않는다. 고정된 크기의 스레드 풀이 있고 이 스레드 풀을 모든 AsyncTask가 공유한다. API 버전에 따라서 1개의 태스크만 실행될 수도 있고, 여러 개의 태스크가 병렬적으로 동시에 수행될 수도 있는데, 최신 버전은 1개만 실행되며 모든 태스크는 순차적으로 실행하게 되어 있다. 따라서 어떤 태스크가 데 시간이 오래 걸린다면 다른 태스크가 실행되지 못한다.

 AsyncTask를 사용하는 대략적인 과정은 다음과 같다. AsyncTask를 상속받는 클래스를 만들어서 필요한 함수를 오버라이드한다. 그 뒤 UI 스레드에서 AsyncTask를 상속한 클래스의 객체를 생성하여 execute 메소드를 실행시킨다.
 AsyncTask는 3개의 타입 파라미터를 받는다. 각각은 순서대로 Params, Progress, Result라고 부른다. Params는 태스크를 실행시킬 때 넘기는 파라미터의 타입이고, Progress는 UI 스레드에 태스크의 진행 상황을 넘기기 위해서 사용되는 파라미터의 타입이고, Result는 태스크의 결과를 UI 스레드에 넘길 때 사용되는 타입이다.

 AsyncTask를 상속받는 클래스는 반드시 doInBackground 함수를 구현해야 한다. 이 함수는 태스크를 백그라운드에서 실행시킬 일을 의미한다. 이 함수는 자동으로 백그라운드 스레드에서 실행되며, 수동으로 실행시켜서는 안 된다. 또한 이는 백그라운드 스레드에서 실행되기 때문에 UI를 수정하면 안 된다. 중간에 UI를 수정하고 싶으면 publishProgress 함수를 호출하여 UI 스레드에서 콜백이 실행되도록 해야 한다.  그 외에 필요에 따라서 onPreExecute, onPostExecute, onProgressUpdate, onCancelled 함수를 구현할 수 있다. 각각 태스크가 수행되기 전, 수행된 후, publishProgress 함수가 불렸을 때, 태스크가 취소됐을 때 UI 스레드에서 호출된다. 위의 상황에서 UI를 변경시키고자 할때는 위의 함수들을 구현해서 사용해야 한다.


1) http://developer.android.com/intl/ko/reference/android/os/AsyncTask.html

2016-03-04

멀티 쓰레드 환경에서 fork는 조심해야 한다.

 리눅스나 유닉스 같은 POSIX 시스템에서는 fork를 이용해서 자신과 똑같은 프로세스를 만들 수 있다. 이때 fork를 호출한 프로세스를 부모 프로세스라고 하고, 새로 생성된 프로세스를 자식 프로세스라고 부르는데, 자식 프로세스는 부모 프로세스의 모든 메모리를 복사한다.

 fork는 그 뒤 exec을 해서 다른 바이너리를 실행시키는 fork-exec이 일반적인 사용법이다. 하지만 자식 프로세스부모 프로세스와 완전히 같은 메모리를 가지기 때문에, 스레드가 존재하지 않던 시절에는 exec을 하지 않고 병렬 처리를 하기 위해서도 자주 사용되었다. 지금은 스레드를 사용하는 것이 더 사용하기 쉽고 가벼운 방식이기에 스레드를 병렬처리를 위해 스레드를 주로 사용하지만, 스레드보다 서로 간에 독립적이기 때문에 일을 분리하기 위해서 사용하기도 한다.

 분명히 fork는 없어서는 안 될 기능이지만 fork에는 태생적 한계가 있다. 애초에 fork는 스레드라는 개념이 존재하지 않던 시절에 만들어졌기 때문에 thread-safe 하지 않다. 따라서 멀티스레드 환경에서 사용하려면 조심해서 사용해야 한다.

 fork가 멀티스레드 환경에서 문제를 일으키는 이유는 fork부모 프로세스의 메모리를 전부 복사하지만, fork를 호출한 스레드를 제외한 나머지 스레드들은 죽어버리기 때문이다. 따라서 fork를 호출한 스레드 이외의 스레드에서 획득한 자원은 아무것도 해제되지 않는다. 메모리 릭도 충분히 문제지만, 이는 단순한 메모리 릭만을 의미하지 않는다.  fork가 복사한 메모리에는 힙이나 스택 이외에 mutexcondition variable 들도 포함된다. 만약 mutexcondition variable이 다른 스레드에서 사용된 채로 fork 된다면 해당 변수로 보호되는 critical section에는 다시는 진입할 수 없게 된다. 특히나 malloc 같은 thread-safe 한 함수는 대부분 내부적으로 글로벌한 mutex를 사용하기 때문에 내 코드에 mutex가 없어도 안심할 수 없다. 따라서 멀티스레드 환경에서는 fork 하기 전에 다른 스레드가 전부 멈추는 것을 확인하는 것이 좋다.

 하지만 fork 후 바로 exec 하면 이런 것을 신경 쓸 필요 없다. exec의 경우 메모리를 완전히 새로 만들기 때문에 이런 문제가 발생하지 않는다.

2015-01-12

Global Interpreter Lock이란?

GIL이란?

 GIL이란 Global Interpreter Lock의 약자로 여러개의 쓰레드가 있을떄 쓰레드간의 동기화를 위해 사용되는 기술 중 하나이다. GIL은 전역에 lock을 두고 이 lock을 점유해야만 코드를 실행할 수 있도록 제한한다. 따라서 동시에 하나 이상의 쓰레드가 실행되지 않는다. 예를 들어 아래 그림과 같은 3개의 쓰레드가 분산해서 일을 처리하게 될 때도 실제로 CPU를 점유할 수 있는 thread는 한 개뿐이다. 따라서 실제로 사용하는 코어는 하나뿐이라는 것이다.
싱글 코어 컴퓨터에서 multi thread program을 실행하는 모습 같다.

GIL의 efficiency

 직관적으로 멀티코어에서도 코어를 하나밖에 사용 못 한다면 GIL을 사용해서 multi threads를 지원하는 것은 성능에 큰 문제가 있을 거라고 생각된다. 하지만 이는 대부분의 경우에 큰 문제가 되지 않는다. 정확히 말해서 프로그램은 대부분 I/O bound이기 때문에 문제가 되지 않는다. I/O bound의 경우 대부분 시간을 I/O event를 기다리는 데 사용하기 때문에 event를 기다리는 동안 다른 thread가 CPU를 사용하면 된다.
 반대로 말해서 프로그램이 CPU bound인 경우에는 multi-threaded program을 작성해도 성능이 향상되지 않는다. 오히려 lock을 acquire하고 release하는 시간 때문에 성능이 떨어지기도 한다.

GIL의 장점

 멀티 쓰레드 프로그램에서 성능이 떨어질 수도 있지만, CPython, PyPy, Ruby MRI, RubiniusLua interpreter 등 많은 인터프리터 구현체들이 GIL을 사용하고 있다. 그 이유는 우선 GIL을 이용한 multi-threads를 구현하는 것이 parallel 한 multi-threads를 구현하는 것보다 훨씬 쉽다는 것이다.
 게다가 이런 parallel 한 multi-threads 구현체들의 문제는 싱글 쓰레드에서 오히려 더 느려진다는 것이다. 그래서 CPython이나 Ruby MRI에서는GIL을 없애려는 많은 시도가 있었지만, 결국 싱글 쓰레드에서의 성능 저하를 극복하지 못하고 GIL로 돌아왔다. 결국, 파이썬의 창시자인 귀도 반 로섬은 CPython에서 GIL을 없애는 대신 싱글 쓰레드에서 성능을 떨어뜨릴 구현은 받아들이지 않겠다고 선언하기도 했다. 1)

1) https://wiki.python.org/moin/GlobalInterpreterLock#line-31