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

时间:2015-01-11 02:36:48

标签: c# multithreading task-parallel-library

我正在创建一个将远程网络文件复制到本地磁盘的应用程序,整个操作包含File.Copy(remotePath, localPath)个调用。有时复制操作会挂起或运行得更慢,但是不会抛出任何异常,但是我想要阻止它,并且无法从取消令牌中检查IsCancellationRequested,就像这里的许多帖子推荐用于操作是可分的。在这种情况下我该如何取消任务?

2 个答案:

答案 0 :(得分:1)

令人遗憾的是,.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);
    }
}

但这并没有导致副本取消。我不知道是不是因为你不能从委托中更改bool,或者是因为CopyFileEx不经常检查bool是否发生变化。使用取消副本的回调方法更加可靠。


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

答案 1 :(得分:1)

将这两个文件打开一个FileStream并以块的形式复制它。 如果你能使用异步方法,请使用。

简单的形式是读取数据以缓冲和写入 缓冲出来。扩展版本用于读取一个缓冲区 并同时从第二个缓冲区写入。

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

如果出现问题,你会在某个时间点得到一个时间 取决于流的ReadTimeout WriteTimeout属性。

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