首页 > 解决方案 > 在 Windows 切换线程之前强制完成对 ac# 方法的异步调用

问题描述

我有一个项目使用 Parallel.For 调用来一次执行文件的批量修改,因为它们都需要相当长的时间,我想在修改过程中提供反馈。整个过程还备份更改的文件并在执行过程中创建撤消批处理命令。撤消文件必须输出撤消步骤并在执行任何其他任务之前刷新撤消文件缓冲区,以便在更改文件的过程实际开始之前保存所有批处理步骤(复制原始文件和删除副本)。例如......如果我正在更改两个文件“A.bin”“B.Bin”我希望批处理文件说:

copy "A.original" "A.bin"
delete "A.original"
copy "B.original" "B.bin"
delete "B.original"

问题是异步调用可以在对产生上述输出的方法的并行调用之间切换,该方法会创建一个具有以下输出的文件:

copy "A.original" "A.bin"
copy "B.original" "B.bin"
delete "A.original"
delete "B.original"

这会产生一种情况,如果程序崩溃或在每个步骤中文件之间的过程中出现问题,“撤消”脚本最终会留下“删除”行,从而使撤消批处理留下需要删除的垃圾文件。

有没有办法在 Windows 切换到另一个线程之前标记/强制方法或块完成?根据我对 async/await 的了解,这有一个不同的用例,不会完成我需要的(这是我在网上查找如何执行此类操作时可以找到的唯一搜索结果)。

这是实际将步骤添加到批处理文件的代码。这整个方法必须在没有线程切换的情况下完整执行:

internal static void AddCommit(CommitType type, string sourcePath, string destPath = null)
{
    if ((type & CommitType.RestoreBackup) != 0)
    {
        if (destPath == null)
            throw new ArgumentNullException();
        UndoScript.WriteLine("copy \"" + sourcePath + "\" \"" + destPath + "\" /y");
    }

    if ((type & CommitType.UndoDeleteBackup) != 0)
        UndoScript.WriteLine("delete \"" + sourcePath + "\" /q");

    if ((type & CommitType.CommitDeleteBackup) != 0)
        CommitScript.WriteLine("delete \"" + sourcePath + "\" /q");

    UndoScript.Flush();
    if (CommitScript != null)
        CommitScript.Flush();
}

标签: c#multithreadingstreamwriterparallel.for

解决方案


您的问题中有很多东西,很难准确地掌握您对答案的期望。也就是说,实际上只有一个明确的问题

有没有办法在 Windows 切换到另一个线程之前标记/强制方法或块完成?

这很容易回答:

像 Windows 这样的抢先式多任务操作系统的整个前提是进程基本上无法控制线程的上下文切换。

最接近的方法是调整线程优先级。在某些情况下,这可能类似于控制上下文切换,但即使这样也不能完全控制进程,原因如下:

  1. 线程调度包括缓解线程饥饿。具有提升优先级的线程将优先一段时间,但如果这样做会使其他线程的 CPU 时间不足,最终操作系统仍将接管并让其他线程运行。
  2. 当线程要求无法立即提供的东西时,它们基本上放弃了它们的时间片。最常见的示例是 I/O,这正是您的场景。当您要求操作系统对磁盘执行某些操作时,您的线程就会说“好吧,我现在已经完成了......当你得到我想要的东西时告诉我。” 无论线程的优先级是什么,操作系统都会暂停线程并让另一个线程运行。

您的问题没有足够的细节来确切地知道场景是什么。但是您至少有两种选择:

  1. 如果您的工作有较小的操作组需要相互一致,但不需要在所有操作之间保持一致(即与其他操作组),那么只需将这些组设为您的并发粒度。然后,您可以保证每个组内的操作顺序。
  2. 如果所有操作一起应该是相互一致的,并且您仍然希望它们被同时处理,那么将其视为“事务”。这对于可能发生的故障会使系统处于不一致/损坏状态的并发场景很常见。具体来说:添加某种状态,例如作为标记的文件,表明操作已经开始,只有在整个操作完成后才删除状态。

在您的示例中,第二个选项可能涉及将所有副本作为单个事务进行,然后仅在第一个事务完成时才将所有文件删除作为第二个事务进行。这样,复制操作期间的失败将使系统处于可以恢复的状态(继续复制或回滚到先前的状态),并且可以类似地处理删除操作期间的失败。

最后,我要指出,如果您的所有文件操作都发生在同一台设备上,那么使用并发可能几乎没有好处。根据代码实际在做什么,单个线程可能能够使设备保持与两个或更多线程几乎一样繁忙。毕竟,即使是 SSD 也比 CPU 慢得多。添加更多线程可能只会造成一种情况,即所有线程都在一个设备上相互竞争,不会产生加速。在某些情况下,它甚至可能会减慢速度。

所以最好的解决方案可能是完全放弃整个并发方面。您应该向自己证明,多线程解决方案的测量性能实际上比单线程解决方案快得多,以证明额外的复杂性是合理的。


推荐阅读