Since they launched last year, I've spent a lot of time with Xojo preemptive threads, both with brand new apps built from scratch for preemptive threading and with existing apps retrofitted for preemptive threading. They are a double-edged sword that gives you incredible power but also incredible danger.
The greatest threat is deadlocking your code, which occurs when two or more threads wait for a lock that will never be released. I'll show you some common pitfalls and solutions, as well as a brilliant way to make your life easier and other potential problems to look out for.
The Locking Problem
Using either CriticalSection or Semaphore to lock parts of your code is a critical part of implementing preemptive threading. Locks work like bouncers who control how many people can enter a building at once. You define how many people the bouncer allows in at once. A CriticalSection allows one person at a time, while a Semaphore allows a specified number of people. When someone leaves, the bouncer lets the next person in.
The problem arises when someone sneaks out the bathroom window. The bouncer won't know that someone left and will prevent someone else from taking their place forever. In your code, this is what happens when you accidentally leave a lock held.
Consider the following code, where mLock is a CriticalSection defined in the same module as this method:
mLock.Enter
Var Dict As Dictionary
Dict.Value("One") = 1
mLock.Leave
Setting aside the fact that you don't need to lock a local variable, did you catch the problem? Dict was never initialized, so a NilObjectException will be raised before the lock can be released. If you're using an unhandled exception handler to keep your code running, there's now a problem because another thread can never enter that lock. This is a locking imbalance.
This is also a great example of why it's a bad idea to continue running after the Application.UnhandledException event has fired. It indicates that something unexpected has gone wrong and your code is now in an unpredictable state. It's a rule I often violate myself, but it's still a bad idea.
Defending Your Locks
The code could be improved using a try block.
mLock.Enter
Try
Var Dict As Dictionary
Dict.Value("One") = 1
mLock.Leave
Catch Err As RuntimeException
mLock.Leave
Raise Err
End Try
This works, but I find it kind of ugly that I have to remember to unlock in both the try and catch sections. Instead, let's consider reporting the exception.
mLock.Enter
Try
Var Dict As Dictionary
Dict.Value("One") = 1
Catch Err As RuntimeException
App.ReportException(Err)
End Try
mLock.Leave
This certainly looks nicer, but it introduces a new problem: EndException and ThreadEndException. These are triggered when your app tries to quit or terminate a thread. You should not catch them. Okay, so let's update the code accordingly.
mLock.Enter
Try
Var Dict As Dictionary
Dict.Value("One") = 1
Catch Err As RuntimeException
If Err IsA EndException Or Err IsA ThreadEndException Then
mLock.Leave
Raise Err
End If
App.ReportException(Err)
End Try
mLock.Leave
While this is technically sufficient, it requires writing and maintaining a lot of boilerplate code every time you need to obtain a lock.
The Brilliant Solution
Kem Tekinay's LockHolder class will make your life so much easier, that it really should be something built into Xojo.
It works by passing a lock object to a new instance of this class. When the class goes out of scope, the destructor fires and releases the lock. Therefore, it doesn't matter if an exception is thrown; your lock will be released. Our example code could then be written as follows:
Var Holder As New LockHolder(mLock)
Var Dict As Dictionary
Dict.Value("One") = 1
That's it! If you need to unlock it during execution, simply set the variable to nil. This makes lock balancing nearly foolproof.
Only Nearly Foolproof?
There are still ways your code can get into trouble. Polling and explicit yields, in both your code and plugin code, are especially dangerous.
Bad Practice Traps
For example, consider this code that waits for a socket to send data before closing.
Var Holder As New LockHolder(mLock)
// Do some work
While Socket.BytesLeftToSend > 0
Socket.Poll()
Thread.SleepCurrent(10)
Wend
The Thread.SleepCurrent method will cause the current thread to sleep. However, if executed on the main thread, a new iteration of the main loop will start, creating the illusion of multitasking. This is essentially the same as the problematic App.DoEvents, and with preemptive threading, it can easily cause a deadlock. Even with cooperative threading, this code can cause a deadlock; it's just more difficult.
For this particular example, you'll need to use the SendComplete event. Yes, this makes the code more difficult, but now you can see why a polling loop is considered a bad practice.
Good Practice Traps
Consider the following example of connecting to a database and inserting a record.
Var Connection As New PostgreSQLDatabase
Connection.Host = "127.0.0.1"
Connection.Port = 5432
Connection.UserName = "thom"
Connection.Password = "this isn't a real password"
Connection.DatabaseName = "testing"
Connection.Connect
Connection.BeginTransaction
Connection.ExecuteSQL("INSERT INTO mytable (col1, col2) VALUES ($1, $2);", "Value1", "Value2")
Connection.CommitTransaction
Have you noticed how this code is dangerous? No, it's not because of the lack of an exception handler. I bet you haven't because, aside from the lack of error handling, this is a very respectable block of code.
The problem stems from the MultiThreaded property, which is set to true by default. I didn't include it here to make it as obvious as it would be in real code. When this property is enabled, the database plugin yields during statements. As in the last example, this causes a new iteration of the main event loop to occur in the middle of your existing code. Have you ever seen a stack trace in which a timer's action appears mixed in with completely unrelated code? I have, and, as in the previous example, it's the equivalent of calling App.DoEvents.
For preemptive threads, always disable any type of yielding. You don't need it. The SQLiteDatabase class has a ThreadYieldInterval property that should be set to 0 for preemptive threads. Fortunately, 0 is the default.
Conclusion
Preemptive threads are like the mythical Sirens. They'll tempt you with tales of amazing performance, and your initial tests will reinforce that temptation while lulling you into a false sense of security. Then, when you think you've got it figured out and launch it in production, they'll drown you. They'll exploit every flaw in your code, leaving you desperate for answers. There will be few of those. Due to the sunk cost fallacy, you'll spend countless hours trying to fix the problem.
In other words, Xojo was right to avoid these for so long. If you don't absolutely need preemptive threads, don't use them. Use workers or multiple processes instead. Your sanity will thank you.
That said, if you can tame them, they can give you incredible power.
