Avoiding "Simultaneous accesses to ..., but modification requires exclusive access." - automate action after modification of a property in a property observer

Hello,

I have a codable struct in my basic Linux executable which represents the per-install configurable properties. It's called AppConfig and it can read and write itself out to the filesystem.

I was hoping to automate this in a way, where if certain properties in this struct are altered then it would automatically save to disc.

So, I tried a very simple property wrapper and then a didSet property observer which would call the instance method to write the struct out to file, but both of these fail due to **Simultaneous accesses to 0x1011385e0, but modification requires exclusive access.**

From another thread here I see that this is due to ownership/safety - it's not ideal to read a property whilst it's being modified.

I would have thought that this would be safe, as didSet happens after the modification has happened, but I'm sure that there's a sensible implementation reason why this fails.

I suppose my real question is this: How does one get around this? How does one get safely notified that a property has changed?

My first thought was that I could use a short timer to delay the call to the function that writes the struct out to disc and therefore make sure that the 'modification' had passed. But this seems a bit of a hack.

Is there a better way?

Thanks.

Can you show a small self contained snippet that reproduces the issue?

Hmmm, that's weird. When I extract it down to an example it works fine in a Playground!

:thinking:

I'd recommend to try in the real small project, it may work differently in playground.

Just tried the sample as a Swift Package and worked as well...

Strange indeed.

Anyway, I used such approach where I wrote the changes value to the disk in the didSet, and it worked totally fine. I didn't use a property wrapper though, that might be the difference.

Maybe this is the issue: in my actual app there is a web server which acts as a UI (Hummingbird) which operates on not the main thread. The Config struct is a global variable. Maybe the web server is 'holding on' to the struct from another thread?

The standard exclusivity checks aren't thread safe, so if the exclusivity violation is happening across threads, you may not reliably see the check. However, if you enable Thread Sanitizer, you should get cross-thread exclusivity checking as well, along with the general data race checking TSan does. That might help reproduce the issue.

2 Likes

Thanks Joe. I'm not that familiar with multi-threading so will have to read-up on TSan (and threading in general). It looks like it's not the web server, maybe a race as both accesses are on the same thread and both in my own code.

That's the issue then: that global is getting accessed from more than one thread and that's a problem. Easy to test:

private var _realValue: T = ...
var value: T {
    get {
        dispatchPrecondition(.onQueue(theQueue))
        return _realValue
    }
    set {
        dispatchPrecondition(.onQueue(theQueue))
        _realValue = newValue
    }
}

Standard solutions: serialise access by using a lock, a queue, an actor.

OK, I've managed to reduce it down in the playground. Please excuse my ignorance - I'm sure I'm doing something very wrong here.

So - my initial Playground test used property observers and didn't crash, but when I added a property wrapper, it crashed just like the main project in Xcode.

//  AppConfiguration.swift

import Foundation



@propertyWrapper struct AutoSaveStringToDisc: Codable {
	var wrappedValue: String {
		
		//	Delayed
//		didSet {
//			//	Delay call to write out slightly so that we aren't in the mutation phase
//			let interval = 0.25
//			let delayTimer = Timer(fire: Date() + interval, interval: interval, repeats: false, block: {(t:Timer) -> Void in
//				_ = config.persistConfigToFilesystem()
//			})
//			RunLoop.main.add(delayTimer, forMode: RunLoop.Mode.common)
//		}
		
		didSet {
			_ = config.persistConfigToFilesystem()
		}

	}
}
							  

//	MARK: - AppConfig -

///	Struct to hold the global Config for the app.  Stored as JSON on file between launches.
public struct AppConfiguration: Codable {
	
	//	Ignore the enums that aren't codable
	enum CodingKeys: CodingKey {
		case configFolderPath
		case stringOne
		case stringTwo
	}
	

	//    MARK: - Properties -
	var configFolderPath: String  = "/tmp"
	
	var stringOne: String = "One" {
		didSet {
			persistConfigToFilesystem()
		}
	}
	
	var stringTwo: String = "Two" {
		didSet {
			persistConfigToFilesystem()
		}
	}

	@AutoSaveStringToDisc var stringThree: String = "Three"

	
	//	MARK: - Functions -
	
	///	Prints the config as JSON - Useful for copying to a file.
	func spitOutJSON() {
		print("INFO: Spitting out current config as JSON")
		do {
			let encodedData = try JSONEncoder().encode(self)
			let jsonString = String(data: encodedData, encoding: .utf8)
			print(jsonString ?? "Could not encode JSON")
		} catch {
			print(error)
		}
	}
		
