简体   繁体   English

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

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

I'm making an app that copies remote network files over to local disk and the whole operation consists of File.Copy(remotePath, localPath) calls. 我正在制作一个将远程网络文件复制到本地磁盘的应用程序,整个操作由File.Copy(remotePath, localPath)调用组成。 Sometimes copy operations hang or run extra slowly but don't throw any exceptions yet, but I want to stop it, and there is no way to check for IsCancellationRequested from a cancellation token, like many posts here recommend for cases where the operations are divisible. 有时复制操作会挂起或运行得特别慢,但是还没有引发任何异常,但是我想停止它,而且无法从取消令牌中检查IsCancellationRequested ,就像这里的许多帖子都建议将操作整除的情况一样。 What do I do in this case for canceling tasks? 在这种情况下,我该怎么做才能取消任务?

Unfortuantly .NET has no managed way to cancel a file copy operation, to my knowledge it can only be done via native calls. 不幸的是,.NET没有取消文件复制操作的托管方法,据我所知,它只能通过本地调用来完成。

The native call you would need to make is to FileCopyEx it has a parameter and a callback function that allows for cancellation. 您需要对FileCopyEx进行本机调用,它具有一个参数和一个允许取消的回调函数。

Here is how you would do it in .NET: 这是您在.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);
}

One thing to note, my first attempt at this was 需要注意的一件事是,我对此的第一次尝试是

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

but that did not cause the the copy to cancel. 但这并没有导致副本被取消。 I don't know if it was just because you can't change the bool from a delegate or if it was because CopyFileEx does not check as frequently to see if the bool changes. 我不知道这仅仅是因为您不能从委托更改布尔值还是因为CopyFileEx没有频繁检查布尔值是否发生变化。 Using the callback method of canceling the copy is much more reliable. 使用取消副本的回调方法更加可靠。


APPENDEX: APPENDEX:

Here is a copy of all of the enums and delegates so you don't need to hunt them down on Pinvoke.net like I did. 这是所有枚举和委托的副本,因此您无需像我一样在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);

Open both files a FileStream and copy it in chunks. 打开两个文件的FileStream并将其分块复制。 Use if u can the async methods. 如果您可以使用异步方法,请使用。

The simple form is to read data to buffer and write the buffer out. 简单的形式是读取要缓冲的数据并写出缓冲。 The extened Version is to read in one buffer and write from a second buffer at the same time. 扩展的版本将读入一个缓冲区,并同时从第二个缓冲区写入。

after or before each read/write call, the Operation is cancelable. 在每个读/写调用之后或之前,该操作是可取消的。

If there is a Problem u will get at some Point a timeoutexception depending on the ReadTimeout WriteTimeout property of the stream. 如果存在问题,则根据流的ReadTimeout WriteTimeout属性,您将在某个时刻获得timeoutexception。

The Windows Default buffer size is 4096 Bytes. Windows默认缓冲区大小为4096字节。 Here are to Extension methods: 这是扩展方法:

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

You can use it like this: 您可以像这样使用它:

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