繁体   English   中英

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

[英]Aborting/canceling tasks that consist of a single unbreakable operation

我正在制作一个将远程网络文件复制到本地磁盘的应用程序,整个操作由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没有频繁检查布尔值是否发生变化。 使用取消副本的回调方法更加可靠。


APPENDEX:

这是所有枚举和委托的副本,因此您无需像我一样在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并将其分块复制。 如果您可以使用异步方法,请使用。

简单的形式是读取要缓冲的数据并写出缓冲。 扩展的版本将读入一个缓冲区,并同时从第二个缓冲区写入。

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

如果存在问题,则根据流的ReadTimeout WriteTimeout属性,您将在某个时刻获得timeoutexception。

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);

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM