与多个生产者/多个消费者的并发

时间:2015-04-04 19:55:53

标签: concurrency go producer-consumer

我可能遗漏了一些东西,或者没有理解Go如何处理并发(或者我对并发本身的了解),我已经设计了一些代码来理解多个生产者/消费者。

这是代码:

package main

import (
    "fmt"
    "time"
    // "math/rand"
    "sync"
)

var seq uint64 = 0
var generatorChan chan uint64
var requestChan chan uint64

func makeTimestamp() int64 {
    return time.Now().UnixNano() / int64(time.Millisecond)
}

func generateStuff(genId int) {
    var crap uint64
    for {
        crap = <-requestChan
        // <- requestChan
        seq = seq+1
        fmt.Println("Gen ", genId, " - From : ", crap, " @", makeTimestamp())
        generatorChan <- uint64(seq)
    }
}

func concurrentPrint(id int, work *sync.WaitGroup) {
    defer work.Done()

    for i := 0; i < 5; i++ {
        requestChan<-uint64(id)
        fmt.Println("Conc", id, ": ", <-generatorChan)
    }
}

func main() {
    generatorChan = make(chan uint64)
    requestChan = make(chan uint64)
    var wg sync.WaitGroup
    for i := 0; i < 20; i++ {
        go generateStuff(i)
    }
    maximumWorker := 200
    wg.Add(maximumWorker)
    for i := 0; i < maximumWorker; i++ {
        go concurrentPrint(i, &wg)
    }
    wg.Wait()
}

当它运行时,它打印(主要是按顺序)所有数字从1到1000(200个消费者得到每个数字5次)。 我希望有些消费者可以打印完全相同的数字,但似乎 requestChan 就像屏障一样,即使有20个goroutines服务于 generateStuff 通过增加全局变量来生成数字。

一般来说Go或Concurrency我有什么问题?

我会预料到类似 generateStuff 的两个例行程序的情况会一起醒来并同时增加seq,因此有两个消费者打印相同的数字二次。

编辑关于playgolang的代码:http://play.golang.org/p/eRzNXjdxtZ

2 个答案:

答案 0 :(得分:1)

您有多个工作人员可以同时运行,并且所有工作人员同时尝试并发出请求。由于requestChan是无缓冲的,因此它们都阻止等待读者同步并接受他们的请求。

您有多个生成器将通过requestChan与请求者同步,生成结果,然后阻止未缓冲的generatorChan,直到工作者读取结果。请注意,它可能是一个不同的工作人员。

没有其他同步,所以其他一切都是非确定性的。

  • 一台发电机可以记录所有请求。
  • 生成器可以抓取请求并完成递增seq 在任何其他发电机碰巧有机会运行之前。甚至可能只有一个处理器。
  • 所有生成器都可以抓取请求,并且最终都希望在同一时间增加seq,从而导致各种问题。
  • 工作人员可以从他们碰巧发送到或来自完全不同的发电机的同一发电机获得响应。

通常,如果不添加同步来强制执行其中一种行为,则无法确保实际发生这些行为。

请注意,对于数据竞争,这本身就是另一个非确定性事件。可能会得到任意值,程序崩溃等等。假设在竞争条件下,这个值可能只是一个或一些相对无害的结果,这是不安全的。

对于实验,您可以做的最好的事情是GOMAXPROCS。通过环境变量(例如env GOMAXPROCS=16 go run foo.go之后的env GOMAXPROCS=16 ./foogo build)或通过调用程序中的runtime.GOMAXPROCS(16)。默认值为1,这意味着可能隐藏数据竞赛或其他“奇怪”行为。

您还可以通过在不同位置添加对runtime.Goschedtime.Sleep的调用来稍微影响一些事情。

如果您使用竞争检测器(例如,使用go run -race foo.googo build -race),您还可以看到数据竞争。该程序不仅应该在退出时显示“Found 1 data race(s)”,而且还应该在首次检测到比赛时使用堆栈跟踪转储大量细节。

以下是您的实验代码的“清理”版本:

package main

import (
    "log"
    "sync"
    "sync/atomic"
)

var seq uint64 = 0
var generatorChan = make(chan uint64)
var requestChan = make(chan uint64)

func generator(genID int) {
    for reqID := range requestChan {
        // If you want to see a data race:
        //seq = seq + 1
        // Else:
        s := atomic.AddUint64(&seq, 1)
        log.Printf("Gen: %2d, from %3d", genID, reqID)
        generatorChan <- s
    }
}

func worker(id int, work *sync.WaitGroup) {
    defer work.Done()

    for i := 0; i < 5; i++ {
        requestChan <- uint64(id)
        log.Printf("\t\t\tWorker: %3d got %4d", id, <-generatorChan)
    }
}

func main() {
    log.SetFlags(log.Lmicroseconds)
    const (
        numGen    = 20
        numWorker = 200
    )
    var wg sync.WaitGroup
    for i := 0; i < numGen; i++ {
        go generator(i)
    }
    wg.Add(numWorker)
    for i := 0; i < numWorker; i++ {
        go worker(i, &wg)
    }
    wg.Wait()
    close(requestChan)
}

Playground(但请注意,操场上的时间戳不会有用,并且调用runtime.MAXPROCS可能无法执行任何操作)。进一步请注意,操场缓存结果,因此重新运行完全相同的程序将始终显示相同的输出,您需要进行一些小的更改或只是在您自己的机器上运行它。

使用logfmt分析生成器的大小变化,因为前者提供并发保证,删除数据竞争,使输出看起来更好等等。

答案 1 :(得分:0)

  

Channel types

     

通道提供了一种同时执行函数的机制   通过发送和接收指定元素的值进行通信   类型。未初始化频道的值为零。

     

可以使用内置的新的初始化通道值   function make,它采用通道类型和可选容量   作为参数:

make(chan int, 100)
     

容量(以元素数量)设置缓冲区的大小   这个频道。如果容量为零或不存在,则通道为   无缓冲和通信只有当发送者和发送者都成功时才会成功   接收器准备好了。否则,通道被缓冲   如果缓冲区未满,则通信成功而不会阻塞   (发送)或不空(接收)。零通道永远不会准备好   通信。

您使用无缓冲的频道限制频道通信。

例如,

generatorChan = make(chan uint64)
requestChan = make(chan uint64)