Swift Concurrency 1편부터 이어지는 내용임으로 1편부터 보시고 와주시길 바랍니다
시작하겠습니다.
데이터 레이스... 직역을 해볼까요?
데이터 경쟁
?
맞습니다. 데이터 레이스는 이름 그대로 데이터 경쟁을 의미합니다.
좀더 파고들어 보자면.
멀티 쓰레드 / 프로세스
환경 에서 발생하는 현상이며,
여러 쓰레드 / 프로세스
가동일한 메모리
에 접근하여 할때 발생하는 문제 라고 할수 있죠.
간단한 예시를 만들어 생각해 보도록 합시다.
import Foundation
class Counter {
var count = 0 // 초기값을 0으로
func increment() {
count += 1 값을 1씩 증가 시킵니다.
}
}
let counter = Counter()
let queue = DispatchQueue.global()
// 10개 비동기 작업을 생성 하겠습니다.
for _ in 0..<10 {
queue.async {
for _ in 0..<1000 {
counter.increment()
}
}
}
// 1초 대기하여 모든 작업이 마치도록 합니다.
sleep(1)
print("Final count: \(counter.count)")
자 이때, 여러 스레드가
counter.increment()
메서드를 동시에 호출하게 될것입니다.
즉여러쓰레드가
count에 접근
하게 될것이죠. 결과는 즉 예측할수 없게 됩니다.각 시도 출력값: Final count: 9905, Final count: 9903, Final count: 9914
즉 같은 값을 공유하게된 상황에서 발생.
여러가지 방법이 있겠지만
DispatchQueue
를 통해 해결해 보도록 하겠습니다.
label 즉 데이터를 직렬화 해보죠
import Foundation
class Counter {
private let queue = DispatchQueue(label: "counterQueue")
private var count = 0
var counter: Int {
return queue.sync { count }
}
func increment() {
queue.async {
self.count += 1
}
}
}
let counter = Counter()
let globalQueue = DispatchQueue.global()
for _ in 0..<10 {
globalQueue.async {
for _ in 0..<1000 {
counter.increment()
}
}
}
sleep(1)
print("Final count: \(counter.counter)")
출력값은 Final count: 10000 이 고정입니다.
import Foundation
class Counter {
private var count = 0
private let lock = NSLock()
func increment() {
lock.lock()
count += 1
lock.unlock()
}
var value: Int {
lock.lock()
let result = count
lock.unlock()
return result
}
}
let counter = Counter()
let queue = DispatchQueue.global()
for _ in 0..<10 {
queue.async {
for _ in 0..<1000 {
counter.increment()
}
}
}
sleep(1)
print("Final count: \(counter.value)")
왜 이 같이 해결이 가능했을까요?
DispatchQueue
를 통해 해결을 하였을땐,DispatchQueue(label: "counterQueue")
를 생성하여 클래스 내부 변수인 count 에 대한 접근을 관리하게 하였습니다.
이는 즉, 결과값은 하나의 직렬 큐 (serial Queue) 로 생성되어, 큐에 추가된 작업들이 실행 되도록 하였죠.
또한return queue.sync { count } }
를 통해 동기적으로 값을 반환하게 하였음으로, 해당 작업도 직렬화 되게 합니다.
func increment() 작업에서도
queue.async
를 통해 비동기 적으로 coute 를 증가시키지만
해당 작업은queue
에 비동기적으로 추가 됨으로, 작업이 순서대로 실행 됨으로 쓰레드가 동시에 호출 하더라도, count에 대한 접근은 직렬화 되게 됩니다.
import Foundation
class HowAboutSwiftConcurrency {
/// 라면봉지 5개가 있다고 가정해 보죠.
var ramen = 5
func eat() {
ramen -= 1
}
}
let example = HowAboutSwiftConcurrency()
Task {
for _ in 0..<5 {
example.eat()
}
}
Task {
for _ in 0..<5 {
example.eat()
}
}
Task {
print("현재 라면은...? : \(example.ramen)")
}
위의 코드에서 Data Race가 발생할 수 있습니다.
여러 Task가 동시에 example.eat() 메서드를 호출하여 ramen 변수를 감소시키고,
동시에 example.ramen 값을 읽습니다.
아까 말했던DateRace
가 발생할수 있는 원인3가지 조건
이 기억나시나요?
다시 정리해 보죠.
동시 접근
: 여러 Task가 동시에 ramen 변수에 접근.쓰기 작업 포함
: eat 메서드는 ramen 변수를 수정.동기화 여부X
: 변수 접근이 동기화 되어 있지 않습니다.
Actor
의 등장...
Actor
는 공유되는 가변 상태에 대한 동기화 메커니즘 입니다.
Actor
는 내부 자체에 상태를 가지고 프로그램의 나머지 부분과 격리 되는 특성이 있습니다.
Actor
에 접근 하는 방법이Actor
를 통한 방법이기에 어떤 쓰레드가Actor
에 접근하려
한다면 다른 코드들은Actor
에 접근하지 못하도록 합니다. 다시 정리해서
Actor
는 프로토콜을 채택하고 Extension 구현이 가능합니다.
또한, 클래스와 마찬가지로,참조타입
이며,
프로그램에서인스턴스 데이터
를 나머지와 분리하고 데이터가동기화 접근을 보장
합니다.
import Foundation
actor HowAboutSwiftConcurrency {
/// 라면봉지 5개가 있다고 가정해 보죠.
var ramen = 5
func eat() {
ramen -= 1
}
func getRamenCount() -> Int {
return ramen
}
}
let example = HowAboutSwiftConcurrency()
Task {
for _ in 0..<5 {
await example.eat()
}
}
Task {
for _ in 0..<5 {
await example.eat()
}
}
Task {
let ramenCount = await example.getRamenCount()
print("Remaining ramen: \(ramenCount)")
}
요번 시간에는 DataRace 를 다루어 보았는데 아직... 끝이 아닙니다.
더 많은 내용들이 있습니다... 이번 시간도 긴글 읽어 주셔서 감사드리며. 다음 시간엔 Data Race 2부 - 부제목 Actor 를 다루어 보도록 하겠습니다. 감사합니다.