繁体   English   中英

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

[英]Concurrency with multiple producers/multiple consumers

我可能在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次数字)。 我曾期望一些消费者会打印出完全相同的数字,但是即使有20个goroutines为generateStuff提供服务,通过增加全局变量来生成数字, requestChan仍在像屏障一样工作,以防止出现这种情况。

我一般对Go或并发性有什么疑问?

我曾希望有一种情况发生,比如两个generateStuff类型的go例程会一起被唤醒,并同时增加seq,从而使两个使用者两次打印相同的数字。

在playgolang上的编辑代码: http ://play.golang.org/p/eRzNXjdxtZ

您有多个可以同时运行并且可以同时尝试并发出请求的工作程序。 由于requestChan是无缓冲的,因此它们全部阻塞,等待阅读器同步并接受他们的请求。

您有多个生成器,这些生成器将通过requestChan与请求者同步,产生一个结果,然后在未缓冲的generatorChan上进行阻塞,直到工作者读取结果为止。 请注意,它可能是其他工人。

没有额外的同步,因此其他所有内容都是不确定的。

  • 一个生成器可以列出所有请求。
  • 生成器可以捕获一个请求,并在其他任何生成器有机会运行之前通过递增seq进行处理。 仅使用一个处理器,这甚至可能。
  • 所有生成器都可以捕获请求,并且最终seq完全同时增加seq ,从而导致各种问题。
  • 工人们可以从碰巧发送到的同一发电机中获得响应,也可以从完全不同的发电机中获得响应。

通常,没有添加同步来强制其中一种行为,就无法确保其中任何一种确实发生。

请注意,在数据竞争中,它本身是另一个非确定性事件。 可能会获得任意值,程序崩溃等。假定在竞争条件下该值可能只是一个或某些相对无害的结果而被抵消是不安全的。

为了进行试验,您可能最GOMAXPROCS是启动GOMAXPROCS 通过环境变量(例如, env GOMAXPROCS=16 ./foogo build之后env GOMAXPROCS=16 go run foo.goenv GOMAXPROCS=16 ./foo ),或者从程序中调用runtime.GOMAXPROCS(16) 默认值为1,这意味着可能会隐藏数据争用或其他“奇怪”行为。

您还可以通过在不同时间点添加对runtime.Goschedtime.Sleep调用来稍微影响事物。

如果您使用竞争检测器(例如,使用go run -race foo.googo build -race ),也可以看到数据竞争。 该程序不仅应在出口处显示“找到1个数据竞赛”,而且还应在首次检测到竞赛时转储很多带有堆栈跟踪的详细信息。

这是您的代码的“清理”版本,用于实验:

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)
}

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

由于前者可以保证并发性,消除数据争用,使输出看起来更好等,因此在很大程度上可以进行较小的更改,例如将生成器分流,使用log而不是fmt

频道类型

通道提供了一种机制,用于通过发送和接收指定元素类型的值来同时执行功能以进行通信。 未初始化通道的值为nil。

可以使用内置函数make创建新的初始化通道值,该函数将通道类型和可选容量作为参数:

 make(chan int, 100) 

容量(以元素数为单位)设置通道中缓冲区的大小。 如果容量为零或不存在,则通道是无缓冲的,并且仅在发送方和接收方都准备就绪时通信才能成功。 否则,如果缓冲区未满(发送)或不为空(接收),则通道将被缓冲,并且通信将成功进行而不会阻塞。 零通道永远不会准备好进行通信。

您正在通过使用无缓冲通道来限制通道通信。

例如,

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM