UPDATE
Лучше, чем ленивое значение, Async.StartChild
, предложенное Петричеком, поэтому я изменил lazyDownload
на asyncDownload
Вы можете использовать MailboxProcessor
в качестве менеджера загрузки, который обрабатывает кэш. MailboxProcessor - это структура в F #, которая обрабатывает очередь сообщений, не допуская коллизий.
Сначала вам нужен процессор, способный поддерживать состояние:
let stateFull hndl initState =
MailboxProcessor.Start(fun inbox ->
let rec loop state : Async<unit> = async {
try let! f = inbox.Receive()
let! newState = f state
return! loop newState
with e -> return! loop (hndl e state)
}
loop initState
)
Первый параметр - это обработчик ошибок, второй - начальное состояние, в данном случае Map<string, Async<string>>
. Это наш downloadManager
:
let downloadManager =
stateFull (fun e s -> printfn "%A" e ; s) (Map.empty : Map<string, _>)
Для вызова MailBox нам нужно использовать PostAndReply
:
let applyReplyS f (agent: MailboxProcessor<'a->Async<'a>>) =
agent.PostAndReply(fun (reply:AsyncReplyChannel<'r>) ->
fun v -> async {
let st, r = f v
reply.Reply r
return st
})
Эта функция ожидает функцию папки, которая проверяет кэш и добавляет Async<string>
, если ничего не найдено, и возвращает обновленный кеш.
Сначала функция asyncDownload
:
let asyncDownload url =
async {
let started = System.DateTime.UtcNow.Ticks
do! Async.Sleep 30
let finished = System.DateTime.UtcNow.Ticks
let r = sprintf "Downloaded %A it took: %dms %s" (started / 10000L) ((finished - started) / 10000L) url
printfn "%s" r
return r
}
Просто фиктивная функция, которая возвращает строку и информацию о времени.
Теперь функция папки, которая проверяет кеш:
let folderCache url cache =
cache
|> Map.tryFind url
|> Option.map(fun ld -> cache, ld)
|> Option.defaultWith (fun () ->
let ld = asyncDownload url |> Async.StartChild |> Async.RunSynchronously
cache |> Map.add url ld, ld
)
наконец, наша функция загрузки:
let downloadUrl url =
downloadManager
|> applyReplyS (folderCache url)
// val downloadUrl: url: string -> Async<string>
Тестирование
let s = System.DateTime.UtcNow.Ticks
printfn "started %A" (s / 10000L)
let res =
List.init 50 (fun i -> i, downloadUrl (string <| i % 5) )
|> List.groupBy (snd >> Async.RunSynchronously)
|> List.map (fun (t, ts) -> sprintf "%s - %A" t (ts |> List.map fst ) )
let f = System.DateTime.UtcNow.Ticks
printfn "finish %A" (f / 10000L)
printfn "elapsed %dms" ((f - s) / 10000L)
res |> printfn "Result: \n%A"
выдает следующий вывод:
started 63676683215256L
Downloaded 63676683215292L it took: 37ms "2"
Downloaded 63676683215292L it took: 36ms "3"
Downloaded 63676683215292L it took: 36ms "1"
Downloaded 63676683215291L it took: 38ms "0"
Downloaded 63676683215292L it took: 36ms "4"
finish 63676683215362L
elapsed 106ms
Result:
["Downloaded 63676683215291L it took: 38ms "0" - [0; 5; 10; 15; 20; 25; 30; 35; 40; 45]";
"Downloaded 63676683215292L it took: 36ms "1" - [1; 6; 11; 16; 21; 26; 31; 36; 41; 46]";
"Downloaded 63676683215292L it took: 37ms "2" - [2; 7; 12; 17; 22; 27; 32; 37; 42; 47]";
"Downloaded 63676683215292L it took: 36ms "3" - [3; 8; 13; 18; 23; 28; 33; 38; 43; 48]";
"Downloaded 63676683215292L it took: 36ms "4" - [4; 9; 14; 19; 24; 29; 34; 39; 44; 49]"]