.NET Core 2.0 正则表达式超时死锁问题

11
我有一个 .NET Core 2.0 应用程序,需要迭代很多文件(共计 6,0000 个),这些文件大小不同(总共 220GB)。
我使用以下方式枚举它们:
new DirectoryInfo(TargetPath)
    .EnumerateFiles("*.*", SearchOption.AllDirectories)
    .GetEnumerator()

并使用它们进行迭代

Parallel.ForEach(contentList.GetConsumingEnumerable(),
    new ParallelOptions
    {
        MaxDegreeOfParallelism = Environment.ProcessorCount * 2
    },
    file => ...

在此之内,我有一系列的正则表达式列表,然后使用它们扫描文件。

Parallel.ForEach(_Rules,
    new ParallelOptions
    {
        MaxDegreeOfParallelism = Environment.ProcessorCount * 2
    },
    rule => ... 

最后,我使用 Regex 类的一个 实例 来获取匹配项

RegEx = new Regex(
    Pattern.ToLowerInvariant(),
    RegexOptions.Multiline | RegexOptions.Compiled,
    TimeSpan.FromSeconds(_MaxSearchTime))
这个实例被所有文件共享,因此我只编译它一次。有175个模式应用于这些文件。应用程序在随机的位置死锁,并完全无响应。无论尝试多少次try/catch都无法阻止其发生。如果我使用相同的代码并为.NET Framework 4.6编译它,则可以正常工作而没有任何问题。我尝试了很多方法,目前似乎有效(但我非常小心!)是不使用实例,而是每次调用静态的Regex.Matches方法。我无法确定性能损失有多大,但至少我没有出现死锁的情况。我需要一些见解或者起到警示作用。更新:我通过以下方式获取文件列表:
private void GetFiles(string TargetPath, BlockingCollection<FileInfo> ContentCollector)
    {
        List<FileInfo> results = new List<FileInfo>();
        IEnumerator<FileInfo> fileEnum = null;
        FileInfo file = null;
        fileEnum = new DirectoryInfo(TargetPath).EnumerateFiles("*.*", SearchOption.AllDirectories).GetEnumerator();
        while (fileEnum.MoveNext())
        {
            try
            {
                file = fileEnum.Current;
                //Skip long file names to mimic .Net Framework deficiencies
                if (file.FullName.Length > 256) continue;
                ContentCollector.Add(file);
            }
            catch { }
        }
        ContentCollector.CompleteAdding();
    }

在我的 Rule 类中,以下是相关的方法:

_RegEx = new Regex(Pattern.ToLowerInvariant(), RegexOptions.Multiline | RegexOptions.Compiled, TimeSpan.FromSeconds(_MaxSearchTime));
...
    public MatchCollection Matches(string Input) { try { return _RegEx.Matches(Input); } catch { return null; } }
    public MatchCollection Matches2(string Input) { try { return Regex.Matches(Input, Pattern.ToLowerInvariant(), RegexOptions.Multiline, TimeSpan.FromSeconds(_MaxSearchTime)); } catch { return null; } }

那么,这里是匹配代码:

    public List<SearchResult> GetMatches(string TargetPath)
    {
        //Set up the concurrent containers
        ConcurrentBag<SearchResult> results = new ConcurrentBag<SearchResult>();
        BlockingCollection<FileInfo> contentList = new BlockingCollection<FileInfo>();

        //Start getting the file list
        Task collector = Task.Run(() => { GetFiles(TargetPath, contentList); });
        int cnt = 0;
        //Start processing the files.
        Task matcher = Task.Run(() =>
        {
            //Process each file making it as parallel as possible                
            Parallel.ForEach(contentList.GetConsumingEnumerable(), new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount * 2 }, file =>
            {
                //Read in the whole file and make it lowercase
                //This makes it so the Regex engine does not have
                //to do it for each 175 patterns!
                StreamReader stream = new StreamReader(File.OpenRead(file.FullName));
                string inputString = stream.ReadToEnd();
                stream.Close();
                string inputStringLC = inputString.ToLowerInvariant();

                //Run through all the patterns as parallel as possible
                Parallel.ForEach(_Rules, new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount * 2 }, rule =>
                {
                    MatchCollection matches = null;
                    int matchCount = 0;
                    Stopwatch ruleTimer = Stopwatch.StartNew();

                    //Run the match for the rule and then get our count (does the actual match iteration)
                    try
                    {
                        //This does not work - Causes Deadlocks:
                        //matches = rule.Matches(inputStringLC);

                        //This works - No Deadlocks;
                        matches = rule.Matches2(inputStringLC);

                        //Process the regex by calling .Count()
                        if (matches == null) matchCount = 0;
                        else matchCount = matches.Count;
                    }

                    //Catch timeouts
                    catch (Exception ex)
                    {
                        //Log the error
                        string timeoutMessage = String.Format("****** Regex Timeout: {0} ===> {1} ===> {2}", ruleTimer.Elapsed, rule.Pattern, file.FullName);
                        Console.WriteLine(timeoutMessage);
                        matchCount = 0;
                    }
                    ruleTimer.Stop();

                    if (matchCount > 0)
                    {
                        cnt++;
                        //Iterate the matches and generate our match records
                        foreach (Match match in matches)
                        {
                            //Fill my result object
                            //...

                            //Add the result to the collection
                            results.Add(result);
                        }
                    }
                });
            });
        });

        //Wait until all are done.
        Task.WaitAll(collector, matcher);

        Console.WriteLine("Found {0:n0} files with {1:n0} matches", cnt, results.Count);


        return results.ToList();
    }

