Async Execution and Blocking in Ignition

I recently was made aware an issue that got me thinking about scripting in asynchronous contexts in Ignition.

In this case, blocking functions such as sleep and system.tag.writeBlocking were being used in tag event scripts and appeared to be causing problems.

The proposed fix was to move that work into system.util.invokeAsynchronous.

The recommendation implied a few things to me, based off my experience with other languages and async runtimes:

  • Tag event scripts execute in a shared execution context with other scheduled tasks.
  • Blocking operations in that context can prevent other scheduled tasks from making progress.
  • system.util.invokeAsynchronous is intended to offload long-running or blocking work onto a thread pool where blocking is acceptable, so that the original execution context can continue.

In general, the main reasons I would expect to want to block a thread are:

  • Expensive CPU-bound work
  • Synchronous I/O

Those are the situations where I would expect to reach for something like system.util.invokeAsynchronous.

My questions are:

  1. Are my assumptions sound?
  2. If so is there documentation or guidance describing which Ignition scripting contexts users should avoid blocking in?
  3. Is there a need for more general education around this topic: what blocking means, when it is acceptable, and what are the strategies for managing blocking tasks?

One related thought I have is if the idea "write a value and wait" is a common anti-pattern. My understanding is that system.tag.writeBlocking waits for the write operation itself to complete, but that does not necessarily mean the new value has already been published back through the tag system. If a script then sleeps for some guessed amount of time hoping the value will update, that seems fragile. Even if that logic is moved to a thread where blocking is acceptable, it also seems like it could become problematic if many of these tasks stack up.

I would appreciate any clarification. I suspect part of my confusion is trying to map concepts from other runtimes/frameworks onto Ignition without fully understanding how Ignition manages these execution contexts internally.

Yes. Some detailed reading for you.

Key points:

  • Tag events (defined on the tag) run in a pool of three threads, by default. If you have three event scripts simultaneously sleeping or waiting on the network (DB, OPC, web APIs, whatever), all other tag events will stall.

  • system.util.invokeAsynchronous can solve the above, but launching threads is not a light-weight operation, and you can fill RAM with stalled threads. Extensive CPU bound work is a good reason to use a separate thread. Synchronous I/O usually is not.

  1. Your assumptions are close.
  2. Not really. Except here on the forum. The docs do mention the per-tag queues and the missed events flag, which implies limitations on tag events.
  3. It would be nice if there were good guides for selecting tag events versus gateway tag change events. (The latter use one thread and only one thread for the given list of tags, and are independent of other thread pools.)

Yes. Looping or waiting in a thread for some signal to change is very much an anti-pattern. The wait should be re-engineering into another tag event or tag change event, with a state machine to indicate what applies when. No waiting.

All that said, there are some things that are fine in tag events:

  • system.tag.readBlocking() to collect values from a bunch of other tags. This is near instant, as only current values are retrieved. The "blocking" aspect is that you get the results synchronously.
  • system.tag.writeBlocking() when the target is a memory tag. (Or multiple memory tags.)
  • system.tag.writeAsync()
  • system.db.runSF*() to queue updates in the Store and Forward system.