Search code examples
c#multithreadingexceptionreaderwriterlockslimreaderwriterlock

LockRecursionException in multiple threaded function


I dont know how to describe it but I'm getting exception that schouldn't have a place when a code is good written. This exception is about issue with ReaderWriterLockSlim and it's LockRecursionException; it is appearing at "ScreenLocker.EnterReadLock();" line. Can't find problem with my code and description what to do or what might be wrong at internet, that's why i writting this question here and asking you all for help. This is code I have problem with:

public static List<Dictionary<int, int>> RunTasks(ScreenScanning ss)
    {
        var listOfTasks = new List<Task>();
        List<Dictionary<int, int>> PosXOfBlocksAndMeaningOfIt = new List<Dictionary<int, int>>();
        for (var i = 0; i <= BlocksOnYAxisOnScreen; i++)
        {
            ScreenLocker.EnterReadLock();
            var t = new Task(() =>
            {
                PosXOfBlocksAndMeaningOfIt.Add(ss.XAxysScan(PosYOfRowsToScan[i], Screen, ref ScreenLocker));
            });
            listOfTasks.Add(t);
        }
        Task.WaitAll(listOfTasks.ToArray());
        return PosXOfBlocksAndMeaningOfIt;
    }

and that are functions called by this method:

public Dictionary<int, int> XAxysScan(int posY, Bitmap screen, ref ReaderWriterLockSlim screenLocker)
    {
        screenLocker.ExitReadLock();
        Dictionary<int, int> partOfMainTable = new Dictionary<int, int>();
        partOfMainTable.Add(666, posY); //used in BotViewUpdate in DataToTableInterpreter
        for (int i = 0; i <= 1920; i++)
        {
            if (screen.GetPixel(i, posY) == ColorsInRow[0])
            {
                if (IsFarmable(posY, ColorsInRow, i, screen))
                {
                    partOfMainTable.Add(i, 1);
                }
            }
            else if (IsBackground(BackgroundColors, i, posY, screen))
            {
                partOfMainTable.Add(i, 0);
            }
            else
            {
                partOfMainTable.Add(i, 2);                
            }
        }
        return partOfMainTable;
    }

How can you see I'm releaseing lock right after entering XAxysScan function.


Solution

  • How can you see I'm releasing lock right after entering XAxysScan function.

    The ReaderWriterLockSlim is a synchronization object that allows multiple threads to read from a resource, but only allow 1 resource to write to it(ideally).

    The reason why this is important is because the specific way that ReaderWriterLockSlim is implemented to achieve this effect, requires something called Managed Thread Affinity, which basically means that whatever Task or Thread that called the EnterReadLock() must be the same Task or thread that calls ExitReadLock();.

    When we look at the following, we can see you Have RunTasks(ScreenScanning ss) enter the lock, but you immediately start a new child Task and pass the ReaderWriterLockSlim as a reference to XAxysScan().

    ScreenLocker.EnterReadLock();
    var t = new Task(() =>
    {
        PosXOfBlocksAndMeaningOfIt.Add(ss.XAxysScan(PosYOfRowsToScan[i], Screen, ref ScreenLocker));
    });
    

    Only the same Task that enters a lock can be the one to release that lock. At least for synchronization objects like ReaderWriterLockSlim that use Managed Thread Affinity.

    Consider moving EnterReadLock() into the XAxysScan() method.

    public Dictionary<int, int> XAxysScan(int posY, Bitmap screen, ref ReaderWriterLockSlim screenLocker)
    {
        screenLocker.EnterReadLock();
        try{
            Dictionary<int, int> partOfMainTable = new Dictionary<int, int>();
        partOfMainTable.Add(666, posY); //used in BotViewUpdate in DataToTableInterpreter
            for (int i = 0; i <= 1920; i++)
            {
                if (screen.GetPixel(i, posY) == ColorsInRow[0])
                {
                    if (IsFarmable(posY, ColorsInRow, i, screen))
                    {
                        partOfMainTable.Add(i, 1);
                    }
                }
                else if (IsBackground(BackgroundColors, i, posY, screen))
                {
                    partOfMainTable.Add(i, 0);
                }
                else
                {
                    partOfMainTable.Add(i, 2);                
                }
            }
            return partOfMainTable;
        }
        finally
        {
            // make sure that even if we encounter an error, we still exit the lock so other threads can enter the lock / begin writing
            screenLocker.ExitReadLock();
        }