[英]F# parallel design pattern
我试图在F#Console项目中并行运行一些相同的任务。
任务如下
ConcurrentQueue
取出表名(此队列包含我的程序需要处理的表名) 所以基本上每个任务都是一个while循环(姿势作为递归)来连续处理表。 我想并行启动4个任务,另一件事是我想在控制台上用户按键停止执行,比如Enter
键。 但只有当前任务完成第4步时才应停止执行。
我尝试了以下内容
let rec DownloadHelper (cq:ConcurrentQueue<Table>) sqlConn =
let success, tb = cq.TryDequeue()
if success then
printfn "Processing %s %s" tb.DBName tb.TBName
Table2CSV tb.DBName tb.TBName sqlConn
DownloadHelper cq sqlConn
let DownloadTable (cq:ConcurrentQueue<Table>) connectionString=
use con = new SqlConnection(connectionString)
con.Open()
DownloadHelper cq con
let asyncDownloadTask = async { return DownloadTable cq connectionString}
let asyncMultiDownload =
asyncDownloadTask
|> List.replicate 4
|> Async.Parallel
asyncMultiDownload
|>Async.RunSynchronously
|>ignore
上面的代码有两个问题,
我的第二次尝试如下使用CancellationToken,
let tokenSource = new CancellationTokenSource()
let cq = PrepareJobs connectionString
let asyncDownloadTask = async { DownloadTable cq connectionString}
let task = async {
asyncDownloadTask
|> List.replicate 4
|> Async.Parallel
|>ignore}
let val1 = Async.Start(task, cancellationToken =tokenSource.Token)
Console.ReadLine() |> ignore
tokenSource.Cancel()
Console.ReadLine() |> ignore
0
但似乎我根本无法开始任务。
您的代码有三个问题。
首先, DownloadHelper
应该只做一个表。 通过使其递归,您正在采取过多控制并抑制并行性。
其次,简单地将操作放在async
表达式中并不会神奇地使其异步。 除非DownloadTable
函数本身是异步的,否则代码将一直阻塞直到完成。
因此,当您并行运行四次下载时,一旦启动,无论取消令牌如何,它们都将全部运行完成。
第三,在你的第二个例子中,你使用Async.Parallel
但是然后抛弃输出,这就是为什么你的task
什么都不做! 我想你想要做的就是抛弃异步的结果 ,而不是异步本身。
这是我的代码版本,用于演示这些要点。
首先,一个占用时间的虚函数:
let longAtomicOperation milliSecs =
let sw = System.Diagnostics.Stopwatch()
let r = System.Random()
let mutable total = 0.0
sw.Start()
while sw.ElapsedMilliseconds < int64 milliSecs do
total <- total + sin (r.NextDouble())
// return something
total
// test
#time
longAtomicOperation 2000
#time
// Real: 00:00:02.000, CPU: 00:00:02.000, GC gen0: 0, gen1: 0, gen2: 0
请注意,此功能不是异步 - 一旦启动它将运行完成。
现在让我们把它变成一个async
:
let asyncTask id = async {
// note that NONE of the operations are async
printfn "Started %i" id
let result = longAtomicOperation 5000 // 5 seconds
printfn "Finished %i" id
return result
}
异步块中的所有操作都不是异步的,因此我们没有任何好处。
以下是并行创建四个任务的代码:
let fourParallelTasks = async {
let! results =
List.init 4 asyncTask
|> Async.Parallel
// ignore
return ()
}
不会忽略Async.Parallel
的结果,但Async.Parallel
其分配给一个值,该值会强制运行任务。 async
表达式作为一个整体返回单位。
如果我们测试它:
open System.Threading
// start the task
let tokenSource = new CancellationTokenSource()
do Async.Start(fourParallelTasks, cancellationToken = tokenSource.Token)
// wait for a keystroke
System.Console.WriteLine("press a key to cancel")
System.Console.ReadLine() |> ignore
tokenSource.Cancel()
System.Console.ReadLine() |> ignore
即使按下某个键,我们也会得到这样的输出,因为一旦启动,每个任务都将运行完成:
press a key to cancel
Started 3
Started 1
Started 2
Started 0
Finished 1
Finished 3
Finished 2
Finished 0
另一方面,如果我们创建一个串行版本,如下所示:
let fourSerialTasks = async {
let! result1 = asyncTask 1
let! result2 = asyncTask 2
let! result3 = asyncTask 3
let! result4 = asyncTask 4
// ignore
return ()
}
然后,即使任务是原子的,也会在每个步骤之间测试取消令牌,这允许取消子序列任务。
// start the task
let tokenSource = new CancellationTokenSource()
do Async.Start(fourSerialTasks, cancellationToken = tokenSource.Token)
// wait for a keystroke
System.Console.WriteLine("press a key to cancel")
System.Console.ReadLine() |> ignore
tokenSource.Cancel()
System.Console.ReadLine() |> ignore
按下键时,可以在每个步骤之间取消上述代码。
要以四个批次的方式处理队列中的所有元素,只需将并行版本转换为循环:
let rec processQueueAsync() = async {
let! result = processFourElementsAsync()
if result <> QueueEmpty then
do! processQueueAsync()
// ignore
return ()
}
最后,对我来说,使用async 不是关于并行运行,而是编写非阻塞代码。 因此,如果您的库代码是阻塞的,那么异步方法不会带来太多好处。
要确保代码是非阻塞的,您需要在助手中使用异步版本的SqlDataReader
方法,例如NextResultAsync
。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.