中止/取消由单个牢不可破的操作组成的任务

本文关键字:操作 任务 牢不可破 取消 单个 中止 | 更新日期: 2023-09-27 18:37:00

我正在制作一个将远程网络文件复制到本地磁盘的应用程序,整个操作由File.Copy(remotePath, localPath)调用组成。有时复制操作挂起或运行得特别慢,但还没有抛出任何异常,但我想停止它,并且没有办法从取消令牌中检查IsCancellationRequested,就像这里的许多帖子建议操作可分割的情况一样。在这种情况下,我该怎么做才能取消任务?

中止/取消由单个牢不可破的操作组成的任务

不幸的是,.NET 没有取消文件复制操作的托管方法,据我所知,它只能通过本机调用来完成。

您需要进行的本机调用是FileCopyEx,它具有允许取消的参数和回调函数。

下面是在 .NET 中执行此操作的方法:

[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool CopyFileEx(string lpExistingFileName, string lpNewFileName,
    CopyProgressRoutine lpProgressRoutine, IntPtr lpData, ref bool pbCancel,
    CopyFileFlags dwCopyFlags);
private const int CopyCanceledErrorCode = 1235;
private static void Copy(string oldFile, string newFile, CancellationToken cancellationToken)
{
    var cancel = false;
    CopyProgressRoutine callback =
        (size, transferred, streamSize, bytesTransferred, number, reason, file, destinationFile, data) =>
        {
            if (cancellationToken.IsCancellationRequested)
                return CopyProgressResult.PROGRESS_CANCEL;
            else
                return CopyProgressResult.PROGRESS_CONTINUE;
        };
    if (!CopyFileEx(oldFile, newFile, callback, IntPtr.Zero, ref cancel, CopyFileFlags.COPY_FILE_RESTARTABLE))
    {
        var error = Marshal.GetLastWin32Error();
        if (error == CopyCanceledErrorCode)
            throw new OperationCanceledException(cancellationToken);
                //Throws the more .NET friendly OperationCanceledException
        throw new Win32Exception(error);
    }
    //This prevents the callback delegate from getting GC'ed before the native call is done with it.
    GC.KeepAlive(callback);
}

需要注意的一件事是,我第一次尝试这样做是

private static void Copy(string oldFile, string newFile, CancellationToken cancellationToken)
{
    bool cancel = false;
    using (cancellationToken.Register(() => cancel = true))
    {
        if (!CopyFileEx(oldFile, newFile, null, IntPtr.Zero, ref cancel, CopyFileFlags.COPY_FILE_RESTARTABLE))
        {
            throw new Win32Exception(Marshal.GetLastWin32Error());
        }
        GC.KeepAlive(cancel);
    }
}

但这并没有导致副本取消。我不知道这是否只是因为您无法更改委托的布尔值,还是因为 CopyFileEx 没有经常检查布尔值是否发生变化。使用取消复制的回调方法要可靠得多。


附录:

这是所有枚举和委托的副本,因此您无需像我一样在 Pinvoke.net 上寻找它们。

enum CopyProgressResult : uint
{
    PROGRESS_CONTINUE = 0,
    PROGRESS_CANCEL = 1,
    PROGRESS_STOP = 2,
    PROGRESS_QUIET = 3
}
enum CopyProgressCallbackReason : uint
{
    CALLBACK_CHUNK_FINISHED = 0x00000000,
    CALLBACK_STREAM_SWITCH = 0x00000001
}
[Flags]
enum CopyFileFlags : uint
{
    COPY_FILE_FAIL_IF_EXISTS = 0x00000001,
    COPY_FILE_RESTARTABLE = 0x00000002,
    COPY_FILE_OPEN_SOURCE_FOR_WRITE = 0x00000004,
    COPY_FILE_ALLOW_DECRYPTED_DESTINATION = 0x00000008,
    COPY_FILE_COPY_SYMLINK = 0x00000800 //NT 6.0+
}
delegate CopyProgressResult CopyProgressRoutine(
    long TotalFileSize,
    long TotalBytesTransferred,
    long StreamSize,
    long StreamBytesTransferred,
    uint dwStreamNumber,
    CopyProgressCallbackReason dwCallbackReason,
    IntPtr hSourceFile,
    IntPtr hDestinationFile,
    IntPtr lpData);

打开两个文件一个 FileStream 并将其分成块复制。如果可以,请使用异步方法。

简单的形式是读取数据进行缓冲并写入缓冲出来。扩展版本是在一个缓冲区中读取并同时从第二个缓冲区写入。

在每个读/写调用之后或之前,操作是可取消。

如果有问题,您将在某个时候收到超时异常取决于流的读取超时写入超时属性。

Windows 默认缓冲区大小为 4096 字节。以下是扩展方法:

public static void WriteTo(this Stream source, Stream distiantion, int bufferSize = PNetIO.DefaultBufferSize, CancellationToken cancellationToken = default(CancellationToken))
    {
        var buffer = new byte[bufferSize];
        int c;
        while ((c = source.Read(buffer, 0, bufferSize)) > 0)
        {
            distiantion.Write(buffer, 0, c);
            cancellationToken.ThrowIfCancellationRequested();
        }
    }
    public static async Task WriteToAsync(this Stream source, Stream distiantion, int bufferSize = PNetIO.DefaultBufferSize, CancellationToken cancellationToken = default(CancellationToken))
    {
        if (!source.CanRead)
            return;
        var buffer = new Byte[bufferSize * 2];
        var c = await source.ReadAsync(buffer, bufferSize, bufferSize, cancellationToken).ConfigureAwait(false);
        if (c <= 0)
            return;
        var w = distiantion.WriteAsync(buffer, bufferSize, c, cancellationToken);
        while ((c = await source.ReadAsync(buffer, 0, bufferSize).ConfigureAwait(false)) > 0)
        {
            await w.ConfigureAwait(false);
            w = distiantion.WriteAsync(buffer, 0, c, cancellationToken);
            if ((c = await source.ReadAsync(buffer, bufferSize, bufferSize, cancellationToken).ConfigureAwait(false)) <= 0)
                break;
            await w.ConfigureAwait(false);
            w = distiantion.WriteAsync(buffer, bufferSize, c, cancellationToken);
        }
        await w.ConfigureAwait(false);
        await distiantion.FlushAsync(cancellationToken).ConfigureAwait(false);
    }

你可以像这样使用它:

var cancel = new CancellationTokenSource();
        using (var source = File.OpenRead("source"))
        using (var dest = File.OpenWrite("dest"))
            await source.WriteToAsync(dest, cancellationToken: cancel.Token);