You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何readAllEXIFdates任务在producerCompleted更新后无法退出循环?

生产者消费者循环无法退出的原因分析

相关代码

方法 readAllFilesAtRootFolder

public static Dictionary<string, List<mediaFile>> readAllFilesAtRootFolder(
    string rootFolder, string[] extensions, bool subFolders,
    ConcurrentQueue<mediaFile> fileQueue, CancellationToken cancellationToken)
{
    var allFiles = new Dictionary<string, List<mediaFile>>();
    IEnumerable<string> files;
    try
    {
        if (subFolders)
        {
            files = extensions.SelectMany(ext => System.IO.Directory.GetFiles(
                rootFolder, ext, SearchOption.AllDirectories));
        }
        else
        {
            files = extensions.SelectMany(ext => System.IO.Directory.GetFiles(
                rootFolder, ext, SearchOption.TopDirectoryOnly));
        }
        int i = 1;
        foreach (var file in files)
        {
            if (cancellationToken.IsCancellationRequested)
                break;

            mediaFile mediaFile = new mediaFile();
            string extension = Path.GetExtension(file).Replace(".", "").ToUpper();
            // 如果扩展名不为空,按扩展名分组文件
            if (!string.IsNullOrEmpty(extension))
            {
                if (!allFiles.ContainsKey(extension))
                {
                    allFiles[extension] = new List<mediaFile>();
                }
                mediaFile.fileName = file;
                //mediaFile.exif_date = getImageExifDate(file.ToString());
                fileQueue.Enqueue(mediaFile);
                allFiles[extension].Add(mediaFile);
            }
        }
        return allFiles;
    }
    catch { }
    return allFiles;
}

方法 readAllEXIFdates

public static void readAllEXIFdates(ConcurrentQueue<mediaFile> fileQueue,
    ConcurrentDictionary<string, List<mediaFile>> resultDictionary,
    CancellationToken cancellationToken, bool producerCompleted)
{
    var allEXIFFiles = new Dictionary<string, List<mediaFile>>();

    {
        // 给主线程留出空闲时间
        //Thread.Sleep(100);
        while (!cancellationToken.IsCancellationRequested
            && (!producerCompleted || !fileQueue.IsEmpty))
        {
            if (fileQueue.TryDequeue(out var mediaFile))
            {
                // 处理EXIF日期
                mediaFile.exif_date = getImageExifDate(mediaFile.fileName);
                var extension = Path.GetExtension(mediaFile.fileName)
                    .ToUpperInvariant().TrimStart('.');

                if (!string.IsNullOrEmpty(extension))
                {
                    resultDictionary.AddOrUpdate(extension,
                        new List<mediaFile> { mediaFile },
                        (key, existingList) =>
                        {
                            existingList.Add(mediaFile);
                            return existingList;
                        });
                }
            }
            else
            {
                // Thread.Sleep(100);
                // 队列无文件时短暂等待
            }
        }
    }
}

按钮点击事件处理器

private async void button4_Click_1(object sender, EventArgs e)
{
    string[] extenions = getSelectExtensions(chkExtensions);

    label5.Text = "读取文件中,请稍候....";

    _cancellationTokenSource = new CancellationTokenSource();

    string[] extensions = getSelectExtensions(chkExtensions);

    label5.Text = "读取文件中,请稍候...";

    // 定义共享资源
    var fileQueue = new ConcurrentQueue<mediaFile>();
    var resultDictionary = new ConcurrentDictionary<string, List<mediaFile>>();

    bool producerCompleted = false;
    // 生产者任务
    var producerTask = Task.Run(() =>
    {
        try
        {
            readAllFilesAtRootFolder(
                "H:/My Photos/IMages/2015 - Dhaham's Choir Concert",
                extensions, chkSubFolders.Checked, fileQueue,
                _cancellationTokenSource.Token);
        }
        finally
        {
            producerCompleted = true;
        }
    });

    var consumerTask = Task.Run(() => readAllEXIFdates(fileQueue, resultDictionary,
        _cancellationTokenSource.Token, producerCompleted));
    // 等待两个任务完成
    await Task.WhenAll(producerTask, consumerTask);

    label8.Text = $"处理文件总数: {resultDictionary.Values.Count}";
}

问题描述

预期readAllEXIFdates()方法会在producerCompleted被更新为true时退出循环,但实际该循环始终无法感知到producerCompleted的更新——即便将producerCompleted改为类的公共字段也无效。循环代码如下:

while (!cancellationToken.IsCancellationRequested
    && (!producerCompleted || !fileQueue.IsEmpty))

原因分析

1. 值类型按值传递导致副本独立

producerCompleted是值类型(bool),在传递给readAllEXIFdates方法时会创建独立副本。生产者任务修改的是外部的producerCompleted变量,而消费者方法内部使用的是初始化时传入的false副本,二者完全独立,因此消费者永远看不到外部变量的更新,!producerCompleted始终为true。

2. 类字段的线程可见性问题

即便把producerCompleted改成类的公共字段,依然存在线程可见性问题:普通字段没有内存屏障,CPU会将字段值缓存到线程本地缓存中,消费者线程可能一直读取缓存里的旧值(false),无法感知生产者线程对字段的修改(true)。

可行解决方案

  • 使用引用类型包装:用bool[]或自定义类包装状态,比如class Flag { public bool Completed; },传递引用后修改能被消费者感知。
  • 添加内存屏障:给类字段加上volatile修饰符,强制线程每次从主内存读取最新值,而非本地缓存。
  • 使用同步原语:比如ManualResetEventSlim,生产者任务完成时触发事件,消费者通过事件状态判断是否退出。
  • 利用Task的完成状态:不用单独的bool变量,消费者直接通过producerTask.IsCompleted判断生产者是否完成(需确保线程安全)。

内容的提问来源于stack exchange,提问作者PCG

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 23:37:33