[swift-users] Data races with copy-on-write

Michel Fortin michel.fortin at michelf.ca
Tue Dec 5 08:23:56 CST 2017


The array *storage* is copy on write. The array variable (which essentially contains a pointer to the storage) is not copy on write. If you refer to the same array variable from multiple threads, you have a race. Rather, use a different copy of the variable to each thread. Copied variables will share the same storage but will make a copy of the storage when writing to it.

I'm not sure what is the problem with your SynchronizedArray example. But I would try reimplementing `var element` this way:

	var elements: Array<Element> {  
		return access { $0 }
	}  

If this change fixes the race it means the compiler is making the copy after the `unlock()` with your previous code, which could explain the detected race. I suppose the additional copy afterwards fixes the race because of an internal implementation detail (like changing the reference count for the storage). I'd be wary of the optimizer breaking this trick though.

> Le 5 déc. 2017 à 5:20, Romain Jacquinot via swift-users <swift-users at swift.org> a écrit :
> 
> Hi,
> 
> I'm trying to better understand how copy-on-write works, especially in a multithreaded environment, but there are a few things that confuse me.
> 
> From the documentation, it is said that:
> "If the instance passed as object is being accessed by multiple threads simultaneously, isKnownUniquelyReferenced(_:) may still return true. Therefore, you must only call this function from mutating methods with appropriate thread synchronization. That will ensure that isKnownUniquelyReferenced(_:) only returns true when there is really one accessor, or when there is a race condition, which is already undefined behavior."
> 
> Let's consider this sample code:
> 
> func mutateArray(_ array: [Int]) {  
> 	var elements = array  
> 	elements.append(1)  
> }  
> 
> let q1 = DispatchQueue(label: "testQ1")  
> let q2 = DispatchQueue(label: "testQ2")  
> let q3 = DispatchQueue(label: "testQ3")  
> 
> let iterations = 1000  
> 
> var array: [Int] = [1, 2, 3]  
> 
> q1.async {  
> 	for _ in 0..<iterations {  
> 		mutateArray(array)  
> 	}  
> }  
> 
> q2.async {  
> 	for _ in 0..<iterations {  
> 		mutateArray(array)  
> 	}  
> }  
> 
> q3.async {  
> 	for _ in 0..<iterations {  
> 		mutateArray(array)  
> 	}  
> }  
> 
> // ...  
> 
> From what I understand, since Array<T> implements copy-on-write, the array should be copied only when the mutating append(_:) method is called in mutateArray(_:). Therefore, isKnownUniquelyReferenced(_:) should be called to determine whether a copy is required or not.
> 
> However, it is being accessed by multiple threads simultaneously, which may cause a race condition, right? So why does the thread sanitizer never detect a race condition here? Is there some compiler optimization going on here?
> 
> On the other hand, the thread sanitizer always detects a race condition, for instance, if I add the following code to mutate directly the array:
> 
> for _ in 0..<iterations {  
> 	array.append(1)  
> }  
> 
> In this case, is it because I mutate the array buffer that is being copied from other threads?
> 
> 
> Even strangier, let's consider the following sample code:
> 
> class SynchronizedArray<Element> {  
> 
> 	// [...]  
> 
> 	private var lock = NSLock()  
> 	private var _elements: Array<Element>  
> 
> 	var elements: Array<Element> {  
> 		lock.lock()  
> 		defer { lock.unlock() }  
> 		return _elements  
> 	}  
> 	
> 	@discardableResult  
> 	public final func access<R>(_ closure: (inout T) throws -> R) rethrows -> R {  
> 		lock.lock()  
> 		defer { lock.unlock() }  
> 		return try closure(&_value)  
> 	}  
> }  
> 
> let syncArray = SynchronizedArray<Int>()  
> 
> func mutateArray() {  
> 	syncArray.access { array in  
> 		array.append(1)  
> 	}  
> 
> 	var elements = syncArray.elements  
> 	var copy = elements // [X] no race condition detected by TSan when I add this line  
> 	elements.append(1) // race condition detected by TSan (if previous line is missing)  
> }  
> 
> // Call mutateArray() from multiple threads like in the first sample code.  
> 
> The line marked with [X] does nothing useful, yet adding this line prevents the race condition at the next line to be detected by the thread sanitizer. Is this again because of some compiler optimization?
> 
> However, when the array buffer is being copied, we can mutate the same buffer with the append(_:) method, right? So, shouldn't the thread sanitizer detect a race condition here?
> 
> Please let me know if I ever misunderstood how copy-on-write works in Swift.
> 
> Also, I'd like to know:
> - besides capture lists, what are the correct ways to pass a copy-on-write value between threads?
> - for thread-safe classes that expose an array as a property, should I always copy the private array variable before returning it from the public getter? If so, is there any recommended way to force-copy a value type in Swift ?
> 
> Any help would be greatly appreciated.
> Thanks.
> 
> Note: I'm using Swift 4 with the latest Xcode version (9.2 (9C40b)) and the thread sanitizer enabled.
> 
> _______________________________________________
> swift-users mailing list
> swift-users at swift.org
> https://lists.swift.org/mailman/listinfo/swift-users



-- 
Michel Fortin
https://michelf.ca

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-users/attachments/20171205/e086e93c/attachment.html>


More information about the swift-users mailing list