동시성은 항상 성능을 높여준다.
⇒ 여러 프로세서가 동시에 처리할 계산이 충분히 많은 경우에만 성능이 높여준다.
동시성을 구현해도 설계는 변하지 않는다.
⇒ 단일 스레드와 다중 스레드는 설계가 판이하게 다르다.
⇒ 일반적으로 무엇과 언제를 분리하면 시스템 구조가 크게 달라진다.
웹 또는 EJB 컨테이너를 사용하면 동시성을 이해할 필요가 없다. (Swift 같은 경우, Alamofire나 Moya 같은 라이브러리를 이용하면 굳이 동시성 자체를 이해할 필요가 없다는 뜻으로 이해하면 될 듯)
⇒ 실제로 동시성이 어떻게 동작하는지, 어떻게 동시 수정, 데드락 등과 같은 문제를 피할 수 있는지를 알아야만 한다.
동시성은 다소 부하를 유발한다.
⇒ 성능 측면에서 부하가 걸리며, 코드도 더 짜야 한다.
동시성은 복잡하다.
⇒ 간단한 문제여도 동시성은 복잡하다.
일반적으로 동시성 버그는 재현하기 어렵다.
⇒ 그렇기에 진짜 결함으로 간주되지 않고 일회성 문제로 여겨 무시하기 쉬움
동시성을 구현하려면 흔히 근본적인 설계 전략을 재고해야 한다. ⭐️⭐️
⇒ 스레드를 한 개만 더 늘린 후 코드 한 줄을 거쳐가는 경로를 셌을 때, 바이트 코드만 고려해도 잠재적인 경로는 최대 12,870개에 달함.
⇒ 대다수 경로는 올바른 결과를 내놓으나 잘못된 결과를 내놓는 일부 경로가 존재하므로 동시성을 구현하려면 설계 전략을 재고해보아야 한다.
단일 책임 원칙 (SRP)
자료 범위를 제한하라
class Counter {
var value = 0
}
let counter = Counter()
let queue = DispatchQueue(label: "", attributes: .concurrent)
for _ in 0..<1000 {
// 스레드 1000개를 사용하여 counter.value의 값을 하나 올림
queue.async {
counter.value += 1
}
}
queue.sync(flags: .barrier) {
// 여러 스레드가 동시에 counter의 value를 올리다보니
// 정말 드물지만 간혹가다가 1000이 아닌 다른 값이 나오기도 한다.
print(counter.value)
}
자료 사본을 사용하라.
스레드는 가능한 독립적으로 구현하라.
class CookieBox {
private var cookies = [String]()
func add(cookie: String) {
cookies.append(cookie)
print("기계가 \(cookie)를 바구니에 넣었어요!")
}
func take() -> String? {
if !cookies.isEmpty {
let takenCookie = cookies.removeFirst()
print("아이가 \(takenCookie)를 상자에서 꺼냈어요!")
return takenCookie
} else {
print("상자가 비었어요!")
return nil
}
}
}
let cookieBox = CookieBox()
// 생산자와 소비자가 동시에 실행되면 생산자와 소비자간의 경쟁 상태가 펼쳐져
// 생산자가 쿠키를 이미 구웠는데도 소비자가 쿠키를 기다리고 있는 상황이 발생하거나
// 쿠키가 없는데 소비자가 쿠키를 빼가거나 하는 이상한 상황이 펼쳐진다. ⭐️⭐️
// 생산자
DispatchQueue.global().async {
for i in 1...5 {
cookieBox.add(cookie: "쿠키 \(i)")
sleep(1) // 쿠키를 만드는데 시간이 조금 걸려요.
}
}
// 소비자
DispatchQueue.global().async {
for _ in 1...5 {
_ = cookieBox.take()
sleep(1) // 쿠키를 꺼내는데 시간이 조금 걸려요.
}
}
============================================================================
// 이런 생산자 소비자 문제를 해결하기 위해서 "Semaphore"을 사용한다. ⭐️⭐️⭐️
class CookieBox {
private var cookies = [String]()
// Semaphore를 사용하여 한 번에 하나씩만 쿠키가 들어갔다 나갔다 하도록 조절한다.
private let lock = DispatchSemaphore(value: 1)
func add(cookie: String) {
lock.wait() // 상자에 넣기 전에 잠깐 기다려요.
cookies.append(cookie)
print("기계가 \(cookie)를 바구니에 넣었어요!")
lock.signal() // 이제 다른 사람이 상자를 사용할 수 있어요.
}
func take() -> String? {
if !cookies.isEmpty {
lock.wait() // 상자에서 꺼내기 전에 잠깐 기다려요.
let takenCookie = cookies.removeFirst()
print("아이가 \(takenCookie)를 상자에서 꺼냈어요!")
lock.signal() // 이제 다른 사람이 상자를 사용할 수 있어요.
return takenCookie
} else {
print("상자가 비었어요!")
lock.signal() // 이제 다른 사람이 상자를 사용할 수 있어요.
return nil
}
}
}
let cookieBox = CookieBox()
// Semaphore를 사용하면 "경쟁 상태" 가 벌어지지 않게 된다. ⭐️⭐️
// 생산자
DispatchQueue.global().async {
for i in 1...5 {
cookieBox.add(cookie: "쿠키 \(i)")
sleep(1) // 쿠키를 만드는데 시간이 조금 걸려요.
}
}
// 소비자
DispatchQueue.global().async {
for _ in 1...5 {
_ = cookieBox.take()
sleep(1) // 쿠키를 꺼내는데 시간이 조금 걸려요.
}
}
클라이언트에서 잠금 ⇒ 클라이언트에서 첫 번째 메소드를 호출하기 전에 서버를 잠근다.
- 즉, 공유 자원을 사용하는 경우 해당 자원의 접근을 1) Semaphore나 2) NSLock등의 lock으로 통제하여 임계 영역 안에서 한 가지 스레드만 접근하도록 만듦. 이것을 프로그래밍 용어로 “상호 배제 (Mutual Exclusion)” 라고 한다. ⭐️⭐️
import Foundation
class MoneyCalculator {
var money = 10000
func deduct() {
money = money - 1000
}
}
let moneyCalculator = MoneyCalculator()
DispatchQueue.global().async {
moneyCalculator.deduct()
print(moneyCalculator.money)
}
DispatchQueue.global().async {
moneyCalculator.deduct()
print(moneyCalculator.money)
}
// 8000
// 8000
// 두 가지 스레드가 하나의 메소드에 동시에 접근하므로, 결과가 같게 나오는 현상이 발생.
=============================
// **NSLock**등의 lock으로 임계 영역에 한 가지 스레드만 접근할 수 있게끔 막는다. ⭐️⭐️
class MoneyCalculator {
var money = 10000
let locker = NSLock()
func deduct() {
money = money - 1000
}
}
let moneyCalculator = MoneyCalculator()
// 2번 스레드
DispatchQueue.global().async {
// 2번 스레드가 실행되는 동안 다른 스레드가 접근하지 못하도록 잠근다.
moneyCalculator.locker.lock()
moneyCalculator.deduct()
print(moneyCalculator.money)
// 메소드 실행이 종료되었다면 잠금을 해제하여 다른 스레드가 접근하도록 허용한다.
moneyCalculator.locker.unlock()
}
// 3번 스레드
DispatchQueue.global().async {
// 3번 스레드가 실행되는 동안 다른 스레드가 접근하지 못하도록 잠근다.
moneyCalculator.locker.lock()
moneyCalculator.deduct()
// 메소드 실행이 종료되었다면 잠금을 해제하여 다른 스레드가 접근하도록 허용한다.
moneyCalculator.locker.unlock()
}
// 9000
// 8000
서버에서 잠금 ⇒ 서버에다 “서버를 잠그고 모든 메소드를 호출한 후 잠금을 해제하는” 메소드를 구현한다. 클라이언트는 이 메소드를 호출한다.
class MoneyCalculator {
var money = 10000
let locker = NSLock()
func openAndDeductAndClose() {
locker.lock()
money = money - 1000
print(money)
locker.unlock()
}
}
let moneyCalculator = MoneyCalculator()
// 2번 스레드
DispatchQueue.global().async {
moneyCalculator.openAndDeductAndClose()
}
// 3번 스레드
DispatchQueue.global().async {
moneyCalculator.openAndDeductAndClose()
}
// 9000
// 8000
연결 서버 ⇒ 잠금을 수행하는 중간 단계를 생성한다. '서버에서 잠금' 방식과 유사하지만 원래 서버는 변경하지 않는다.
// 실제 수행을 하는 객체를 생성
class MoneyCalculator {
func deduct() {
money = money - 1000
}
}
// 잠금을 수행하는 중간 단계를 생성
class MoneyCalculatorManager {
let moneyCalculator: MoneyCalculator
let locker = NSLock()
init(moneyCalculator: MoneyCalculator) {
self.moneyCalculator = moneyCalculator
}
func openAndDeductAndClose() {
locker.lock()
moneyCalculator.deduct()
locker.unlock()
}
}
let moneyCalculator = MoneyCalculator()
let moneyCalculatorManager = MoneyCalculatorManager(moneyCalculator: moneyCalculator)
// 2번 스레드
DispatchQueue.global().async {
moneyCalculatorManager.openAndDeductAndClose()
}
// 3번 스레드
DispatchQueue.global().async {
moneyCalculatorManager.openAndDeductAndClose()
}
// 9000
// 8000
말이 안 되는 실패는 잠정적인 스레드 문제로 취급하라.
다중 스레드를 고려하지 않은 순차 코드부터 제대로 돌게 만들자.
다중 스레드를 쓰는 코드 부분을 다양한 환경에 쉽게 끼워 넣을 수 있도록 스레드 코드를 구현하라.
다중 스레드를 쓰는 코드 부분을 상황에 맞춰 조정할 수 있게 작성하라.
프로세서 수보다 많은 스레드를 돌려보라.
다른 플랫폼에서 돌려보라.
코드에 보조 코드를 넣어돌려라. 강제로 실패를 일으키게 해보라.
class ThreadJigglePoint {
static func jiggle() {
let semaphore = DispatchSemaphore(value: 1)
let randomValue = Int.random(in: 0...2)
switch randomValue {
case 0:
let sleepInterval = UInt32.random(in: 1...3)
sleep(sleepInterval)
case 1:
semaphore.wait()
// 마지막에 semaphore.signal() 메소드를 실행함으로 다른 스레드가
// 들어갈 수 있도록 자리를 마련한다.
defer { semaphore.signal() }
case 2:
print("Do nothing")
default:
fatalError("Unexpected random value")
}
}
}
=========================================================================
// 2번 스레드
DispatchQueue.global().async {
// 어떤 로직을 처리
ThreadJigglePoint.jiggle() // 흔들기 코드로 오류 찾기
// 어떤 로직을 처리
ThreadJigglePoint.jiggle() // 흔들기 코드로 오류 찾기
// 어떤 로직을 처리
ThreadJigglePoint.jiggle() // 흔들기 코드로 오류 찾기
}
// 3번 스레드
DispatchQueue.global().async {
// 어떤 로직을 처리
ThreadJigglePoint.jiggle() // 흔들기 코드로 오류 찾기
// 어떤 로직을 처리
ThreadJigglePoint.jiggle() // 흔들기 코드로 오류 찾기
// 어떤 로직을 처리
ThreadJigglePoint.jiggle() // 흔들기 코드로 오류 찾기
}