	///	Writes the config out to flie as JSON
	func persistConfigToFilesystem() -> Bool {
		print("persisting appConfig to disc.")
		let path = configFolderPath + "/testConfig.json"
		let encoder = JSONEncoder()
		encoder.outputFormatting = .prettyPrinted
		do {
			let url = URL(fileURLWithPath: path)
			let data = try encoder.encode(self)
			//	The folder should be guaranteed to exist due to the housekeeping() in GateControl.
			try data.write(to: url, options: [.atomic])
		} catch {
			print("Could not write appConfig JSON to: \(path) - \( error)")
			return false
		}
		return true
	}
	
	mutating func loadConfigFromFilesystem() {
		let importPath = configFolderPath + "/testConfig.json"
		print("INFO: loading config from filesystem \(importPath)")
		var importedConfig: AppConfiguration
		do {
			if let importedJsonData = try String(contentsOfFile: importPath).data(using: .utf8) {
				importedConfig = try JSONDecoder().decode(AppConfiguration.self, from: importedJsonData)
			} else {
				print("ERROR: AppConfig: could not convert string to JSON \(importPath)")
				return
			}
		} catch {
			print("ERROR: AppConfig: Could not load config from JSON - \(importPath) - \(error)")
			return
		}
		//	We should now have a valid config instance.

		//	Swap ourselves for the imported instance.
		self = importedConfig
	}


}



class ClientClass {

	//	The current Timing Profile.
	var activeProfile = String()
	{
		didSet {
//			primeEventWatcher()
			config.stringOne = activeProfile
			config.stringThree = activeProfile
		}
	}

}




//	MARK: - Entry Point -


var config = AppConfiguration()
config.stringOne = "Hello"
config.stringTwo = "World"
config.stringThree = "DubDub Next Week..."

//testConfig.persistConfigToFilesystem()

let client = ClientClass()
client.activeProfile = "Test"


Which outputs:

