jasper的技术小窝

关注DevOps、运维监控、Python、Golang、开源、大数据、web开发、互联网

Golang中非你所想的runtime.GOMAXPROCS(1)

作者:jasper | 分类:Golang | 标签:   | 阅读 395 次 | 发布:2017-08-06 5:42 p.m.

在Golang中,我们可以通过runtime.GOMAXPROCS(x)来指定运行当前应用的系统线程数,而goroutine实际上也是运行在系统线程上的,那么如果我们将runtime.GOMAXPROCS(1)设为1,是不是就表示应用在单线程上运行,这样就可以不用加锁或者channel啦?答案是否定的,我们来细聊一下。

并发和并行

首先需要理解一下并发和并行,这个在网上一搜一大片,这里就不细说;对于runtime.GOMAXPROCS(1)这种场景下,是“并发但不并行”。

线程和goroutine

Golang中的任何函数或方法都可以创建为goroutine。我们可以认为主函数正在执行为goroutine,但是Golang运行时并没有启动goroutine。 Goroutines被认为是轻量级的,因为它们使用的内存和资源很少,而且它们的初始堆栈大小很小。在版本1.2之前,堆栈大小从4K开始,1.2版本之后,从8K开始。堆栈有能力根据需要增长。

操作系统调度线程以对可用处理器运行,并且Golang运行时计划goroutines在绑定到单个操作系统线程的逻辑处理器内运行。如果Golang运行时分配一个逻辑处理器(runtime.GOMAXPROCS(1))来执行为我们的程序创建的所有goroutine。即使使用这个单一的逻辑处理器和操作系统线程,也可以安排数十万个goroutine同时运行,同时具有惊人的效率和性能。

如果将运行时配置为使用多个逻辑处理器,则调度程序将在这些逻辑处理器之间分配goroutines,这将导致goroutines在不同的操作系统线程上运行。但是,要实现真正的并行性,您需要在具有多个物理处理器的机器上运行程序。否则,即使Golang运行时正在使用多个逻辑处理器,goroutines将同时运行在单个物理处理器上。

例子

在应用程序中构建并发,我们的goroutine将尝试访问同一个资源。读写共享资源必须始终是原子的。换句话说,读和写必须由goroutine一次发生,因为在多个goroutine中如果不可见,顺序就不能保证,在我们的程序中就存在data race。

type test struct {
    Num int
}

func main() {
    runtime.GOMAXPROCS(1)

    t := &test{}

    go func() {
        for i := 0; i < 100; i++ {
            time.Sleep(300 * time.Microsecond)
            num := t.Num
            time.Sleep(300 * time.Microsecond)
            t.Num = num + 1
        }
    }()

    go func() {
        for i := 0; i < 100; i++ {
            num := t.Num
            time.Sleep(300 * time.Microsecond)
            t.Num = num + 1
        }
    }()

    time.Sleep(10 * time.Second) // 等待goroutine完成
    fmt.Println(t.Num)

}

这里我们设置了runtime.GOMAXPROCS(1),如果都顺序执行,那么最后的输出应该是200,然而事实情况下不是。我们用race来检测一下(检测方法见上文)必然存在着data race。结果我就不贴了,那么要达到我们预期的效果,上面的例子我们这样改:

func main() {
    runtime.GOMAXPROCS(1)

    t := &test{}
    ch := make(chan int, 2)

    go func() {
        for i := 0; i < 100; i++ {
            t.lock.Lock()
            time.Sleep(300 * time.Microsecond)
            num := t.Num
            time.Sleep(300 * time.Microsecond)
            t.Num = num + 1
            t.lock.Unlock()
        }
        ch <- 1
    }()

    go func() {
        for i := 0; i < 100; i++ {
            t.lock.Lock()
            num := t.Num
            time.Sleep(300 * time.Microsecond)
            t.Num = num + 1
            t.lock.Unlock()
        }
        ch <- 1
    }()
    for i:=0;i<2 ;i++  {
        <- ch
    }
    fmt.Println(t.Num)
}

这样就能得到我们所要的结果200了,而且用race检测也通过了,注意这里我们用到了channel和lock,其中channel保证了main goroutine和两个goroutine的可见性,而lock保证了两个goroutine之间的可见性。

结论

其实在早期的版本中默认就是runtime.GOMAXPROCS(1),在资源足够的情况下,仍然不会影响goroutine的效率。用runtime.GOMAXPROCS(1)也保证不了任何事情,当然在实际的开发中更不应该这么去设定。


转载请注明出处:http://www.opscoder.info/golang_GOMAXPROCS_1

其他分类: