i have a single item pool implementation:
public class OneItemPool<T>
{
private T? _item;
private object _lockForChangeValue = new();
private object _lockForPostItem = new();
private object _lockForGetItem = new();
public void PostItem(T item)
{
lock (_lockForPostItem)
{
// waiting for _item to be taken
while (WeHaveItem())
{
}
lock (_mutexForChangeValue)
{
_item = item;
}
}
}
public T GetItem()
{
lock (_lockForGetItem)
{
// waiting for _item to appear
while (!WeHaveItem())
{
}
lock (_lockForChangeValue)
{
var item = _item;
_item = default(T);
return item;
}
}
}
bool WeHaveItem()
{
return !object.ReferenceEquals(_item, default(T));
}
}
As you can see I don’t have a lock inside bool WeHaveItem(), I know object.ReferenceEquals is not an atomic operation.
I know this can happen:
Thread A calling WeHaveItem() and sees that _item is null
Meanwhile Thread B is changing value of _item
Thread A continues execution under assumption that _item is null while its not true anymore
suppose _item is not null and Thread A is calling PostItem, then check WeHaveItem and wait for it to become null
while Thread B calls GetItem and changes _value to null
Thread A will get the old result _item != null
but this shouldn’t affect Thread A at all, because Thread A next operation will be to recheck WeHaveItem
I think there won’t be any problems.
But real tests show that this is not the case.
I set up my test like this
while (true)
{
var pool = new OneItemPool<string>();
var threads = new List<Thread>();
for (int i = 0; i < 3; i++)
{
var k = i;
var thr1 = new Thread(() =>
{
OneItemPool.PostItem(k.ToString());
OneItemPool.GetItem();
});
threads.Add(thr1);
}
foreach (var thread in threads)
{
thread.Start();
}
foreach (var thread in threads)
{
thread.Join();
}
}
One PostItem for one GetItem, but the test ends up deadlocking inside the PostItem and the _item value is not null.
This shows that two GetItem executed successfully for one PostItem.
If I add lock inside WeHaveItem the deadlock goes away.
I’ve been thinking about this for two days and can’t imagine this is possible.