작성
·
361
0
private let threadSafeCountQueue = DispatchQueue(label: " ")
private var _count = 0
public var count: Int {
get {
return threadSafeCountQueue.sync {
_count
}
}
set {
threadSafeCountQueue.sync {
_count = newValue
}
}
}
for _ in 0..<30000 {
DispatchQueue.global().async {
count += 1
}
}
Thread.sleep(forTimeInterval: 15)
print(count)
앨런님의 설명처럼 count를 싱크처리 하고 async로 count를 증가시키는데 정상적인 값이 안나옵니다. 어떻게 하면 될까요?
답변 2
1
안녕하세요 성훈 님!
비동기적인 일처리를 위해서는 실제로 조금 "오래걸리는 일"을 통해서 실험을 해보실 필요가 있습니다. 어떻게 보면 지금 현재 처리 원하는 코드의 경우, 오히려 비동기 입장에서 보면 특수한 작업이기 때문에 제대로된 값을 확인하실 수 없는 것입니다.
(제가 드린 PDF파일 - 237페이지에서 설명드린 아래 이미지와 같은 일이 일어나고 있습니다.)
왜냐하면
객체를 내부를 직렬큐로 설계했다고 하더라도 성훈 님이 원하는 형태의 작업은 직렬큐에서
읽기 + 쓰기 + 읽기 + 쓰기 + 읽기 + 쓰기... 이지만,
실제로 일어나는 일은
읽기 + 읽기 + 읽기 + 읽기 + 쓰기 + 읽기 + 읽기 + 읽기 + 읽기 + 쓰기... 형태의 일이 일어납니다.
왜 그런지를 다시한번 설명드려보면..
컴퓨터 입장에서는
count += 1 // ===> count = count + 1
이라는 작업자체가 너무나 빠른 시간 안에 일어나는 일이기 때문에
count (쓰기) = count + 1 (읽기)
(컴퓨터는 1초에 30억번 정도의 일을 하고, 저렇게 변수의 있는 값을 바꾸는 일은 정말 너무나 빠르게 일어나죠. 읽기(get)는 0.00000000016 초 정도로 너무나 빠르게 일어나고, 쓰기(set)는 읽기에 비해 4배정도의 시간이 걸립니다.)
그런데 작업을 반복문을 통해서 아주 빠르게 비동기적으로 일을 시키면..
for _ in 0..<30000 {
DispatchQueue.global().async {
// 쓰기 // 읽기 (+더하기)
count = count + 1
}
}
컴퓨터 입장에서는
0째 반복 주기를 시작하면서 읽기 일을 하고, (쓰기가 끝나기 전에) 1번째 반복문 주기 읽기 일을 하고 (또 쓰기가 끝나기 전에) 2번째 반복문 주기 읽기 일을 시작하고 (이쯔음 0번째 반복주기 쓰기 일이 끝남) ... 이런식으로 작업이 진행 됩니다.
*아래 괄호 안은 반복문의 반복주기를 표시함
읽기(0) + 읽기(1) + 읽기(2) + 쓰기(0) + 읽기(3) +......
그래서 성훈님이 쓰신 코드 처럼 (클래스로 만들고) 거의 동일하게 만들어서
class SomeClass {
let threadSafeCountQueue = DispatchQueue(label: "some")
private var _count = 0
public var count: Int {
get {
threadSafeCountQueue.sync {
return _count
}
}
set {
threadSafeCountQueue.sync {
self._count = newValue
}
}
}
}
let some = SomeClass()
for _ in 0..<1000 {
DispatchQueue.global().async {
// 쓰기 // 읽기
some.count = some.count + 1
}
}
반복주기만 조금 줄여도 어쨌든 제가 말씀드린 것처럼 변수를 읽어오고 쓰는 작업자체가 너무 빠르게 일어나기 때문에.. 직렬큐라고 하더라도 읽기 + 쓰기 + 읽기 + 쓰기 + 읽기 + 쓰기의 순서를 보장할 수 없습니다. 이건 지금 설정하신 작업의 특성(단순 변수의 count증가) 때문에 일이 너무나 빨리 일어나기 때문이라고 말씀드릴 수 있을 것 같아요.
그래서, 만약에 여기서 코드를 조금만 바꿔서 순서대로 일어나게 작업을 바꿔야 한다면
class SomeClass2 {
let threadSafeCountQueue = DispatchQueue(label: "some")
private var _count = 0
public var count: Int {
get {
threadSafeCountQueue.sync {
// 읽기가 오래걸리도록 ⭐️
Thread.sleep(forTimeInterval: 0.1)
return _count
}
}
set {
// 쓰기는 직렬큐 배제 ⭐️
self._count = newValue
}
}
}
let some2 = SomeClass2()
for _ in 0..<1000 {
DispatchQueue.global().async {
some2.count = some2.count + 1
}
}
Thread.sleep(forTimeInterval: 200)
print(some2.count)
이런식으로 하면 되지 않을까 싶습니다.
위에서 말씀드린 대로 (컴퓨터가 실행하는 작업 입장에서 보면) 읽기 작업이 너무 빠르게 걸리기 때문에 순서를 보장할 수 없다고 말씀드렸고, 그래서 읽기 작업이 조금 오래걸리도록 (반복문에서 비동기 작업의 배정도 조금 오래 걸림) 설정했습니다.
Thread.sleep(forTimeInterval: 0.1)
그리고 읽기 작업이 충분히 오래 걸리니까, 오히려 쓰기 작업은 직렬로 처리해주지 않아도 될 것 같아서 직렬을 풀기도 했고, 특히나 이런 종류의 작업의 특성상 반복문으로 일의 배정을 아주 빠르게 하고 있기 때문에 (아무리 직렬큐라고 하더라도 처음부터 읽기 쓰기 작업을 하나로 묶어버리는 게 아닌 이상)
읽기(count + 1) + 쓰기(다시할당) + 읽기(count + 1) + 쓰기(다시할당) + 읽기 + 쓰기... 의 순서를 보장하기가 어렵기 때문에 쓰기 작업의 경우, 직렬처리를 해주지 않은 이유도 있습니다.
아니면, 일 자체가 빠르다는 특성을 감안하여, 아래 처럼 읽기 쓰기 작업을 묶어서 시리얼큐에 배정시키는 것도 방법이 될 수 있겠지요.
let some = SomeClass()
let queue1 = DispatchQueue(label: "serial")
for _ in 0..<1000 {
DispatchQueue.global().async {
// 읽기 쓰기 작업을 묶어서 배정 ⭐️
queue1.sync {
some.count = some.count + 1
}
}
}
Thread.sleep(forTimeInterval: 60)
print(some.count)
무튼 여러가지로 왜 그런 현상이 일어나는지 명확하게 설명드린 것 같고, 중요한 포인트는 지금 실행하신 작업(count = count + 1)자체의 특성 상.. 너무 빠르게 일이 일어나기 때문에 직렬큐로 처리하더라도 읽기 쓰기 순서를 보장해서 처리 할 수 없다는 것입니다.
문제에 대해서 이해하셨길 바라요.
그래서, 직렬큐 + 시리얼 조차도 절대적인 처리는 될 수 없고.. 작업의 특성을 잘 판단하시고 코드를 짜셔야 할 것 같아요!
감사합니다. :)
0