更新2 我运行的测试没有死锁,但当它接近结束时,似乎停滞了,但我仍然可以用VS进入进程。然后我意识到我没有在我的测试中设置超时,而我在发布的代码中设置了超时(rule.Matchesrule.Matches2)。“有”超时,它会死锁。“没有”超时,它不会死锁。两者在.NET Framework 4.6中仍然有效。因为某些模式在一些大文件上会停顿,所以我需要为正则表达式设置超时。

更新3: 我一直在尝试不同的超时值,似乎是线程运行、超时异常和超时值的某种组合导致Regex引擎死锁。我无法确定确切的原因,但超时时间≥5分钟似乎有所帮助。作为临时解决办法,我可能会将该值设置为10分钟,但这不是永久解决方案!


1
你能提供一个最小的代码示例吗?有可能是你的代码中存在冲突或隐藏事件,或者是第三方库引起了此问题。 - Steve
1
由于进程完全死锁,我无法发布堆栈跟踪。 我甚至无法使用Visual Studio中断。 任何尝试暂停或引入断点都会导致VS冻结,直到我杀死应用程序,然后它会显示“无法设置断点!” - James Nix
1
C# 7.0 in a Nutshell中得知:Parallel.For和Parallel.ForEach通常在外部循环上运行得最好。这是因为前者提供了更大的工作块来并行化,从而稀释了管理开销。通常不需要同时并行化内部和外部循环。 这是针对GetMatches中的Parallel.ForEach - JohnyL
1
你确定这是死锁吗?看起来可能只是需要更长的时间。无论如何,这里的很多代码都可以整理一下。例如,你不需要BlockingCollection。只需让GetFiles返回一个IEnumerable并且为每个文件使用yield return。这样可以完全摆脱任务。你不需要内部的Parallel.ForEach(只需使用常规的foreach),并且可以利用线程本地变量而不是results - Rob
如果您仍然可以重现此问题 - 您可以通过外部触发进程的转储并在调试器中打开该转储来查看堆栈。例如,尝试使用perfview - Andy Ayers
显示剩余3条评论
1个回答

1
如果我要猜的话,我会怪正则表达式。
  • RegexOptions.Compiled 在 .NET Core 2.0 中没有实现 ()
  • 你的 175 个模式中可能有一些是 稍微邪恶

这可能导致 .NET Framework 4.6 和 .NET Core 2.0 之间的显着性能差异,从而导致应用程序无响应。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接