persisting appConfig to disc.
persisting appConfig to disc.
Simultaneous accesses to 0x111beea90, but modification requires exclusive access.
Previous access (a modification) started at  (0x111be5640).
Current access (a read) started at:
0    libswiftCore.dylib                 0x00007ff817702010 swift::runtime::AccessSet::insert(swift::runtime::Access*, void*, void*, swift::ExclusivityFlags) + 442
1    libswiftCore.dylib                 0x00007ff817702270 swift_beginAccess + 66
6    com.apple.dt.Xcode.PlaygroundStub-macosx 0x000000010f1c3ea0 main + 0
7    CoreFoundation                     0x00007ff807ed7c00 __invoking___ + 140
8    CoreFoundation                     0x00007ff807ed79f0 -[NSInvocation invoke] + 305
9    CoreFoundation                     0x00007ff807f070c3 -[NSInvocation invokeWithTarget:] + 70
10   ViewBridge                         0x00007ff80fd2246e __68-[NSVB_ViewServiceImplicitAnimationDecodingProxy forwardInvocation:]_block_invoke_2 + 46
11   ViewBridge                         0x00007ff80fce75d7 -[NSViewServiceMarshal withHostWindowFrameAnimationInProgress:perform:] + 53
12   ViewBridge                         0x00007ff80fd223f4 __68-[NSVB_ViewServiceImplicitAnimationDecodingProxy forwardInvocation:]_block_invoke + 113
13   AppKit                             0x00007ff80afa0bd3 +[NSAnimationContext runAnimationGroup:] + 55
14   AppKit                             0x00007ff80afa0b69 +[NSAnimationContext runAnimationGroup:completionHandler:] + 82
15   ViewBridge                         0x00007ff80fd3ee64 runAnimationGroup + 133
16   ViewBridge                         0x00007ff80fcf3e97 +[NSVB_AnimationFencingSupport _animateWithAttributes:animations:] + 113
17   ViewBridge                         0x00007ff80fd22335 -[NSVB_ViewServiceImplicitAnimationDecodingProxy forwardInvocation:] + 143
18   CoreFoundation                     0x00007ff807ed6227 ___forwarding___ + 756
19   CoreFoundation                     0x00007ff807ed6120 _CF_forwarding_prep_0 + 120
20   CoreFoundation                     0x00007ff807ed7c00 __invoking___ + 140
21   CoreFoundation                     0x00007ff807ed79f0 -[NSInvocation invoke] + 305
22   CoreFoundation                     0x00007ff807f070c3 -[NSInvocation invokeWithTarget:] + 70
23   ViewBridge                         0x00007ff80fcea128 -[NSVB_QueueingProxy forwardInvocation:] + 321
24   CoreFoundation                     0x00007ff807ed6227 ___forwarding___ + 756
25   CoreFoundation                     0x00007ff807ed6120 _CF_forwarding_prep_0 + 120
26   CoreFoundation                     0x00007ff807ed7c00 __invoking___ + 140
27   CoreFoundation                     0x00007ff807ed79f0 -[NSInvocation invoke] + 305
28   CoreFoundation                     0x00007ff807f070c3 -[NSInvocation invokeWithTarget:] + 70
29   CoreFoundation                     0x00007ff807ed6227 ___forwarding___ + 756
30   CoreFoundation                     0x00007ff807ed6120 _CF_forwarding_prep_0 + 120
31   CoreFoundation                     0x00007ff807ed7c00 __invoking___ + 140
32   CoreFoundation                     0x00007ff807ed79f0 -[NSInvocation invoke] + 305
33   ViewBridge                         0x00007ff80fcafc31 __deferNSXPCInvocationOntoMainThread_block_invoke + 142
34   ViewBridge                         0x00007ff80fca42b4 __wrapBlockWithVoucher_block_invoke + 37
35   ViewBridge                         0x00007ff80fd40449 kNotRunningOnAppKitCompatibleThread_block_invoke + 323
36   CoreFoundation                     0x00007ff807ef3b65 __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__ + 12
37   CoreFoundation                     0x00007ff807ef391c __CFRunLoopDoBlocks + 398
38   CoreFoundation                     0x00007ff807ef259b __CFRunLoopRun + 898
39   CoreFoundation                     0x00007ff807ef1d01 CFRunLoopRunSpecific + 560
40   HIToolbox                          0x00007ff81196dc89 RunCurrentEventLoopInMode + 292
41   HIToolbox                          0x00007ff81196d92d ReceiveNextEventCommon + 199
42   HIToolbox                          0x00007ff81196d8d8 _BlockUntilNextEventMatchingListInModeWithFilter + 64
43   AppKit                             0x00007ff80af86276 _DPSNextEvent + 858
44   AppKit                             0x00007ff80af84fbc -[NSApplication(NSEvent) _nextEventMatchingEventMask:untilDate:inMode:dequeue:] + 1214
45   ViewBridge                         0x00007ff80fcc1417 __77-[NSViewServiceApplication vbNextEventMatchingMask:untilDate:inMode:dequeue:]_block_invoke + 111
46   ViewBridge                         0x00007ff80fcc11af -[NSViewServiceApplication _withToxicEventMonitorPerform:] + 114
47   ViewBridge                         0x00007ff80fcc1371 -[NSViewServiceApplication vbNextEventMatchingMask:untilDate:inMode:dequeue:] + 151
48   ViewBridge                         0x00007ff80fcae9fe -[NSViewServiceApplication nextEventMatchingMask:untilDate:inMode:dequeue:] + 99
49   AppKit                             0x00007ff80af7789e -[NSApplication run] + 586
50   AppKit                             0x00007ff80af4b9d1 NSApplicationMain + 817
51   libxpc.dylib                       0x00007ff807b6a4ee _xpc_objc_main + 867
52   libxpc.dylib                       0x00007ff807b6a197 xpc_main + 96
53   ViewBridge                         0x00007ff80fca9547 xpc_connection_handler + 0
54   ViewBridge                         0x00007ff80fcc3cbb NSViewServiceMain + 1789
55   com.apple.dt.Xcode.PlaygroundStub-macosx 0x000000010f1c3ea0 main + 39
56   dyld                               0x00007ff807abdcb0 start + 1903
Fatal access conflict detected.

I wouldn't trust playground: it's constantly reading variables (to show their current state) which might conflict with your writes to those variables. When I run your example in a playground I can see the crash you are talking about but when I run it as a console command line app it runs fine.

2 Likes

I've reduced it right down.

Here's a package which demos it.

Checked on mac - works fine for me.

Yeah, this code is incorrect. Specifically the recursive relationship between TestPropertyWrapper and global via var stringThree is not safe.

When you do global.stringThree = activeProfile in ClientClass, you begin a mutable access of global. Within that mutable access of global, the TestPropertyWrapper that wraps var stringThree makes a read access of global indirectly. This is forbidden: for the duration of the mutating access of global you are not allowed to touch the variable global from anywhere. This is The Law of Exclusivity.

1 Like

Thanks for explaining @lukasa I knew that It was bound to be my fault.

In fact as I reduced it down to the most basic case the code looks horrible!

I wonder why I am not getting exclusivity violation runtime errors with this example. Double checked - the relevant settings are enabled in the project.

Yes, that is odd. My Mac is Intel but I tried it on a Raspberry Pi out of curiosity but got the same error as my Mac.

There has been spotty enforcement of this in some cases in the past: Exclusive access duration relative to didSet. I'd recommend filing a bug on GitHub with a complete description of your environment (Xcode version, Swift version, macOS version, hardware version) and your project or compiler command line and test environment. That will let the Swift team nail it down.

1 Like