0

So I know this question has been asked here before, but the situation here is a little different.

I have a service Application that spawns worker threads. The main service thread is organized as so:

public void PollCrunchFilesTask()
{
    try
    {
        var stuckDeletedServTableFiles = MaintenanceDbContext.stuckDeletedServTableFiles;
        var stuckErrorStatusFiles = MaintenanceDbContext.stuckErrorStatusFiles;
        while (_signalPollAutoEvent.WaitOne())
        {
            try
            {
                Poll();
                lock (stuckDelLock)
                {
                    if(stuckDeletedServTableFiles.Count > 0)
                        MaintenanceDbContext.DeleteFilesToBeDeletedInServiceTable(stuckDeletedServTableFiles);
                }

                lock (errorStatusLock)
                {
                    if (stuckErrorStatusFiles.Count > 0)
                        MaintenanceDbContext.UpdateStuckErrorServiceLogEntries(stuckErrorStatusFiles);
                }
            }
            catch (Exception ex)
            {
                
            }
        }
    }
    catch (Exception ex)
    {
        
    }
}

Inside Poll you have this logic:

public void Poll()
{
    try
    {
        if (ProducerConsumerQueue.Count() == 0 && ThreadCount_Diff_ActiveTasks > 0)
        {
            var dequeuedItems = MetadataDbContext.UpdateOdfsServiceEntriesForProcessingOnPollInterval(ThreadCount_Diff_ActiveTasks);

            var handlers = Producer.GetParserHandlers(dequeuedItems);

            foreach (var handler in handlers)
            {
                ProducerConsumerQueue.EnqueueTask(handler.Execute, CancellationTokenSource.Token);
            }

        }
        
    }
    catch (Exception ex)
    {
        
    }
}

That ProducerConsumerQueue.EnqueueTask(handler.Execute, CancellationTokenSource.Token); launches 4 worker threads and inside any one of these threads, the following function is called at any time:

public static int DeleteServiceEntry(string logFileName)
{
    int rowsAffected = 0;
    var stuckDeletedServTableFiles = MaintenanceDbContext.stuckDeletedServTableFiles;
    try
    {
        
        string connectionString = GetConnectionString();
        throw new Exception($"Testing Del HashSet");
        using (SqlConnection connection = new SqlConnection())
        {
            //Attempt some query
        }
        
    }
    catch (Exception ex)
    {
        lock (stuckDelLock)
        {
            stuckDeletedServTableFiles.Add(logFileName);
        }
    }

    return rowsAffected;
}

Now I am testing the stuckDeletedServTableFiles hashset which is only called when there is an exception during a query. this is why I purposefully throw an exception. That hashset is the one that is operated on on the main service thread in the function DeleteFilesToBeDeletedInServiceTable(); who's excerpt is defined below:

public static int DeleteFilesToBeDeletedInServiceTable(HashSet<string> stuckDeletedServTableFiles)
{
    int rowsAffected = 0;
    string logname = String.Empty; //used to collect error log
    var removedHashset = new HashSet<string>();
    try
    {
        var dbConnString = MetadataDbContext.GetConnectionString();
        string serviceTable = Constants.SERVICE_LOG_TBL;
        using (SqlConnection connection = new SqlConnection(dbConnString))
        {
            SqlCommand cmd = new SqlCommand();
            cmd.CommandType = CommandType.Text;
            cmd.CommandText = $"DELETE FROM {serviceTable} WHERE LOGNAME = @LOGNAME";
            cmd.Parameters.Add("@LOGNAME", SqlDbType.NVarChar);
            cmd.Connection = connection;
            connection.Open();
            foreach (var logFname in stuckDeletedServTableFiles)
            {
                cmd.Parameters["@LOGNAME"].Value = logFname;
                logname = logFname;
                int currRowsAffected = cmd.ExecuteNonQuery();
                rowsAffected += currRowsAffected;
                if (currRowsAffected == 1)
                {
                    removedHashset.Add(logFname);
                    Logger.Info($"Removed Stuck {logFname} Marked for Deletion from {serviceTable}");
                }

            }
            Logger.Info($"Removed {rowsAffected} stuck files Marked for Deletion from {serviceTable}");
        }

        stuckDeletedServTableFiles.ExceptWith(removedHashset);
    }
    catch (Exception ex)
    {
        
    }
    return rowsAffected;
}

