Swift에서 값 유형이 있는 데이터 경합
String
)을 조작할 때 잘못된 스레딩 동기화로 인해 interesting discussion around a possible data race condition이 발생했습니다.버그 코드
final class MyClass {
var token: String
init(_ token: String = "") {
self.token = token
}
func myMethod() -> Bool {
token.isEmpty
}
}
언뜻 보기에 이것이 올바른 것처럼 보일 수 있습니다. 값 유형인
var
가 있는 String
와 token
에서 isEmpty
만 호출하는 String
가 비어 있는지 확인하는 메서드가 있습니다. 간단한 코드와 안전한 권리? 뭐, 스레딩을 도입하지 않는 한 괜찮지만, 스레딩을 도입하는 순간은 그렇지 않을 것입니다. 자세히 설명하겠습니다.시험
Thread Sanitizer를 활성화한 상태에서 이 테스트를 실행하는 경우:
func test_data_race() {
let sut = MyClass()
DispatchQueue.concurrentPerform(iterations: 1_000_000) { i in
sut.token = "\(i)"
_ = sut.myMethod()
}
}
다음 출력이 표시됩니다.
WARNING: ThreadSanitizer: data race (pid=8329)
Read of size 8 at 0x000107c1aab8 by thread T2:
#0 closure #1 in DataTests.test_data_race() DataTests.swift:69 (Tests:arm64+0xde354)
#1 partial apply for closure #1 in DataTests.test_data_race() <compiler-generated> (Tests:arm64+0xde3e4)
#2 partial apply for thunk for @callee_guaranteed (@unowned Int) -> () <null>:73675156 (libswiftDispatch.dylib:arm64+0x42f4)
#3 _dispatch_client_callout2 <null>:73675156 (libdispatch.dylib:arm64+0x35dc)
Previous write of size 8 at 0x000107c1aab8 by main thread:
#0 closure #1 in DataTests.test_data_race() DataTests.swift:69 (Tests:arm64+0xde374)
#1 partial apply for closure #1 in DataTests.test_data_race() <compiler-generated> (Tests:arm64+0xde3e4)
#2 partial apply for thunk for @callee_guaranteed (@unowned Int) -> () <null>:73675156 (libswiftDispatch.dylib:arm64+0x42f4)
#3 _dispatch_client_callout2 <null>:73675156 (libdispatch.dylib:arm64+0x35dc)
#4 _swift_dispatch_apply_current <null>:73675156 (libswiftDispatch.dylib:arm64+0x43a0)
#5 @objc DataTests.test_data_race() <compiler-generated> (Tests:arm64+0xde448)
#6 __invoking___ <null>:73675156 (CoreFoundation:arm64+0x11c5ec)
Location is heap block of size 32 at 0x000107c1aaa0 allocated by main thread:
#0 __sanitizer_mz_malloc <null>:73675156 (libclang_rt.tsan_iossim_dynamic.dylib:arm64+0x51004)
#1 _malloc_zone_malloc <null>:73675156 (libsystem_malloc.dylib:arm64+0x1527c)
#2 DataTests.test_data_race() DataTests.swift:66 (Tests:arm64+0xde07c)
#3 @objc DataTests.test_data_race() <compiler-generated> (Tests:arm64+0xde448)
#4 __invoking___ <null>:73675156 (CoreFoundation:arm64+0x11c5ec)
Thread T2 (tid=6246748, running) is a GCD worker thread
SUMMARY: ThreadSanitizer: data race DataTests.swift:69 in closure #1 in DataTests.test_data_race()
따라서 ThreadSanitizer는 토큰에 액세스할 때 코드에서 데이터 경합을 감지합니다.
무슨 뜻인가요? 기본적으로 변수를 잘못 사용하고 있습니다. 읽기 및 쓰기 작업을 동시에 수행하지만 변수 자체는 보호되지 않으며 값 유형이라는 사실은 도움이 되지 않습니다.
이것이 무엇을 일으킬 수 있습니까? 정의되지 않았지만 실제로는 컴파일 최적화가 활성화되면 충돌이 발생할 가능성이 높습니다.
수정
자, 이 간단한 코드는 다른 스레드에서 병렬로 토큰을 읽고 쓸 때 충돌할 수 있습니다! 어떻게 고칠 수 있습니까? 읽기/쓰기를 위해 직렬 액세스를 만들기만 하면 됩니다. 이를 수행하는 방법에는 여러 가지가 있지만(다른 프리미티브 사용) 다음 중 하나일 수 있습니다.
final class MyClass {
private let syncQueue = DispatchQueue(label: "com.test.myQueue", attributes: .concurrent)
private var _token: String
var token: String {
get {
syncQueue.sync {
_token
}
}
set {
syncQueue.async(flags: .barrier) {
_token = newValue
}
}
}
init(_ token: String = "") {
_token = token
}
func myMethod() -> Bool {
token.isEmpty
}
}
보시다시피 직렬 쓰기를 강제하여
var
를 보호하여 다중 읽기가 발생할 수 있지만 한 번에 하나의 스레드만 쓰기를 실행할 수 있습니다(장벽은 모든 이전 읽기가 완료되고 연기될 때까지 기다립니다) 쓰기가 완료될 때까지 모든 후속 읽기/쓰기 액세스). 결과 코드는 실행 속도가 느리지만 이제 안전합니다.마지막 생각들
Swift 커뮤니티에서 일반적으로 오해하는 이 문제에 대한 몇 가지 생각을 공유하고 싶습니다.
❌ 값 유형은 스레드로부터 안전합니다.
값 유형에는 복사 시맨틱이 있으므로 본질적으로 데이터 경합으로부터 보호된다고 생각하는 것이 논리적으로 보일 수 있습니다. 그러나 그렇지 않습니다. Swift는 값 유형에서 스레드 안전성을 보장하지 않으므로 여러 스레드에서 any
var
에 액세스하는 것은 잠재적인 데이터 경쟁 조건입니다. 물론 이 문제는 변수let
가 변경할 수 없기 때문에 적용되지 않습니다.❌ 값 유형은 항상 복사됩니다.
그것은 의미론적이지만 후드 아래에서 실제로 일어나는 일은 아닙니다. 값 유형을 전달할 때 Swift 컴파일러는 사본이 필요한지 알고 불필요한 사본을 제거할 만큼 똑똑합니다. 실제로는 CopyOnWrite(COW) 전략을 사용하는데, 값이 수정될 때만 복사본을 만들고 전달될 때는 만들지 않습니다. 결과적으로 대부분의 상황에서 값 유형을 사용하는 경우에도 실제로 동일한 기본 메모리 주소에 대한 포인터를 갖게 됩니다.
❌ 테스트는 항상 프로덕션 코드처럼 작동합니다.
테스트가 충돌하지 않는다는 사실이 프로덕션에서 일부 코드가 충돌할 수 없다고 주장하는 것은 아닙니다. 테스트는 시뮬레이션된 환경에서 실행되며 일반적으로 최종 빌드와 다른 컴파일 옵션이 있습니다. 예를 들어 ARC는 적절한 옵션으로 컴파일할 때 적극적인 최적화를 수행하므로 최종 빌드에서 많은 불필요한 보유/릴리스가 제거됩니다. 이 특별한 경우에는 내 테스트 슈트가 충돌하지 않았고 Thread Sanitizer를 활성화하여 일부 잘못된 사용법만 볼 수 있었습니다.
추가 읽기
Reference
이 문제에 관하여(Swift에서 값 유형이 있는 데이터 경합), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/playtomic/data-races-with-value-types-in-swift-5e45텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)