안녕하세요! 제이븐입니다😁
저번에 포스팅한 프로레스와 스레드에 이어서 이번 주제는
멀티 스레드 환경에서 스레드가 공유 자원에 접근 할때 어떻게 안전하게 공유자원을 사용할 수 있는지
그리고 Thread Safety의 특성을 지키기 위한 Lock에 대해서 알아보도록 하겠습니다.
Thread-Safety란 멀티스레드 환경에서 공유 자원에 동시에 접근할 때도 프로그램의 동작이 예측 가능하고 올바르게 작동하는 특성입니다. 멀티스레드 환경에서 공통된 자원에 여러 스레드가 접근하게 되면 데이터 일관성에 문제가 발생할 수 있는데 Thread-Safety는 이런 문제를 방지합니다.
반대로는 Thread-Safety 하지 않는다면 여러 스레드가 공유 자원에 동시 접근할 때 오류가 발생할 수 있고 멀티 프로세스는 하나의 프로세스가 문제가 있더라도 다른 프로세스에 영향을 주지 않지만 스레드는 하나의 스레드에 문제가 생긴다면 다른 스레드에도 영향이 있기 때문에 (앱, 프로그램)이 죽게 됩니다.
공유 자원(Shared Resource)에 대해서 하나의 스레드만 접근할 수 있도록하는 메커니즘입니다. 상호 배제는 데이터 불일치나 레이스 컨디션(Data Race)를 방지합니다.
상호 배제는 Lock을 사용해서 구현합니다.
원자 연산은 중단되지 않고 한 번에 실행되는 연산을 의미합니다. 여러 스레드가 특정 연산을 동시에 수행할 때, 원자성을 보장하여 연산 중간에 다른 스레드가 개입하지 못하도록 합니다.
원자 연산이 여러 스레드가 접근하더라도 동기화에 문제가 없는 이유는 원자성이 보장되기 때문입니다. 원자성이란 더 이상 나눌 수 없는 성질을 의미하여 원자연산에는 다른 작업이 끼어들 수 없습니다.
원자 연산은 Non-Blocking에서 중요하게 동작합니다.
멀티스레드 환경에서 각 스레드가 다른 스레드와 독립적으로 데이터를 저장하고 접근할 수 있는 메커니즘입니다.
각 스레드가 자신만의 복사본을 갖도록 합니다. 이를 통해 멀티스레드 환경에서 발생할 수 있는 레이스 컨디션(Data Race) 문제를 해결할 수 있습니다.
하지만 복사본이 결국 공유 리소스에 반영되거나, 다른 스레드와 데이터를 공유해야 할 경우 동기화 메커니즘을 통해서 데이터 무결성을 보장해야됩니다.
재진입성은 함수가 여러 스레드에 의해 동시에 호출되더라도 안전하게 실행될 수 있음을 의미합니다.
재진입성의 개념은 함수가 중단되었다가 다시 진입할 때도 그 함수가 안전하게 동작할 수 있어야 한다는 것입니다.
Lock은 멀티스레드 환경에서 스레드가 공유자원에 접근하하는 것을 제어하기위한 동기화 메커니즘입니다.
멀티스레드 환경에서 데이터의 일관성을 유지하고, 레이스 컨디션(Data Race)를 방지하기 위해 사용됩니다.
Lock은 Thread Safety를 지키기 위한 방법중 하나인 상호배제의 특성을 가지고 있습니다. 공유 자원에 대해서 하나의 자원만 접근하도록 합니다.
그래서 Lock을 획득한 스레드만이 공유 자원에 접근할 수 있게 하는 방법입니다.
락을 획득한다는 의미는 락을 획득한 스레드가 데이터를 제어할 수 있는 권한을 얻었다고 표현하면 될 것 같습니다.
그래서 락을 획득한 스레드가할 일이 끝나면 락을 반환하고 다른 스레드가 락을 획득해서 스레드가 하나씩 공유 자원에 접근해서 처리하는 것 입니다.
레이스 컨디션(Data Race)
레이스 컨디션은 두 개 이상의 스레드가 동시에 하나의 공유 자원에 접근할 때 발생할 수 있습니다.
이때 각 스레드가 자신이 의도한 작업을 완료하기 전에 다른 스레드가 자원에 접근하여 값을 변경할 수 있으며, 그로 인해 데이터의 무결성이 깨지고, 시스템의 동작이 의도하지 않은 방향으로 갈 수 있습니다.
예를 들어서 내가 책을 읽고 있는데 옆에 사람이 책의 글자를 바꿔버린다면 내가 제대로된 의미를 파악하기 힘들 것 입니다 여기서 책이 공유자원이고 사람이 스레드입니다.
이러한 상황을 방지하기 위해서 동기화 매커니즘을 사용하는 것 입니다.
Lock을 이해하기 위해서 블로킹을 이해해고 갈 필요가 있다고 생각했습니다.
왜냐하면 Lock이 걸리면 다른 스레드는 해당 스레드를 사용하기 위해 기다릴 것이고 기다리는 스레드가 많아지고 락이 풀리지 않으면 병목 현상이 일어나는데 이 기다리는 스레드들을 블로킹 되었다고 표현하기 때문이고 이 블로킹 상태를 고려하여 락을 설정해줘야되기 때문입니다.
블로킹은 하나의 스레드가 특정 조건이 만족될 때까지 실행을 멈추고 기다리는 것을 의미합니다.
락을 사용하면 블로킹이 발생할 수 있습니다. 즉, 한개의 스레드가 락을 획득한다면 다른 스레드들은 그 락이 해제될 때까지 블로킹이 됩니다.
블로킹 상태가 길어지면 그동안 해당 스레드를 사용하기 위한 다른 스레드들이 전부 블로킹이 되며 작업이 지연될 것이기 때문에 Lock을 설정할 때 블로킹 상태를 고려하여 설정하여야 합니다.
데이터를 변경하지 않고 읽기 작업을 위해 잠그는 락입니다.
공유 락은 특정 데이터에 Lock을 걸 때 다른 스레드가 데이터를 읽어도 정합성은 지켜지기 때문에 공유 락을 막을 이유가 없습니다
하지만 데이터를 읽고 있는 중일 때 다른 스레드가 데이터를 변경한다면 문제가 생길 수 있습니다 그래서 공유 락은 다른 스레드의 베타 락 획득을 막습니다.
데이터를 변경하는 작업을 위해 잠그는 것을 의미합니다.
베타 락은 데이터를 변경을 하기 때문에 다른 스레드가 접근해서 읽기, 쓰기 행동을 한다면 데이터의 정합성이 지켜지지 않기 때문에 다른 스레드의 공유, 베타 락의 획득을 막습니다.
Exclusive라는 단어는 독점하다라는 의미가 있습니다 단어 뜻 대로 메모리를 독점하면서 다른 락의 획득을 막습니다.
공유 락과 배타 락을 사용하여 락을 거는 방법인데 읽기는 동시 접근을 허용하고 쓰기 작업은 배타적으로 허용하는 방법입니다.
제가 이해한 방법은 문서 한장을 여러 사람이 같이 보는 것은 가능합니다. 하지만 여러 사람중 한명이 문서중 문장을 고치거나 지운다면 읽고 있는 사람들은 원문의 내용을 제대로 이해하지 못할 것입니다 그래서 Read-Write Lock은 읽기에는 동시 접근을 허용하고 쓰기 작업은 배타적으로 하게 만들었다고 생각합니다.
Lock과 Unlock을 Busy-Waiting 방식으로 동작합니다. 락을 획득하려는 스레드가 락이 해제될 때까지 반복적으로 락의 상태를 확인하는 방식으로 동작합니다.
스핀락은 락을 획득할 때까지 Busy-Waiting을 하여 임계영역(Critical Section)이 짧은 경우 효과적입니다. 하지만 락의 시간이 길어지면 Busy-Waiting 동안 CPU 자원을 소비하므로 자원 낭비가 될 수 있다는 단점이 있어서 임계영역이 짧을 경우 효과적이게 사용할 수 있습니다.
Busy-Wating
스레드가 특정 조건을 기다리면서, 아무 일도 하지 않고 반복적으로 조건을 확인하는 것을 의미
Busy-Wating은 Blocking이 걸린 스레드가 계속해서 Lock 획득을 위해서 반복적으로 공유자원에 대한 Lock이 해제되었는지 묻는 방법입니다.
예를 들자면 화장실에 누군가 들어가서 문을 잠구면 밖에서 사람이 나올때까지 계속 문을 두드리는 행위입니다.
임계영역(Critical Section)
동시에 접근해서는 안되는 자원이나 코드 영역입니다.
임계영역에서는 여러 스레드가 동시에 접근할 경우 데이터 불일치나 예상치 못한 동작이 발생할 수 있습니다. 따라서 임계영역은 한 번에 하나의 스레드만 접근할 수 있도록 보호되어야 합니다.

