Explain lock() command?

Hello guys.
I’ve seen in some examples that they use a lock()and unlock() command in a clock timer when setting items from UI …
Can someone please explain what this command actually does?

The explain like I’m 5 explanation is:

Rules run in a parallel fashion. More than one rule can be actively running at the same time. This is a good thing because it allows openHAB to make better use of the computer’s resources.

However, if you have two Rules running at the same time (it could be the same Rule but just triggered twice really fast) these two rules can potentially stomp all over each other. For example consider the following rule:

rule "Rule Stomping"
when
    Item MyTriggerItem received update
then
    MyActionItem.sendCommand(if(MyActionItem.state == ON) OFF else ON)
    Thread::sleep(1000)
    MyActionItem.sendCommand(if(MyActionItem.state == OFF) ON else OFF)
end

In the above rule, when MyTriggerItem receives an update, MyActionItem is toggled, sleeps for a second, then toggled again. At the end of the rule MyActionItem will be in the same state as it started. Assuming MyActionItem was ON to start:

Instance 1: MyActionItem == OFF
Instance 1: Sleep for 1 second
Instance 1: MyActionItem == ON

However, if the Rule is triggered and half a second later triggered again, you will have two instances of the Rule running at the same time. So what will happen is, again assuming MyActionItem was ON:

Instance 1: Toggles MyActionItem OFF
Instance 1: Sleeps 1 second
Instance 2: Toggles MyActionItem ON
Instance 2: Sleeps 1 second
Instance 1: Toggles MyActionItem OFF
Instance 2: Toggles MyActionItem ON

So, rather than a nice controlled one second toggling cycle as expected, you end up with two half second toggles.

This may not matter or it may matter a whole bunch. For example, some relays and other devices can be damaged if switched on and off too fast.

If it matters, you can prevent the two instances of the Rules from running at the same time by requiring them to grab a resource (i.e. a lock) before proceeding and releasing that resource when it is done.

If we add a lock to the above example:

include java.util.concurrent.locks.ReentrantLock

val ReentrantLock stompingLock = new ReentrantLock

rule "Rule Stomping"
when
    Item MyTriggerItem received update
then
    try {
        stompingLock.lock()
        MyActionItem.sendCommand(if(MyActionItem.state == ON) OFF else ON)
        Thread::sleep(1000)
        MyActionItem.sendCommand(if(MyActionItem.state == OFF) ON else OFF)
    } catch(Throwable t) { }
    finally {
        stompingLock.unlock()
    }
end

In the changes above we created a resource (stompingLock) that each thread must acquire by calling lock() before it will proceed. If another rule called lock() first, the second rule will go to sleep and wait until that first thread calls unlock(). Thus the code between the lock and the unlock can only be executed by one instnace of that Rule at a time. The behavior changes to:

Instance 1: Toggles MyActionItem OFF
Instance 1: Sleeps 1 second
Instance 1: Toggles MyActionItem ON
Instance 2: Toggles MyActionItem OFF
Instance 2: Sleeps 1 second
Instance 2: Toggles MyActionItem ON

As you can see, the two rules now no longer process in parallel but instead in sequence.

The try, catch, finally stuff added is a bit of error handling code. If for some reason there is an error in your code between the lock and the unlock that causes an exception to be thrown and you do not have the try, catch, finally in place then the rule will not have the chance to release the lock by calling unlock and no future triggering of the rule will ever get to run until OH is restarted. By putting the unlock inside the finally clause, even if there is an error the unlock will be called, releasing te lock and allowing future instances of the rule to execute.

This can be used to keep two separate Rules from executing at the same time (just have both rules acquire the same lock before running) or to have rules ignore triggers while another one is running (only execute the logic of the rule is stompingLock.isLocked returns false).

7 Likes

To piggy back on @Rasmus7700 question, when would I want to not use a lock? It seems to me like it would be far better (in my case) to have every rule I make sit within its own lock. This would still allow all the individual rules to function concurrently, but not allow said rules to re-run before completion.

tl;dr, don’t use locks unless you really know what you are doing, and even then don’t use them. There is almost always a better way.

99.9% of the time you don’t need locks. You only need locks if:

  1. You have a provable situation where the triggers for a Rule are likely, in an non-error case, to occur fast enough that you end up with multiple instances of the rule executing at the same time. This already limits the number of cases down to those with a Thread::sleep. Alternatively you have two Rules which operate on the same Items which are likely to run at the same time. This is more likely but easily handle able without resorting to locks.

  2. If you have 1 then if having multiple instances of the rule running at the same time is likely to cause negative in-determinant behavior. In the example I have above, if the rapid turning on and off of the device isn’t a problem and I’m controlling something like the HVAC, who cares if multiple instances of the rule execute at the same time? In the end I am left with the same result. If the Rule does just one thing every time, again who cares if it happens to do it twice? It is only a problem if there is some limitation (e.g. sending two ONs to a device really close together causes a problem) or the order of operations matter (e.g. you don’t want two instances running at the same time because they interfere with each other which is what I tried to show in the example).

  3. If you have 1 and 2, then use a lock only if you can find no other way to accomplish what you are trying to do. Perhaps rework your rules or Items so having them run at the same time doesn’t matter. Perhaps implement a proxy or a Timer to filter or ignore the second event while the first one is still running.

Locks are a dangerous thing to use. They can cause deadlock situations (e.g. there are two resources needed by two rules, one grabs one lock, the other rule grabs the second lock and now both are stuck waiting for the lock they don’t have).

Locks make it really hard for the Thread pool, Thread scheduler, and even the down to the CPU scheduler to optimize the execution of your code because it is much more difficult to predict what needs to be executed next when you have Threads locked.

Implementing locking and unlocking in a safe and correct manner is a hard task for the experienced programmer to get right. Most programmers I’ve worked with over the years, including myself, treat locks like “goto”. It can sometimes get you out of a pinch but you had better have a REALLY good reason to use it because the negatives far outweigh the positives on almost all cases. Most of the time I’ve seen locks used (or goto), including in my own code, it is out of laziness rather than because of need.

And in the vast majority of cases, particularly in the domain of openHAB Rules, they are simply unnecessary. They add a bunch of lines of code and global vars which effectively do nothing except give the rule’s coder a nice fuzzy feeling that they have somehow made their Rules be more deterministic. But the feeling is a false one because unless your home automation system is generating events that cause a Rule to be triggered so fast that you have multiple instances running at the same time routinely and not as an error your Rules are already running perfectly deterministicly.

Implementing locks like this would fall into the same categorization as premature optimization. Don’t spend time complicating your code solving something that isn’t actually a problem.

If you want to spend your time on something, spend it on making it so even if your rules are executing multiple instances at the same time it doesn’t matter. This will result in simpler, safer, and more error resistant code and it will allow your rules to run as fast and efficiently as possible.

7 Likes

As always @rlkoshak you’ve answered my question and several others, too.

If you’re ever in New Zealand gimme a bell because i’m sure I owe you more than a few cold beers by now heh.

1 Like

Thanks a lot for the great explanation… I saw the rule thing was used in a sample for creating a time clock - so it only triggers once even if you change multiple things, which makes great sense according to the explanation above