Overprotecting multithreaded code

Running long tasks in a background thread to keep the UI responsive is one of the main purposes of multithreading. A common code pattern for doing so would look like:

procedure TMainForm.BtnClick(Sender: TObject); begin TThread.CreateAnonymousThread( procedure begin DoSomeWork; end; end).Start; end;

However, such code often needs to show the results of that long work to the user. Working with the GUI from a background thread is not thread-safe, so such code should be executed in the context of the main thread:

procedure TMainForm.BtnClick(Sender: TObject); begin TThread.CreateAnonymousThread( procedure begin DoSomeWork; TThread.Synchronize(nil, procedure begin ShowResults; end); end; end).Start; end;

Because ShowResults will run in the context of the main thread, the code in ShowResults should be as minimal as possible—read: as fast as possible. Once you enter into the Synchronize (or Queue) block, you are no longer executing code in the background thread, but on the main thread. That means if the code in that block takes a long time to run, it will block the UI thread just the same way it would block it if you were running that piece of code directly in the main thread.

Sometimes, in attempts to avoid threading issues, developers overprotect code in background threads and run more code than is necessary inside synchronization blocks. Thread safety can be hard, especially if the developer has only just started sailing in multithreading waters. While overprotecting code can cause other issues, like deadlocks, generally it is better to protect a bit more code than not to protect enough.

However, I have seen cases where overprotecting literally looks like this:

procedure TMainForm.BtnClick(Sender: TObject); begin TThread.CreateAnonymousThread( procedure begin TThread.Synchronize(nil, procedure begin DoSomeWork; ShowResults; end); end; end).Start; end;

In the above example, no line of code is being executed in the background thread, and everything is running in the synchronization block. Such a coding pattern completely defeats the purpose of using threads, and running it will block the UI just the same as the following code:

procedure TMainForm.BtnClick(Sender: TObject); begin DoSomeWork; ShowResults; end;

Using Queue instead of Synchronize will produce similar effects, as both execute the code in the context of the main thread. The difference is that Synchronize will block the thread while it runs, and the thread will finish running after the Synchronize block is executed, while Queue is a non-blocking call.

Why would anyone write code like that?

It is hard to guess how some pieces of code come to life. In this case, I don't think that happened because the developer was overprotective. The most logical explanation is that the developer in question was completely unaware of what the Synchronize method actually does.

Or maybe there was a misunderstanding about how Synchronize gives you thread safety. If you are unaware that Synchronize executes code in the context of the main thread, calling all code running in a background thread inside a Synchronize block may seem like the logical thing to do. After all, you don't want to have threading problems.

If you take a look at the example again, and imagine that you don't know anything about how Synchronize works, it will seem like all the code that is supposed to run in the background thread will run in the background thread, including the code written in the Synchronize block.

Though it started as a totally unimaginable piece of code, suddenly, you can see why would someone write it like that.

Comments

  1. Hello,
    I believe that there are other considerations to make.
    What operations can I do in the background and synchronize?
    in synchronize you should only update the user GUI interface
    It is not very clear and documented whether database access or remote servers (REST) ... are thread safe.
    Moreover, I have seen that the same FMX application that uses Thread ... in windows behaves differently than macOS.
    In my macOS project it often crashes the software

    Antonello

    ReplyDelete
    Replies
    1. Synchronization is mostly for updating GUI. That does not mean that you cannot do anything else besides updating GUI. GUI can only be handled from the context of the main thread and that is why you have to use Synchronize and you cannot use any other thread safety mechanisms.

      For everything else it is usually better to use other kinds of protection, than to synchronize. But that also depends on the code and how much time some code needs to run.

      Working with databases, accessing web services or REST can be used in background threads, but thread safety depends on the actual code and context. Thread safety is not absolute it depends on how you use particular class and whether you share object instances or not. For instance, creating REST client, REST request and using them to retrieve data is only thread safe if only one thread uses them. That mean, you cannot share same REST client between different threads and use it to retrieve data in parallel.

      When you are considering whether something is thread safe or not, you need to determine whether you are sharing data (state) between threads and whether that data (state) can be changed by any of the threads. If it can be changed, then it is not thread safe.

      As far as difference in behavior between macOS and Windows, it is hard to say what is the problem without seeing exact code. Threads are bounded by OS implementations, and there may be other issues in either RTL or your code. Also Windows is far more forgiving platforms as far as bugs are concerned. Buggy code can seemingly run on Windows without problems, while same code will cause crashing on other platforms.

      Delete
    2. thanks, so for the REST example it is better to create the REST client object in the background thread and then destroy it ... so as not to use a shared REST client.

      Delete
    3. Yes, it is better to create REST client object in the background thread.

      Shared REST client is not thread safe at all, but you can safely use REST client created on form or created in context of the main thread if you can ensure that only single thread is using that client at the time.

      For instance, if user can click on the button to invoke REST data retrieval, then until that process in background thread is finished, you should prevent triggering another action with same REST client. You also need to make sure that form is not destroyed while background thread is still using it.

      Delete
  2. For Android, you don't have the choice : all Internet requests must be in the background.

    Juste a note about differences between Synchronize and Queue/ForceQueue : if you use variables from the calling code in your synchronized code, the value is obtained when your code is executed, not copied when you call the Synchronize/Queue/ForceQueue.

    ReplyDelete

Post a Comment

Popular posts from this blog

Coming in Delphi 12: Disabled Floating-Point Exceptions

Assigning result to a function from asynchronous code

Beware of loops and tasks