I want to implement a threadsafe list, but the thread safety has to be ensured on the whole block of operations, not only on a single operation (eg. add, remove etc.) The use case should look like following:
list.Lock();
list.Add(sth);
list.RemoveAt(4);
list.Unlock();
I want the list to require the lock for any operations. For example:
list.Add(sth);
called without prior locking should result in exception. This is why I don't use the lock()
statement - lock checking is crucial for this solution.
Implementing such list using Monitor is not hard - but only until one wants to check, whether the list is locked or not. I thought of the following scenario:
// Inside list class
private object lockObject;
private bool locked;
public void Lock()
{
Monitor.Enter(lockObject);
locked = true;
}
public void Unlock()
{
Monitor.Exit(lockObject);
locked = false;
}
Unfortunately, this code is prone to race conditions - regardless of whether locked
is set prior or following to entering or leaving the critical section.
Another approach involved using TryEnter, but this method actually enters the critical section if the lock is not obtained, which may also lead to race conditions.
How should I implement this mechanism to be thread safe and how to avoid race conditions while checking, whether list is locked or not?
You can make your class responsible for the locking and only expose one method which accepts an Action
that the consumer specifies:
public class LockedCollection
{
private class CollectionImpl : ICollection<int>
{
//Collection methods
}
private sealed class CollectionWrapper : ICollection<int>, IDisposable
{
public CollectionWrapper(CollectionImpl inner)
{
_inner = inner;
}
private CollectionImpl _inner
//Collection methods all just wrapping calls to inner
public void Dispose()
{
_inner = null;
}
}
private CollectionImpl _instance - new CollectionImpl();
private object _lock = new object();
public void DoStuff(Action<ICollection<int>> task)
{
lock(_lock)
{
using(var wrapper = new CollectionWrapper(_instance))
{
task(wrapper);
}
}
}
}
The consumer can have any sequence of operations they like inside task
, and you know that the lock is taken - because you took it yourself.
I'm thinking of something that would resemble to the Builder pattern.
Example usage:
list
.do() // mandatory initial statement.
// Doesn't acquire a lock - it just builds a transaction object
.add(42) // Add an operation to the transaction object.
// Call would be illegal without performing do() before
.removeAt(0) // Another added operation
.end(); // Acquire the lock, perform the specified changes, release the lock.
The lock acquisition to be performed by end()
could be a plain call to sync {...}
- there are no possible race conditions.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With