뮤텍스는 Mutual Exclusion의 줄임말 입니다. 뮤텍스는 하나의 공유된 자원에 대한 여러 스레드의 접근을 막는 것입니다.
잠금(Lock)과 해제(UnLock) 두 가지 기본 작업을 제공하고 스레드가 뮤텍스를 잠그면 다른 스레드는 뮤텍스가 해제될 때 까지 기다립니다. 뮤텍스를 잠근 스레드만이 해제를 할 수 있습니다.

임계 영역에 접근 가능한 프로세스의 수를 가지고 있는 Counter 역할을 합니다.
세마포어는 여러개의 공유자원에 대한 여러 스레드의 접근을 통제하는 역할을 합니다.
P연산과 V연산을 통해서 세마포어의 카운트를 증감 연산하고 여러 스레드에서 접근해서 공유리소스를 사용할 수 있게 만듭니다.
Ex
호텔에는 3개의 방이 있고 이 호텔 방을 열수 있는 열쇠는 3개가 있습니다.
세마포어는 이 호텔의 주인이고 열쇠를 관리하며 손님이 오면 방을 내주는 역할을 합니다. 1개 이상의 공유 리소스를 카운팅 하는 역할을 합니다.
그리고 이제 한 손님이 와서 키를 받아서 들어갑니다 그러면 열쇠의 개수가 3개에서 2개로 줄어듭니다. 이 연산이 P연산으로 세마포어의 카운팅을 감소하는 연산입니다.
이어서 두 사람이 방문해서 키를 받았습니다. 2번의 P연산이 반복될 것 이고 세마포어의 카운팅은 0이 될 것 입니다. 그렇다면 그렇다면 더 이상 손님을 받을 수 없는 상태가 되죠?? 그러면 손님이 와도 방을 줄 수 없는 대기 상태에 들어가게 됩니다.
이제 반대로 방을 다 사용하고 주인(세마포어)에게 키를 반납하는 행위를 V연산이라고 합니다. 그러면 세마포어의 카운팅 개수는 1이 늘어날 것이고 대기중인 사람이 있다면 키를 주는 P연산을 지속하는 것입니다.
뮤텍스와 세마포어는 여러 스레드가 공유 자원에 대한 접근을 제어하는 것을 동일합니다.
이 둘의 가장 큰 차이점은 위에 그림에서 보면 알 수 있듯이 통제하는 공유 자원에 대한 개수입니다.
뮤텍스는 하나의 공유 자원에 대한 접근을 통제하고 세마포어는 한 개 이상의 공유자원을 통제합니다.
그래서 세마포어는 뮤텍스가 될 수 있지만 뮤텍스는 세마포어가 될 수 없습니다.
Lock은 멀티 스레드 환경에서 공유 자원을 안전하게 사용하기 위해서 착안된 방법이지만 Lock에도 여러 문제점들이 있습니다.
잘못된 자원관리로 인하여 두 개 이상의 프로세스 또는 스레드가 아무것도 진행하지 않는 상태로 영원히 대기하는 상황입니다.
아래 4가지 조건이 모두 만족하는 상황을 교착상태라고 합니다.
1. 상호 배제
하나의 프로세스 또는 스레드만이 공유 자원에 접근할 수 있습니다.
2. 점유와 대기
자원을 가진 프로세스 또는 스레드가 다른 자원을 점유하기 위해 기다릴때 가지고 있는 자원을 해제하지 않고 기다리는 상황입니다.
3. 비선점
공유 자원을 점유하고 있는 프로세스 혹은 스레드만이 해당 자원을 해제할 수 있습니다. 즉 강제적으로 자원을 빼앗을 수 없습니다.
4. 순환 대기
자원을 기다리는 프로세스 또는 스레드간에 사이클이 형성되는 상황입니다.
예방(prevention)
교착상태 발생 조건중(상호배제, 점유와 대기, 비선점, 순환대기) 하나를 제거하여 교착상태를 예방할 수 있습니다.
회피(avoidance)
은행원 알고리즘을 사용하여 교착상태를 회피할 수 있습니다.
은행원 알고리즘은 프로세스 또는 스레드가 자원을 요구할 때, 시스템은 자원을 할당한 후에도 안정 상태로 남아있게 되는지 사전에 검사하여 교착 상태 회피합니다. 안정 상태면 자원 할당하고, 그렇지 않다면 다른 프로세스들이 자원 해지까지 대기합니다.
탐지(Detection)
자원 할당 그래프를 통해 교착 상태를 탐지하는 방법입니다.
회복(Recovery)
교착상태를 일으킨 프로세스 또는 스레드를 종료하거나, 할당된 자원을 해제시켜 회복시키는 방법입니다.
라이브 락은 두 개 이상의 프로세스가 서로를 방해하지 않으려고 계속 상태를 바꾸면서도 실질적인 진행을 하지 못하는 상황을 말합니다. 죽지는 않지만, 서로 양보하느라 앞으로 나아가지 못하는 상태라고 할 수 있습니다.
두 사람이 복도에서 마주쳤을 때 서로 길을 양보하려고 옆으로 피하면서 둘다 진행하지 못하는 상황
낮은 우선순위의 프로세스가 높은 우선순위의 프로세스보다 먼저 실행되면서
높은 우선순위의 프로세스가 지연되는 상황을 말합니다.
보통 우선순위가 높은 프로세스가 낮은 우선순위의 프로세스가 소유한 자원을 기다리면서 발생합니다.
중요하고 긴급한 작업이 덜 중요한 작업 때문에 계속 대기하는 상황.
여러 프로세스 혹은 스레드가 하나의 공유자원에 접근할때 하나의 스레드만 락을 획득하고 다른 스레드들은 블로킹되어서 기다리는 상황
고속도로에서 차선이 갑자기 줄어드는 구간을 생각해 볼 수 있습니다. 차선이 줄어들면서 모든 차량의 속도가 그 지점에서 크게 느려지게 되고, 이로 인해 교통 흐름이 전체적으로 느려지게 됩니다. 이 줄어든 구간이 고속도로의 병목 현상을 일으키는 지점입니다.
특정 프로세스가 자원을 할당받지 못해 계속 대기만 하면서 실행되지 못하는 상황입니다.
공유자원이 계속 다른 프로세스에 우선적으로 할당되어 발생할 수 있습니다.
이번 포스팅에서는 Thread-Safety가 무엇인지 그리고 멀티 스레드 환경에서 공유자원을 안전하게 사용하기 위한 Lock에 대해서 알아보았습니다.
공부하면서 느낀 생각이지만 Lock 잘 사용한다고해도 발생할 수 있는 문제점들이 있고 고려해야될 사항들이 많아서 Non-Blocking이라는 방법이 나왔다고 하는데 추후에 이 Non-Blocking에 대해서도 포스팅 해 볼 예정입니다.
그리고 Blocking, Non-Blocking vs 동기, 비동기에 관련된 이야기도 많이 나와서 이 부분에 대해서도 포스팅 할 예정입니다.
질문과 피드백은 언제나 감사합니다!! 🙇🙇