【问题标题】:Closing channels with cyclical dependencies关闭具有周期性依赖关系的通道
【发布时间】:2016-08-21 23:55:54
【问题描述】:

我正在尝试在 Golang 中实现类似 mapreduce 的方法。我的设计如下:

  • 映射工作人员将项目从映射器输入通道中拉出并输出到映射器输出通道

  • 映射器输出通道然后由单个 goroutine 读取。此例程维护先前看到的键值对的映射。如果映射器输出中的下一项具有匹配键,它会将具有匹配键的新旧值都发送到 reduce-input 通道。

  • reduce-input 管道将两个值缩减为一个键值对,并将结果提交到同一个 map-output 通道。

这导致了 mapper 输出和 reduce 输入之间的循环依赖,我现在不知道如何表示 mapper 输出已完成(并关闭通道)。

打破这种循环依赖或知道何时关闭具有这种循环行为的通道的最佳方法是什么?

下面的代码有一个死锁,map输出通道和reduce输入通道互相等待。

type MapFn func(input int) (int, int)
type ReduceFn func(a int, b int) int

type kvPair struct {
    k int
    v int
}

type reducePair struct {
    k  int
    v1 int
    v2 int
}

func MapReduce(mapFn MapFn, reduceFn ReduceFn, input []int, nMappers int, nReducers int) (map[int]int, error) {
    inputMapChan := make(chan int, len(input))
    outputMapChan := make(chan *kvPair, len(input))
    reduceInputChan := make(chan *reducePair)
    outputMapMap := make(map[int]int)
    go func() {
        for v := range input {
            inputMapChan <- v
        }
        close(inputMapChan)
    }()
    for i := 0; i < nMappers; i++ {
        go func() {
            for v := range inputMapChan {
                k, v := mapFn(v)
                outputMapChan <- &kvPair{k, v}
            }
        }()
    }
    for i := 0; i < nReducers; i++ {
        go func() {
            for v := range reduceInputChan {
                reduceValue := reduceFn(v.v1, v.v2)
                outputMapChan <- &kvPair{v.k, reduceValue}
            }
        }()
    }
    for v := range outputMapChan {
        key := v.k
        value := v.v
        other, ok := outputMapMap[key]
        if ok {
            delete(outputMapMap, key)
            reduceInputChan <- &reducePair{key, value, other}
        } else {
            outputMapMap[key] = value
        }
    }
    return outputMapMap, nil
}

【问题讨论】:

  • 即使没有ReducersoutputMapChan也没有关闭,导致永远等待。我认为mapreduce 应该包含两个独立的阶段,mapreduce,而不是将它们循环在一起。
  • 困难来自reduce 阶段的递归性质。由于减少计算可能需要进一步的folding,并且这可能发生任意次数,我需要一些机制来允许reduce 输出的输出流入reduce 输入以进一步减少。
  • reduce 阶段不具有递归性质。映射的中间结果应该拆分为集合,每个集合都可以由reducer单独处理。这就是为什么mapreduce 在分配系统中工作。
  • 您可能在描述传统的 marpeduce 架构,但我特别好奇在 Go 中如何处理循环通道依赖关系。 @Amd 接受的答案很好地证明了这一点。不过,谢谢您的建议,因为您的 mapreduce cmets 通常是正确的:)

标签: go concurrency mapreduce channel


【解决方案1】:

试试这个:

package main

import "fmt"
import "sync"
import "sync/atomic"
import "runtime"
import "math/rand"
import "time"

type MapFn func(input int) *kvPair
type ReduceFn func(a int, b int) int

type kvPair struct {
    k int
    v int
}

type reducePair struct {
    k  int
    v1 int
    v2 int
}

func MapReduce(mapFn MapFn, reduceFn ReduceFn, input []int, nMappers int, nReducers int) (map[int]int, error) {
    inputMapChan := make(chan int, len(input))
    outputMapChan := make(chan *kvPair, len(input))
    reduceInputChan := make(chan *reducePair)
    outputMapMap := make(map[int]int)

    wg := sync.WaitGroup{}
    wg.Add(1)
    go func() {
        defer wg.Done()
        for _, v := range input {
            inputMapChan <- v
        }
        close(inputMapChan)
    }()

    for i := 0; i < nMappers; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for v := range inputMapChan {
                outputMapChan <- mapFn(v)
            }
        }()
    }

    finished := false
    go func() {
        wg.Wait()
        finished = true
    }()

    var count int64
    for i := 0; i < nReducers; i++ {
        go func() {
            for v := range reduceInputChan {
                reduceValue := reduceFn(v.v1, v.v2)
                outputMapChan <- &kvPair{v.k, reduceValue}
                atomic.AddInt64(&count, -1)
            }
        }()
    }

    wg2 := sync.WaitGroup{}
    wg2.Add(1)
    go func() {
        defer wg2.Done()
        for {
            select {
            default:
                if finished && atomic.LoadInt64(&count) == 0 && len(outputMapChan) == 0 {
                    return
                }
                //runtime.Gosched()
            case v := <-outputMapChan:
                key := v.k
                value := v.v
                if other, ok := outputMapMap[key]; ok {
                    delete(outputMapMap, key)
                    atomic.AddInt64(&count, 1)
                    reduceInputChan <- &reducePair{key, value, other}
                } else {
                    outputMapMap[key] = value
                }
            }
        }
    }()

    wg2.Wait()
    return outputMapMap, nil
}

func main() {
    fmt.Println("NumCPU =", runtime.NumCPU())
    t := time.Now()
    a := rand.Perm(1000000)
    //a = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 12, 13, 1, 16, 2}
    m, err := MapReduce(mp, rdc, a, 2, 2)
    if err != nil {
        panic(err)
    }
    fmt.Println(time.Since(t)) //883ms
    fmt.Println(m)
    fmt.Println("done.")
}

func mp(input int) *kvPair {
    return &kvPair{input & 7, input >> 3}
}
func rdc(a int, b int) int {
    b <<= 3
    if a != 0 {
        b |= a
    }
    return b
}

【讨论】:

  • @jack-reilly 我希望这会有所帮助
  • 做得很好!感谢您向我展示如何使用等待组和原子计数器来同步这些块。
猜你喜欢
  • 2021-02-20
  • 2013-11-05
  • 1970-01-01
  • 1970-01-01
  • 2019-03-31
  • 2014-09-06
  • 1970-01-01
  • 1970-01-01
  • 2011-06-06
相关资源
最近更新 更多