Given that the hashset stuckDeletedServTableFiles is able to be accessed by multiple threads at the same time including the main service thread, I put a lock on the mainservice thread before it is operated on by DeleteFilesToBeDeletedInServiceTable() and in the function DeleteServiceEntry(). I am new to C# but I assumed this would be enough? I assume since a lock is called on the main service thread for the function DeleteFilesToBeDeletedInServiceTable(), that lock would prevent anything from using the hashset since its being operated on by the function. Why am I getting this error?

Note I am not modifying the Hashset in the forloop. I only do it after the loop is done. I am getting this error while I loop through the Hashset. I suppose because another thread is attempting to modify it. The question then is, why is a thread able to modify the hashSet when I have a lock on the function that calls it on the service level?

devn00b1090
  • 97
  • 1
  • 8
  • 2
    Locking in no way going to help with that exception which you likely already well aware as this is quite popular problem on SO and everywhere else... But to help with your particular case we need [MCVE] - it's nice that you show code with long descriptive method names, using parametrized queries and exception handling, but that is not minimal example and requires unnecessary effort on side of answerers... – Alexei Levenkov Mar 12 '21 at 03:06
  • Most probably there is unprotected access to the `HashSet` somewhere, or the `HashSet` is not protected everywhere using the same locker object. I have posted [here](https://stackoverflow.com/questions/2779703/guidelines-of-when-to-use-locking/65250343#65250343 "Guidelines of when to use locking") some guidelines about how to use locks, that you might find useful. – Theodor Zoulias Mar 12 '21 at 03:33
  • Thanks for the feedback. I actually finally understand what that "minimal reproducable example" now means after so long lol. Anyways uisng Lock actually fixed it. I just needed to use it any and everytime I am using that Hashset. I posted my solution – devn00b1090 Mar 12 '21 at 03:33
  • @TheodorZoulias It does appear that I need to use Lock on statement/expression where that HashSet is directly called. I assumed that putting a lock around the function that it is called in would be enough. Seems like I don't quite understand how locks work. – devn00b1090 Mar 12 '21 at 03:34
  • Yeap, when you have a non thread-safe object that can be accessed by multiple threads concurrently, you can't be picky. You must protect every single access to it using the same lock, or you have a broken solution with undefined behavior. – Theodor Zoulias Mar 12 '21 at 03:37
  • I don't suppose using a lock statement around a statement that contains a SQL query means that the database/table will be locked to any other transactions right? @TheodorZoulias – devn00b1090 Mar 12 '21 at 03:40
  • Nope, a `lock` affects only your own program, not other processes that are running on the same or other machine. While one thread holds a lock, any other thread that attempts to acquire the same lock will be blocked. This phenomenon is known as "contention", and can affect negatively the performance/scalability of an application. This is why a `lock` should be released ASAP. Doing any work unrelated to the protected shared state (the `HashSet` in your case) while holding a lock is either sloppy programming or a bug, depending on how you see it. – Theodor Zoulias Mar 12 '21 at 03:48
  • Btw my comments may sound a bit harsh and unfriendly. What happens is that your code brings me back memories from some buggy multithreaded code that I wrote some years ago, without knowing really what I was doing, and I am afraid that it is still used in production somewhere. – Theodor Zoulias Mar 12 '21 at 18:07

1 Answers1

0

For Now, I fixed it by surrounding the for loop in DeleteFilesToBeDeletedInServiceTable() with a lock and also calling the same lock on the statement stuckDeletedServTableFiles.ExceptWith(removedHashset); inside that function. I am not sure if there are costs to this but it seems it will work. And giving how in practice how infrequent this issue will occur, I suppose it wont' cost much. Especially since the times that function is called, we are not doing intensive things. The file crunching would have finished by then and the main caller is utilizing another thread

devn00b1090
  • 97
  • 1
  • 8