
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 를 다루어 보도록 하겠습니다. 감사합니다.