스위프트로 할지 CS로 할지 고민하다가
CS에서 통용 되는 이야기라고 해서 CS로..!
여러 스레드나 큐가 동시에 공유 자원에 접근할 때 서로가 경합 하는 현상을 말하며 그로 인해
다른 스레드자원의 상태가 예상하지 못한 상태에서 변경되어 프로그램의 동작이 불확실하거나 오류가 발생할 수 있다
즉 레이스 컨디션은 동시성 문제로 내가 어떤 자원을 읽거나 수정하려고 접근했는데, 동시에 다른 프로세스나 스레드가 그 자원에 접근하여 값을 변경해서 실행 순서나 타이밍에 따라 결과가 달라질 수 있는 오류를 말한다
마치 순환 참조 오류처럼 내가 쓰려고 스레드에 작업을 넣어놨더니 다른 스레드에서 영향을 받아 이미 nil이 되어버린 그런 상황
var counter = 0
DispatchQueue.global().async {
counter += 1
}
DispatchQueue.global().async {
counter += 1
}
1개의 변수에 비동기를 통해서 두개의 작업이 요청된다
cpu 코어가 적어서 동시 실행안될 가능성을 뺀다면..
두 작업이 병렬로 진행되어 두 스레드는 counter를 0으로 인식한다
사실상 위에서 아래로 읽어나갔다면 0에서 +1 이 된값을
다음에는 1로 받아서 +1 을 해서 2가 되어야하지만
동시에 실행이 되어 버려서 값이 그냥 1로 되어버릴수 있다는 것이다.
서로를 고려하지않고 병렬로 내가 받은 counter 는 0이야! 라고 하고 둘다 작업을 +1한 값을 다시 메모리에 써버리는 문제이다
var arrString: [String] = []
DispatchQueue.global().async {
arrString.append("abcd")
}
DispatchQueue.global().async {
arrString.append("efgh")
}
이렇게 배열에서도 문제가 발생할수 있느데
배열이 깨지거나 한쪽이 입력을 못하거나 순서가 정확하지도 않을수 있고 메모리가 깨질수 있다
a스레드가 값을 읽으려고 하는 와중에 b스레드가 읽고 새로운 값을 입력해서 충돌하는 상황
var counter = 0
DispatchQueue.global().async {
counter = counter + 1 // 읽고 쓰기
}
DispatchQueue.global().async {
print(counter) // 동시에 읽기
}
스위프트에서 레이스 컨디션을 해결하기 위해서는 동기화를 사용해야한다
비동기에서 작업을 동시화 시켰을때 생기는 문제이니 동시에 작업이 진행 못하도록 작업을 풀어주어야한다
메인 큐와 같이 직렬 큐 Serial Queue를 사용해서 한작업씩 순차적으로 진행하도록 보내는 것이다
하나의 스레드가 작업을 진행한다면 다른 스레드가 접근을 하지 못하도록 막는 것이다.
var arrString: [String] = []
let lock = NSLock()
DispatchQueue.global().async {
lock.lock() // 접근 제어하기
arrString.append("abcd")
lock.unlock() // 접근 제어 풀기
}
DispatchQueue.global().async {
lock.lock() // 두 번째 스레드도 접근 제어하기
arrString.append("efgh")
lock.unlock() // 접근 제어 풀기
}
이렇게 값에 접근하는 모든 곳에 lock과 Unlock을 해주어야한다
한곳에만 한다면 락을 하기전에 이미 다른 곳에서 값을 읽고 쓰고 하는 중이기에
원하는 것과 다르게 나온다.
이렇게 코드를 구성하면 위에서부터 순차적으로 진행되기에 배열에는 [abcd, efgh] 가 들어가있는다
지정된 카운터 값만큼 스레드가 자원에 접근할 수 있도록 제한하는 세마포어를 사용한 동기화 방법이다
var counter = 0
let semaphore = DispatchSemaphore(value: 1)
// 한 번에 하나의 스레드만 접근 가능
DispatchQueue.global().async {
semaphore.wait() // 세마포어를 획득
counter += 1
semaphore.signal() // 세마포어를 해제
}
DispatchQueue.global().async {
semaphore.wait() // 세마포어를 획득
counter = counter + 1 // 읽고 쓰기
semaphore.signal() // 세마포어를 해제
}
DispatchQueue.global().async {
semaphore.wait() // 세마포어를 획득
counter = counter + 1 // 읽고 쓰기
semaphore.signal() // 세마포어를 해제
}
세마포어를 획득하기 위해서는 존재하는 카운터를 1 감소시킨다
감소시킬 카운터가 없을 경우 0일 경우 대기하며
세마포어를 해제하면 카운터를 1을 증가 시킨다
세마포어는 정수 값을 가지는데 이 값은 자원의 수나 동시에 허용할 스레드의 수 이다
카운터를 1이 아닌 2이상의 수를 입력하면 그만큼의 스레드에서도 동시에 접근이 가능하다.
actor 는 스레드의 안정성을 보장하는 구조체로 actor 내부의 값들은 한번에 하나의 스레드의 접근만 허용한다
actor Counter {
private var value = 0
// 값을 증가
func increment() {
value += 1
}
}
let counter = Counter()
// 여러 비동기 작업에서 counter에 접근
async {
await counter.increment() // 안전하게 증가
}
async {
await counter.increment() // 안전하게 증가
}
이처럼 좀더 가독성이 있고
스레드 접근을 하나씩만 보장하기에 안정성이 있으며 내부 데이터에 접근할 때 동기화를 자동으로 처리하게 된다.
그래서 다른 스레드에서 동시에 데이터 수정을 시도할 수 없으므로 레이스 컨디션이 발생하지 않는다
private 변수로 값을 캡슐화해서 외부에서 직접 접근을 막는 건 선택사항이지만
동시성문제를 해결하기 위해서는 꼭 함수로만 값을 변경 할수 있게 하는 것이 더 좋다
lock과 unlock이나 wait이랑 signal을 보면 하나의 쌍 처럼 열고 닫게 되어있었다
만약에라도 unlock이나 signal을 빼먹으면 어떻게 될까?
var arrString: [String] = ["start"]
let lock = NSLock()
DispatchQueue.global().async {
lock.lock() // 접근 제어하기
arrString.append("abcd")
// lock.unlock() 빠짐
}
DispatchQueue.global().async {
lock.lock() // 두 번째 스레드가 대기 상태로 멈춰 이곳에서 대기
arrString.append("efgh")
lock.unlock() // 접근 제어 풀기
}
sleep(10) // 비동기 작업이 끝날 시간을 확보
print(arrString) // 출력: ["start"]
그렇다면 바로 교착 상태에 들어가게 된다
교착상태라는 건 두 스레드가 서로 락을 기다리며 무한히 멈추게 되는 상태로
unlock으로 인해 어떤 스레드도 접근을 할수 없는 상태로 코드는 흘러가야하기에 접근하지 못하고 후속작업들까지도 멈춰버린 교착상태인것이다.
만약 첫번째 디스패치큐가 아니라 두번째 디스패치큐에서 언락을 안한거라면 "abcd" 도 배열에 들어가 있는다.
signal 도 동일하게 뺴먹는다면 교착상태이게 된다
그래서 레이스 컨디션이란 동시에 같은 데이터를 건드려서 생기는 문제!
이를 위해서는 작업을 잘 풀어주고 원하는 결과를 도출하기 위해 작업을 순차적으로 진행할 수 있게 해주어야한다