So, rather than lock large resources, we would break the processing into tiny tasks, each completely contained in a message object. Each message specified a new message to launch on completion. This way the messaging protocol is preventing resource contention rather than a lock.
[edit] The context involved some licenses software to which we only owned several instances. The original approach would attempt to grab a lock and run the batch. The final approach just created a queue for jobs and one consumer for each available license. Now, none of our compute is spent waiting on locks either.