收藏已修改;枚举操作可能无法执行(带锁的多线程)

Posted

技术标签:

【中文标题】收藏已修改;枚举操作可能无法执行(带锁的多线程)【英文标题】:Collection was modified; enumeration operation may not execute (Multi Threading with Locks) 【发布时间】:2021-06-10 02:19:54 【问题描述】:

所以我知道这个问题以前在这里被问过,但这里的情况有点不同。

我有一个产生工作线程的服务应用程序。主服务线程是这样组织的:

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)
    
        
    

在投票内部你有这个逻辑:

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)
    
        
    

那个ProducerConsumerQueue.EnqueueTask(handler.Execute, CancellationTokenSource.Token);启动 4 个工作线程,并在其中任何一个线程内,随时调用以下函数:

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;

现在我正在测试stuckDeletedServTableFiles 哈希集,该哈希集仅在查询期间出现异常时调用。这就是我故意抛出异常的原因。该哈希集是在函数 DeleteFilesToBeDeletedInServiceTable() 中的主服务线程上操作的哈希集;谁的摘录定义如下:

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;

鉴于 hashsetstadDeletedServTableFiles 能够被多个线程同时访问,包括主服务线程,我在主服务线程被 DeleteFilesToBeDeletedInServiceTable() 操作之前,在函数 DeleteServiceEntry() 中加了一个锁.我是 C# 新手,但我认为这已经足够了?我假设由于在主服务线程上为函数 DeleteFilesToBeDeletedInServiceTable() 调用了锁,因此该锁将阻止任何东西使用哈希集,因为它是由函数操作的。为什么会出现此错误?

注意,我没有在 forloop 中修改 Hashset。我只在循环完成后才这样做。我在遍历 Hashset 时收到此错误。我想是因为另一个线程正在尝试修改它。那么问题来了,为什么当我在服务级别调用它的函数上锁定时,一个线程能够修改 hashSet?

【问题讨论】:

锁定对您可能已经很清楚的异常没有任何帮助,因为这在 SO 和其他任何地方都是非常流行的问题......但是为了帮助您的特殊情况,我们需要 minimal reproducible example - 很高兴您使用参数化查询和异常处理显示具有长描述性方法名称的代码,但这不是最小的示例,并且需要回答者方面不必要的努力...... 很可能在某处存在对HashSet 的不受保护的访问,或者HashSet 在任何地方都没有使用相同的储物柜对象进行保护。我已经发布了here 一些关于如何使用锁的指南,您可能会觉得这些指南很有用。 感谢您的反馈。实际上,经过这么长时间的哈哈,我终于明白了那个“最小的可重现示例”现在意味着什么。无论如何 uisng Lock 实际上已修复它。我只需要在每次使用该哈希集时都使用它。我发布了我的解决方案 @TheodorZoulias 看来我需要在直接调用 HashSet 的语句/表达式上使用 Lock。我认为在调用它的函数周围加一个锁就足够了。好像我不太明白锁是如何工作的。 是的,当你有一个可以被多个线程同时访问的非线程安全对象时,你就不能挑剔了。您必须使用相同的锁来保护对它的每一次访问,否则您的解决方案会出现未定义的行为。 【参考方案1】:

现在,我通过用锁包围 DeleteFilesToBeDeletedInServiceTable() 中的 for 循环来修复它,并在语句stdDeletedServTableFiles.ExceptWith(removedHashset); 上调用相同的锁。在那个函数里面。我不确定这是否有成本,但它似乎会起作用。考虑到这个问题在实践中发生的频率有多低,我想它不会花费太多。特别是自从调用该函数的时间以来,我们并没有做密集的事情。到那时文件处理已经完成,主调用者​​正在使用另一个线程

【讨论】:

以上是关于收藏已修改;枚举操作可能无法执行(带锁的多线程)的主要内容,如果未能解决你的问题,请参考以下文章

收藏已修改;枚举操作可能无法执行。为啥? [复制]

收藏已修改;枚举操作可能无法执行

收藏已修改;枚举操作可能无法执行。到处都在使用锁怎么可能?

带锁的循环内线程执行

C# 集合已修改;可能无法执行枚举操作

集合已修改;可能无法执行枚举操作。