F#异步文件复制

3
要异步复制文件,这样的代码是否可行?
let filecopyasync (source, target) =
    let task = Task.Run((fun () ->File.Copy(source, target, true)))

    // do other stuff

    Async.AwaitIAsyncResult task

特别是,这会启动一个新的线程来执行复制操作,同时我可以“干其他事情”吗?

更新:

找到另一种解决方案:

let asyncFileCopy (source, target, overwrite) =
    printfn "Copying %s to %s" source target
    let fn = new Func<string * string * bool, unit>(File.Copy)
    Async.FromBeginEnd((source, target, overwrite), fn.BeginInvoke, fn.EndInvoke)

let copyfile1 = asyncFileCopy("file1", "file2", true)
let copyfile2 = asyncFileCopy("file3", "file4", true)

[copyfile1; copyfile2] |> seq |>  Async.Parallel |> Async.RunSynchronously |> ignore
3个回答

3
你的问题混淆了两个问题,即多线程和异步处理。重要的是要认识到这些内容是完全不同的概念:
异步处理是关于工作流程的任务,我们独立于主程序流程响应这些任务的完成。
多线程是一种执行模型,可以用来实现异步处理,尽管异步处理可以通过其他方式实现(例如硬件中断)。
现在,当涉及到I/O时,你不应该问的问题是“我能启动另一个线程来为我完成吗?”
为什么呢?
如果你在主线程中进行某些I/O操作,通常会阻塞主线程等待结果。如果你通过创建一个新线程来规避此问题,你实际上并没有解决问题,而只是将其移动了位置。现在,你已经阻塞了你创建的一个新线程或线程池线程。哎呀,同样的问题。
线程是昂贵而宝贵的资源,不应该浪费在等待阻塞I/O完成上。
那么,真正的解决方案是什么呢?
好吧,我们通过这些其他方法之一实现异步处理。这样,我们就可以请求操作系统执行一些I/O操作,并请求它在I/O操作完成时让我们知道。这样,线程在等待结果时不会被阻塞。在Windows中,这通过一种称为I/O完成端口的东西实现。
在F#中如何实现呢?
.NET的CopyToAsync方法可能是最简单的方法。由于它返回一个普通任务,因此创建一个帮助方法会很有用:
type Async with
    static member AwaitPlainTask (task : Task) =
        task.ContinueWith(ignore) |> Async.AwaitTask

那么

[<Literal>]
let DEFAULT_BUFFER_SIZE = 4096

let copyToAsync source dest =
    async {
        use sourceFile = new FileStream(source, FileMode.Open, FileAccess.Read, FileShare.Read, DEFAULT_BUFFER_SIZE, true);
        use destFile = new FileStream(dest, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None, DEFAULT_BUFFER_SIZE, true);
        do! sourceFile.CopyToAsync(destFile) |> Async.AwaitPlainTask
    }

您可以使用 Async.Parallel 与此功能一起并发执行多个副本。

注意:这与您上面写的不同,因为 File.Copy 是一个同步方法,返回值为unit,而 CopyToAsync 是一个异步方法,返回值为Task。您不能通过在它们周围添加异步包装器来魔法般地使同步方法异步,而是需要确保一路使用异步。


如果没有使用FileOptions.Asynchronous标志创建流,CopyToAsync方法是否会执行异步复制呢?请参考例如https://dev59.com/sHNA5IYBdhLWcg3wpfiu#35467471。 - kvb
@kvb 不确定。很多例子表明这是可以的,但快速查看参考源代码让我更加怀疑。为了安全起见,我已经将其更改。 - TheInnerLight
这看起来很酷!非常感谢您的解释。您如何增强它以在IOException上进行给定次数的重试? - user1443098
1
@user1443098 或许为了更加真实和健壮的使用,你需要考虑可能会导致这些异常的原因,并且想办法通过不仅仅是重试来恢复它们。 - TheInnerLight
对于文件复制,我考虑到可能会因为网络中断、文件服务器问题等导致IO错误。为了从这些错误中恢复,通常唯一的选择就是在短暂延迟后进行重试。 - user1443098
显示剩余2条评论

1
如果你只是想在做其他事情的同时在另一个线程上运行某些东西,那么你最初的Task.Run方法应该没问题(请注意,如果调用非泛型的Task.Run,你可以获得Task<unit>,这可能会更容易处理)。但是,你应该明确你的目标 - 可以说,一个“适当”的异步文件复制不需要一个单独的.NET线程(这是一个相对较重的原语),而是依赖于操作系统特性,如完成端口; 由于System.IO.File没有提供本地的CopyAsync方法,因此你需要编写自己的方法(参见https://dev59.com/sHNA5IYBdhLWcg3wpfiu#35467471,其中包含一个简单的C#实现,很容易转换)。

1
你可以使用几个printfns来测试它。我发现我必须使用RunAsynchronously来强制主线程等待复制完成。我不确定为什么await没有起作用,但你可以看到预期的输出集,表明复制在后台发生了。
open System
open System.IO
open System.Threading
open System.Threading.Tasks
let filecopyasync (source, target) =
    let task = Task.Run((fun () ->
          printfn "CopyThread: %d" Thread.CurrentThread.ManagedThreadId; 
          Thread.Sleep(10000);  
          File.Copy(source, target, true); printfn "copydone"))

    printfn "mainThread: %d" Thread.CurrentThread.ManagedThreadId;
    let result=Async.AwaitIAsyncResult task 
    Thread.Sleep(3000)
    printfn "doing stuff"
    Async.RunSynchronously result
    printfn "done"

输出:

filecopyasync (@"foo.txt",@"bar.txt");;
mainThread: 1
CopyThread: 7
doing stuff
copydone
done

看起来不错!下一个问题是:我能确定线程池在任何给定时间有多大吗?我想限制我的线程使用,以避免压倒系统。例如,如果我正在复制许多文件,其中一些非常大。假设我只想使用5个线程(任意)。我能找出来吗? - user1443098
.NET任务使用线程池-CLR将管理您的任务并在池中安排它们。如果您想限制创建的任务数量,这个C#答案可能会有所帮助:https://dev59.com/GnE85IYBdhLWcg3wMQhm - Robert Sim

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接