问题
I have many actions (Async<T> list
) to perform in F#. I can execute most of these actions in parallel, but some might conflict due to file locks etc.
For each action, I can generate a "key" (int
) that determines if the actions might conflict:
- If action
a
has keyi
and actionb
has keyj
andi = j
, thena
andb
might conflict. They must be executed serially. - If action
a
has keyi
and actionb
has keyj
andi <> j
, thena
andb
will never conflict. They may be executed in parallel.
I would like to execute my actions (int * Async<T>) list
in an efficient way and without conflicts.
I imagine that the process would be something like:
- Group all actions by key
- Chain each group serially into one
Async
- Run each chain in parallel
How can I implement this in F#?
How are these problems usually handled?
My attempt at a fully sequential implementation:
let wrapTasks<'T> (tasks : (int * Async<'T>) list) : Async<'T list> = async {
return
tasks
|> Seq.map (fun (k, t) -> t |> Async.RunSynchronously)
|> Seq.toList
}
回答1:
With a helper function taking a 'promise' for a value x
and one for a set of values acc
:
module Async =
let sequence x acc = async {
let! x = x
let! y = acc
return x :: y
}
we can asynchronously group the tasks
by their 'lock id', clean up the resulting list a bit and then sequence
each group into a single async
that 'contains' the list of the results of its group. This list is then processed in parallel. Once ts : 'b list []
is available, we flatten it:
let wrapTasks tasks = async {
let! ts =
tasks
|> List.groupBy fst
|> List.map (snd >> List.map snd)
|> List.map (fun asyncs -> List.foldBack Async.sequence asyncs (async { return [] }))
|> Async.Parallel
return ts |> List.ofArray |> List.collect id
}
This can be tested with e.g.
List.init 50 (fun i -> i % 5, async {
let now = System.DateTime.UtcNow.Ticks
do! Async.Sleep 10
return i, now })
|> wrapTasks
|> Async.RunSynchronously
|> List.groupBy snd
|> List.map (fun (t, rs) -> t, rs |> List.map fst)
|> List.sort
By varying the divisor we can adjust the level of parallelism and convince ourselves that the function works as expected :-)
[(636766393199727614L, [0; 1; 2; 3; 4]); (636766393199962986L, [5; 6; 7; 8; 9]); (636766393200068008L, [10; 11; 12; 13; 14]); (636766393200278385L, [15; 16; 17; 18; 19]); (636766393200382690L, [20; 21; 22; 23; 24]); (636766393200597692L, [25; 26; 27; 28; 29]); (636766393200703235L, [30; 31; 32; 33; 34]); (636766393200918241L, [35; 36; 37; 38; 39]); (636766393201027938L, [40; 41; 42; 43; 44]); (636766393201133307L, [45; 46; 47; 48; 49])]
Full disclosure: I had to execute the test a few times for getting this nice result. Usually numbers will be a bit off.
回答2:
This is a possible solution:
let wrapTasks (tasks : (int * Async<'T>) list) =
tasks
|> List.groupBy fst
|> Seq.map (fun (k, ts) -> async {
for (i, t) in ts do
let! r = t
()
})
|> Async.Parallel
|> Async.RunSynchronously
来源:https://stackoverflow.com/questions/53092395/controlling-async-actions-that-might-conflict-in-f