Reputation: 1279
I know, global variables are "not sexy", but I have few in my current project. I played around with Xcode's Thread Sanitizer and found a data race on them. So I tried to make them thread safe.
As I also want a single point of management for this variables. I tried to do the GCD stuff in getter and setter of the variables.
Finally I found a solution that worked, was accepted by the compiler and the Thread Sanitizer was happy .... BUT... this solution looks quite ugly (see below) and is very slow (did a performance test and it was VERY slow).
Yes, I know, if I use classes for this it might be more "swifty", but there must be an easy solution for a thread safe global variable.
So would you be so kind and give hints and suggestions to optimize this attempt? Anyt hint / idea/ suggestion / comment is welcomed!
// I used a "computed variable", to overcome the compiler errors,
// we need a helper variable to store the actual value.
var globalVariable_Value : Int = 0
// this is the global "variable" we worked with
var globalVariable : Int {
// the setter
set (newValue) {
globalDataQueue.async(flags: .barrier) {
globalVariable_Value = newValue
}
}
// the getter
get {
// we need a helper variable to store the result.
// inside a void closure you are not allow to "return"
var result : Int = 0
globalDataQueue.sync{
result = globalVariable_Value
}
return result
}
}
// usage
globalVariable = 1
print ("globalVariable == \(globalVariable)")
globalVariable += 1
print ("globalVariable == \(globalVariable)")
// output
// globalVariable == 1
// globalVariable == 2
Upvotes: 3
Views: 3512
Reputation: 1279
OOPer asked me to redo the performance tests as found the result strange.
Well, he was right. I did write a simple app (Performance Test App on GitHub) and attached some screenshots.
I run that tests on an iPhone SE with latest IOS. The app was started on the device, not in Xcode. compiler settings were "debug" for all shown test results. I did also test with "full optimizations" (smallest fastest [-Os]), but the results were very similar. I think in that simple tests is not much to optimize.
The test app simply runs the tests described in the answer above. To make it a little bit more realistic, it is doing each test on three parallel async dispatched queues with the qos classes .userInteractive, .default and .background.
There might be better ways to test such things. But for the purpose of this question, I think it's good enough.
I'm happy if anybody would reassess the code and maybe find better test algorithms ... we all would learn from it. I stop my work on this now.
The results are quite strange in my eyes. All three different approaches gives roughly the same performance. On each run there was another "hero", so I assume it is just influenced by other background tasks etc. So even Itai Ferber "nice" solution has in practice no benefit. It's "just" a more elegant code.
And yes, the thread save solution is WAY slower than the not queued solution.
This is the main learning: Yes, it's possible to make global variables thread safe, but there is a significant performance issue with it.
Upvotes: 1
Reputation: 1279
EDIT: I leave this first answer in to keep the history, but a hint of OOPer has lead to a total different view (see next answer).
First of all: I'm quite impressed how fast and well educated the answers flow in (we are on a weekend!)
So the suggestion of Itai Ferber was very good one, and as he asked, I did some performance tests, just to give him something in return ;-)
I run the test with the attached code in a playground. And as you see this is by far not a well designed performance test, it is just a simple test to get a gist of the performance impact. I did several iterations (see table below).
Again: I did it in a Playground, so the absolute times will be much better in a "real" app, but the differences between the tests will be very similar.
Key findings:
interactions shows linear behavior (as expected)
"My" solution (test1) is about 15 times slower than an "un-queued" global variable (test0)
I did a test were I used an additional global variable as the helper variable (test2), this is slightly faster, but not a real break through
The suggested solution from Itai Ferber (test3) is about 6 to 7 times slower that the pure global variable (test0)... so it is twice as fast as "my" solution
So alternative 3 does not only look better, as it doesn't need the overhead for the helper variable it is also faster.
// the queue to synchronze data access, it's a concurrent one
fileprivate let globalDataQueue = DispatchQueue(
label: "com.ACME.globalDataQueue",
attributes: .concurrent)
// ------------------------------------------------------------------------------------------------
// Base Version: Just a global variable
// this is the global "variable" we worked with
var globalVariable : Int = 0
// ------------------------------------------------------------------------------------------------
// Alternative 1: with concurrent queue, helper variable insider getter
// As I used a calculated variable, to overcome the compiler errors, we need a helper variable
// to store the actual value.
var globalVariable1_Value : Int = 0
// this is the global "variable" we worked with
var globalVariable1 : Int {
set (newValue) {
globalDataQueue.async(flags: .barrier) {
globalVariable1_Value = newValue
}
}
get {
// we need a helper variable to store the result.
// inside a void closure you are not allow to "return"
var globalVariable1_Helper : Int = 0
globalDataQueue.sync{
globalVariable1_Helper = globalVariable1_Value
}
return globalVariable1_Helper
}
}
// ------------------------------------------------------------------------------------------------
// Alternative 2: with concurrent queue, helper variable as additional global variable
// As I used a calculated variable, to overcome the compiler errors, we need a helper variable
// to store the actual value.
var globalVariable2_Value : Int = 0
var globalVariable2_Helper : Int = 0
// this is the global "variable" we worked with
var globalVariable2 : Int {
// the setter
set (newValue) {
globalDataQueue.async(flags: .barrier) {
globalVariable2_Value = newValue
}
}
// the getter
get {
globalDataQueue.sync{
globalVariable2_Helper = globalVariable2_Value
}
return globalVariable2_Helper
}
}
// ------------------------------------------------------------------------------------------------
// Alternative 3: with concurrent queue, no helper variable as Itai Ferber suggested
// "compact" design
var globalVariable3_Value : Int = 0
var globalVariable3 : Int {
set (newValue) {
globalDataQueue.async(flags: .barrier) { globalVariable3_Value = newValue }
}
get {
return globalDataQueue.sync { globalVariable3_Value }
}
}
// ------------------------------------------------------------------------------------------------
// -- Testing
// variable for read test
var testVar = 0
let numberOfInterations = 2
// Test 0
print ("\nStart test0: simple global variable, not thread safe")
let startTime = CFAbsoluteTimeGetCurrent()
for _ in 0 ..< numberOfInterations {
testVar = globalVariable
globalVariable += 1
}
let endTime = CFAbsoluteTimeGetCurrent()
let timeDiff = endTime - startTime
print("globalVariable == \(globalVariable), test0 time needed \(timeDiff) seconds")
// Test 1
testVar = 0
print ("\nStart test1: concurrent queue, helper variable inside getter")
let startTime1 = CFAbsoluteTimeGetCurrent()
for _ in 0 ..< numberOfInterations {
testVar = globalVariable1
globalVariable1 += 1
}
let endTime1 = CFAbsoluteTimeGetCurrent()
let timeDiff1 = endTime1 - startTime1
print("globalVariable == \(globalVariable1), test1 time needed \(timeDiff1) seconds")
// Test 2
testVar = 0
print ("\nStart test2: with concurrent queue, helper variable as an additional global variable")
let startTime2 = CFAbsoluteTimeGetCurrent()
for _ in 0 ..< numberOfInterations {
testVar = globalVariable2
globalVariable2 += 1
}
let endTime2 = CFAbsoluteTimeGetCurrent()
let timeDiff2 = endTime2 - startTime2
print("globalVariable == \(globalVariable2), test2 time needed \(timeDiff2) seconds")
// Test 3
testVar = 0
print ("\nStart test3: with concurrent queue, no helper variable as Itai Ferber suggested")
let startTime3 = CFAbsoluteTimeGetCurrent()
for _ in 0 ..< numberOfInterations {
testVar = globalVariable3
globalVariable3 += 1
}
let endTime3 = CFAbsoluteTimeGetCurrent()
let timeDiff3 = endTime3 - startTime3
print("globalVariable == \(globalVariable3), test3 time needed \(timeDiff3) seconds")
Upvotes: 0