在嵌套循环中并行化代码

时间:2009-01-05 03:52:55

标签: multithreading f# async-workflow

您总是听说功能代码本身比非功能代码更容易并行化,因此我决定编写一个执行以下操作的函数:

给定字符串输入,总计每个字符串的唯一字符数。因此,根据输入[ "aaaaa"; "bbb"; "ccccccc"; "abbbc" ],我们的方法将返回a: 6; b: 6; c: 8

这是我写的:

(* seq<#seq<char>> -> Map<char,int> *)
let wordFrequency input =
    input
    |> Seq.fold (fun acc text ->
        (* This inner loop can be processed on its own thread *)
        text
        |> Seq.choose (fun char -> if Char.IsLetter char then Some(char) else None)
        |> Seq.fold (fun (acc : Map<_,_>) item ->
            match acc.TryFind(item) with
            | Some(count) -> acc.Add(item, count + 1)
            | None -> acc.Add(item, 1))
            acc
        ) Map.empty

此代码理想情况下可并行化,因为input中的每个字符串都可以在自己的线程上处理。它不像它看起来那么简单,因为内部循环将项添加到所有输入之间共享的Map。

我希望将内部循环考虑到自己的线程中,我不想使用任何可变状态。 如何使用Async工作流程重新编写此功能?

4 个答案:

答案 0 :(得分:3)

你可以这样写:

let wordFrequency =
  Seq.concat >> Seq.filter System.Char.IsLetter >> Seq.countBy id >> Map.ofSeq

并将其与仅两个额外字符并行化,以使用PSeq DLL中的FSharp.PowerPack.Parallel.Seq模块而不是普通的Seq模块:

let wordFrequency =
  Seq.concat >> PSeq.filter System.Char.IsLetter >> PSeq.countBy id >> Map.ofSeq

例如,从5.5Mb King James圣经计算频率所需的时间从4.75秒降至0.66秒。这个8核机器的速度提高了7.2倍。

答案 1 :(得分:2)

正如已经指出的那样,如果您尝试让不同的线程处理不同的输入字符串,则存在更新争用,因为每个线程都可以增加每个字母的计数。您可以让每个线程生成自己的Map,然后“添加所有映射”,但最后一步可能很昂贵(并且由于共享数据而不太适合利用线程)。我认为使用类似下面的算法可以更快地运行大输入,其中每个线程处理不同的字母数(对于输入中的所有字符串)。因此,每个线程都有自己独立的计数器,因此没有更新争用,也没有最后一步来组合结果。但是,我们需要预处理才能发现'一组唯一字母',而且这一步确实存在相同的争用问题。 (实际上,你可能预先了解了角色的宇宙,例如字母,然后可以创建26个线程来处理az,并绕过这个问题。)无论如何,大概是问题主要是探索'如何编写F#用于跨线程划分工作的异步代码,因此下面的代码演示了它。

#light

let input = [| "aaaaa"; "bbb"; "ccccccc"; "abbbc" |]

// first discover all unique letters used
let Letters str = 
    str |> Seq.fold (fun set c -> Set.add c set) Set.empty 
let allLetters = 
    input |> Array.map (fun str -> 
        async { return Letters str })
    |> Async.Parallel 
    |> Async.Run     
    |> Set.union_all // note, this step is single-threaded, 
        // if input has many strings, can improve this

// Now count each letter on a separate thread
let CountLetter letter =
    let mutable count = 0
    for str in input do
        for c in str do
            if letter = c then
                count <- count + 1
    letter, count
let result = 
    allLetters |> Seq.map (fun c ->
        async { return CountLetter c })
    |> Async.Parallel 
    |> Async.Run

// print results
for letter,count in result do
    printfn "%c : %d" letter count

我确实'完全改变了算法',主要是因为我所拥有的原始算法并不特别适合由于更新争用而导致数据并行化。根据您要学习的内容,这个答案可能会或可能不会特别令您满意。

答案 2 :(得分:1)

并行与async不同,Don Syme explains

所以IMO你最好使用PLINQ进行并行化。

答案 3 :(得分:0)

我不会说F#,但我可以解决这个问题。考虑使用map / reduce:

n = 卡(Σ)为字母表Σ中的符号数σ。

地图阶段:

Spawn n 进程,其中 i -th进程的赋值是为了计算符号σ i 在整个输入向量中。

减少阶段

按顺序收集每个 n 进程的总数。那个向量就是你的结果。

现在,此版本不会对串行版本产生任何改进;我怀疑这里存在隐藏依赖性,这使得这本身难以并行化,但是我太累了,脑力不足以证明它今晚。