lock
statement.C# Example:
class MyClass {
private bool m_initialised = false;
private int m_value = 0;
void Initialise() {
lock(this) { // lock stops two threads initialising at the same time
if(m_initialised) {
return; // already initialised
}
m_value = LoadExpensiveValue();
m_initialised = true;
}
Console.WriteLine("initialised");
}
}
Java has
synchronized
, heck even Objective-C has @synchronized
- but swift has no such thing.If you google for it, you most probably wind up on stack overflow, probably at this article: What is the Swift equivalent to Objective-C's “@synchronized”?.
It boils down to the following
1. Use
objc_sync_enter
to lock2. Use
objc_sync_exit
to unlock3. Don't forget to use
defer
to make sure the unlock always happensNow, if you simply do the above, it's fine, however you (like me) are most probably tempted by that answer on that stack overflow post, which shows that you can write your own synchronised/lock function. Once you apply all the bells and whistles, you get something like this
Swift:
func lock(lockObj: AnyObject, closure: () throws ->T ) rethrows -> T
{
objc_sync_enter(lockObj)
defer { objc_sync_exit(lockObj) }
return try closure()
}
And you can use it like this!
Swift Example:
class MyClass {
private var _initialised = false
private var _value = 0
func initialise() {
lock(self) { // lock stops two threads initialising at the same time
if(_initialised) {
return // already initialised
}
_value = loadExpensiveValue();
_initialised = true
}
print("initialised");
}
}
How cool is that? Swift didn't have a lock keyword, but we were able to write one ourselves, and it's just as nice to use as the native ones from C#, Java or anywhere else! Hooray!
Unfortunately, it is also wrong and broken
The above code compiles with zero errors or warnings, and there are many cases in which you'll never realise there's a bug there. I was bitten by this exact thing just today, hence my motivation to write this post.
The bug is, in the swift implementation, the
print("initialised");
runs when _initialised
is true; If two threads call the function, they will *both* print it.Why? In swift, our
lock
keyword isn't a real keyword. It is a function which is being passed a closure. That is to say, the locked region is a closure, and not part of the outer function.
The return statement for the "already initialised" case returns from the closure, but does not return from the outer
initialise()
function itself. This is in contrast to languages with a real lock
keyword, which does not introduce a new return scope, and where the return statement actually returns from the outer function.My advice: Do not write or use this handy swift lock function. One day you too will write this bug, and when it occurs it's not at all obvious as to what's going on.
Do this instead.
class MyClass {
private var _initialised = false
private var _value = 0
func initialise() {
do {
objc_sync_enter(self); defer{ objc_sync_exit(self) }
if(_initialised) {
return // already initialised
}
_value = loadExpensiveValue();
_initialised = true
}
print("initialised");
}
}
The code is very slightly longer and very slightly uglier, however there are no closures, and the return keyword works how you expect it to.
Perhaps one day swift will add a proper lock keyword and we can all move on from this kind of thing
No comments:
Post a Comment