Diggory
(Diggory)
1
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.
tera
2
Can you show a small self contained snippet that reproduces the issue?
Diggory
(Diggory)
3
Hmmm, that's weird. When I extract it down to an example it works fine in a Playground!

tera
4
I'd recommend to try in the real small project, it may work differently in playground.
Diggory
(Diggory)
5
Just tried the sample as a Swift Package and worked as well...
tera
6
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.
Diggory
(Diggory)
7
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
Diggory
(Diggory)
9
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.
tera
10
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.
Diggory
(Diggory)
11
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.
tera
12
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
Diggory
(Diggory)
13
I've reduced it right down.
Here's a package which demos it.
tera
14
Checked on mac - works fine for me.
lukasa
(Cory Benfield)
15
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
Diggory
(Diggory)
16
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!
tera
17
I wonder why I am not getting exclusivity violation runtime errors with this example. Double checked - the relevant settings are enabled in the project.
Diggory
(Diggory)
18
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.
lukasa
(Cory Benfield)
19
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