Description of Desired Behaviour
I have the following scenario:
- Component A can run freely as long as it is not blocked.
- Any other component can block the component A from running at any time.
- After blocking, those components may unblock A.
- Component A resumes running once ALL components that once blocked it "releases" the blocks. If there at least one component still blocking, the component A should not run.
Is there any canonical design/concept for this behaviour?
I have considered it to be a "infinite" Semaphore, but it kind of works like a reversed version of it. I have seen that C# has this CountdownEvent Class, but it looks like it is more than what I need.
Sample Implementation (In C#):
Public Interface:
public class ComponentA
{
public ComponentA();
public bool IsBlocked();
public Block RequestBlock();
}
public class Block()
{
public void Release();
}
Usage:
ComponentA myComponent = new ComponentA();
myComponent.IsBlocked(); // false
Block firstBlock = myComponent.RequestBlock();
myComponent.IsBlocked(); // true
Block secondBlock = myComponent.RequestBlock();
myComponent.IsBlocked(); // true
firstBlock.Release();
myComponent.IsBlocked(); // true
secondBlock.Release();
myComponent.IsBlocked(); // false
Implementation Details:
public class ComponentA
{
private int _numBlocks;
public ComponentA() {
this._numBlocks = 0;
}
public bool IsBlocked() {
return _numBlocks > 0;
}
public Block RequestBlock() {
this._numBlocks++;
return new Block(() => { this._numBlocks--; });
}
}
public class Block
{
private Action _unBlockAction;
internal Block(Action unblockAction) {
this._unBlockAction = unblockAction;
}
public void Release() {
this._unBlockAction();
}
}
3 Answers 3
You describe a Readers-Writer Lock, but your readers can read after write has started and thread-safety is absent.
I think it is inconsistent to disallow a player to open inventory in dialog, but to allow a player to enter a dialog when inventory is open. If you make requirements symmetric, however, they will exactly match requirements for Readers-Writer lock.
I don't think there is a design pattern per se. You just have to pick the appropriate synchronization primitive.
If you do not need to drain the callers, if you just need to keep new callers from proceeding, you can use a manual reset event. This is like taxiing at an airport, the pilot needs permission from the tower, but ground control could stop issuing permits but not wait for you to arrive at the gate. There is no wait to issue the block.
If you need an exclusive semantic, use a acquire shared/exclusive lock like a reader/writer lock. If tower wants the runway de-iced, they certainly would want to wait until all airplanes which have been issued permission to land have actually landed. There is a wait to issue the block. (Technically, you could not wait for the readers to drain, but who would issue the un-block?)
What this boils down to is State Management, and there are several ways to address this. In the world where there is a user interface, it's common to have an MVVM architecture.
In MVVM, C# you would implement the ICommand
interface to open your inventory window implemented to check the state of all the other models that represent the different game systems. For example:
public class InventoryCommand : ICommand
{
public event EventHandler CanExecuteChanged;
public bool CanExecute(object context)
{
var world = context as World;
bool canExecute = world != null;
// do all your checks here on your game state
canExecute = canExecute && ....;
return canExecute;
}
public void Execute(object context)
{
// actually do the thing here
}
public void RaiseCanExecuteChanged()
{
CanExecuteChanged?.Invoke(EventArgs.Empty);
}
}
All that's left at this stage is to call the RaiseCanExecuteChanged()
method when the world state changes in a way you care about. The InventoryCommand
would be bound to the controls that open the inventory (buttons, key combination, etc.). At least this is the way that the MVVM approach works.
Your solution to the problem is essentially another way to handle the state management, but using a naive garbage collection mechanism called Reference Counting.
The implementation does leave something to be desired because there is no way to force that references to be cleaned up. You are relying on the good graces of the user to remember to release the block appropriately. You also need to prevent the user from accidentally calling the Release
method in a loop. Those are a couple reasons that reference counting is not something that is in common use in garbage collected languages these days.
If we change your Block
implementation to use the IDisposable
pattern, we can take advantage of C#'s garbage collection and using
construct to automate release of the blocks:
internal class Block : IDisposable
{
private Action onComplete;
public Block(Action completeAction)
{
onComplete = completeAction;
}
~Block()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this); // prevent second call in finalizer
}
private void Dispose(bool calledExplicitly)
{
onComplete?.Invoke();
onComplete = null; // clear it to prevent double calls
}
}
Your ComponentA.RequestBlock
class would have to be changed like this:
public IDisposable RequestBlock() // or AcquireBlock()
{
this._numBlocks++;
return new Block(() => { this._numBlocks--; });
}
That leaves the correct way to use it like this:
using(inventory.RequestBlock())
{
// do things, the block is automatically released
// when the using block ends.
}
Also, if the programmer forgets the using
statement, the Block
will eventually get captured by the garbage collector and the counter will decrement. The implementation here ensures the action is called once, and only once.
Explore related questions
See similar questions with these tags.
Block
implemented theIDisposable
interface it can be used in ausing
block making the release more guaranteed. All in all, I think just using aBarrier
is a safer way to ensure all initialization is complete before moving forward.