작성
·
27
0
12강 16분 즘에, Task 의 클로저에 value type 의 프로퍼티를 캡처 리스트로 명시하면 아래와 같은 코드에서는 에러가 나지 않는다고 되어있습니다.
struct ValueCounter {
var value = 0
mutating func increment() -> Int {
value = value + 1
return value
}
}
func test() {
var valueCounter = ValueCounter()
Task {
print(valueCounter.increment())
print(valueCounter.value)
}
Task { [valueCounter] in
var newValueCounter = valueCounter
print(newValueCounter.increment())
print(newValueCounter.value)
}
}
하지만 제가 Xcode 26.0.1 에서 확인했을때는 해당 코드의 첫번째 Task 에서 다음과 같은 컴파일 에러가 발생했습니다
Sending value of non-Sendable type '() async -> ()' risks causing data races
이에 추가로 이것 저것 확인해보다가, 아래와 같이 캡처 리스트를 사용하는 Task 를 먼저 작성하면, 에러가 발생하지 않는것을 확인했습니다.
// 캡처 리스트 사용하는 Task 순서 변경하니 정상
func test2() {
var valueCounter = ValueCounter()
Task { [valueCounter] in
var newValueCounter = valueCounter
print(newValueCounter.increment())
print(newValueCounter.value)
}
Task {
print(valueCounter.increment())
print(valueCounter.value)
}
}
이와 같은 현상을 어떻게 설명할 수 있을지 궁금합니다.
첫번째 예시의 두번째 Task 에서는 [valueCounter] in
으로 현재 값을 캡처하려고해도, 이미 첫번째로 정의된 Task 에서 valueCounter.increment()
를 호출하면서 다른 스레드 (편의상) 에서 값을 변경하고 있기 때문에, 동일 시점에 딱 한개의 쓰레드에서의 접근이 깨져서 이런 에러가 발생하는 걸까요? (그렇다기엔 에러 위치는 첫번째 Task 정의에서 떠서... 아닌가 싶기도하고요..)
두번째 예시의 첫번째 Task 에서는 캡처 리스트로 값을 캡처해서 valueCounter.increment()
를 호출하고, 두번째 Task 는 valueCounter.increment()
를 하려고해도 이 시점에서 valueCounter 를 참조하고 있는건 이곳 뿐이기 때문에 (첫번째 Task 에서는 캡처해서 사용), 동일 시점에 딱 한개의 쓰레드에서의 접근이 보장되어서 에러가 발생하지 않는걸까요?
결과를 기준으로 나름대로 고민을 해봤는데, 어쨌든 다 추측이라서.. 혹시 이와 같은 현상을 어떻게 이해하면 될지 궁금합니다
감사합니다.
답변 1
0
안녕하세요! sujinnaljin 님!
질문주신 내용이
현재 강의의 48강 ~ 52강 내용하고 관련된 내용인데, 이번에 Xcode가 업데이트가 되면서 내부 메커니즘이 이번에 제대로 구현이 되었나 보네요.
(Region Based Isolation 관련 내용이예요, https://github.com/swiftlang/swift-evolution/blob/main/proposals/0414-region-based-isolation.md )
뒤에 강의에서 내용을 자세히 다루고 있으니.. 강의 내용을 보시면 되고,
그 전에 최대한 직관적이고 간단하게만 말씀드려보면,
ValueCounter타입은 값타입이긴 하지만, Task의 클로저 자체는 변수 주소를 캡처해서 사용하기 때문에 valueCounter 변수 주소가 한번 전달(sending)이 되어 버리면, 그 이후의 코드에서는 사용할 수가 없습니다. (그게 Swift Concurrency에서 새롭게 도입한 Shared Mutable State를 다루기 위한 내부 메커니즘이라고 보시면 됩니다.)
(즉, 위의 Task를 "1번 Task"라고 이름 붙이기로 하고, 아래 Task를 "2번 Task"라고 이름 붙이기로 하면.. 이미 1번 Task에서 valueCounter를 차지하고 써버리기로 된 것이기 때문에 2번 Task에서는 더이상 valueCounter의 변수에 조차 접근할 수 없어진 것입니다.)
func test() {
var valueCounter = ValueCounter()
Task {// valueCounter의 주소가 Task 내부로 완전 전달 (더이상 아래 코드에서 사용 불가)
print(valueCounter.increment())
print(valueCounter.value)
}
Task { [valueCounter] in // valueCounter 주소에 접근 불가
var newValueCounter = valueCounter
print(newValueCounter.increment())
print(newValueCounter.value)
}
}
다만, Task와 캡처리스트 순서가 바뀌면 가능한 것입니다. 왜냐면 이번에는 캡처리스트를 먼저 사용했기 때문에, valueCounter변수의 주소가 Task 내부로 완전 전달(sending)이 된 것이 아니기 때문에.. 그 이후의 코드인 Task에서 valueCounter 변수에 접근이 가능한 것입니다.
func test() {
var valueCounter = ValueCounter()
Task { [valueCounter] in // valueCounter 주소가 완전 전달이 된 것이 아님
var newValueCounter = valueCounter
print(newValueCounter.increment())
print(newValueCounter.value)
}
Task {// valueCounter 주소에 접근 가능
print(valueCounter.increment())
print(valueCounter.value)
}
}
네, 그래서 위와 같은 내용을 확인하시면 될 것 같고..
강의 후반부 48강 ~ 52강 내용과 sending키워드 내용까지를 잘 보시면 질문 주셨던 내용에 대해 명확하게 이해하실 수 있으실 것 같습니다.
(제가 강의 촬영 당시에는.. 컴파일러 내부에 sending키워드 관련 부분이 아마 정확하게 구현이 안되어 있어 크래시가 나지 않았는데, 정확하게 말씀드리면 해당 12강 강의 내용 중에 캡처리스트 부분에 대해 설명드리는 내용은 정확하게 컴파일러 업데이트가 되기 전 내용이라, 제대로 동작하고 있지 않은 것으로 보시면 될 것 같습니다. 즉, 지금 설명드린 내용이 Evolution문서와 정확하게 일치하게 컴파일러가 업데이트 된 내용이라고 보시면 됩니다.)
말씀드린 강의 후반부 내용 확인해보시고도 이해가 안되시는 부분이 있으시면 다시 질문주세요 !
감사합니다. :)