由并发引出的话题

并发是一个超大的话题,本文做一些简单的记录和讨论。

基本概念

有必要先了解下与并发密切相关的几个概念。

Critical Section 临界区

所谓临界区,就是在程序中一段代码不能被同时执行的部分。比如,两个线程不能同时去访问的程序代码,这段代码就是临界区。在临界区中,通常会有对共享变量读取或者写入的操作。之前我文章中提到的 就是用来保证只有一个线程处于临界区的一种机制,它保证了多个线程对共享变量的读写正常。

Thread Safe 线程安全

wikipedia中的定义:

指某个函数、函数库在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。

Concurrency vs Parallelism 并发 vs 并行

两个概念相似,但不同。并发是指,在同一时刻,只有一个任务在运行,只是系统通过某种调度算法,同时调度、来回切换运行两个任务,让它们“看起来”是同时在运行。并行则是说,同一时刻,两个任务“真正的”同时进行。

举个栗子,老板布置给你一些任务,很复杂,但你不能一个人同时做。所以你把任务拆分成一些小活儿,交替着干,这是并发。不过你也可以将任务分给你的同事,这样就能真正的同时做了,这是并行。

体现在计算机系统中,当多个任务执行时,单核cpu通过切换时间片(时间片就是cpu分配给各个任务的时间),将计算资源合理的分配(轮转调度)给各个任务,当时间片结束,或者任务阻塞,则cpu进行切换(好处是不会阻塞其他任务,提升了程序运行效率)。这样多个任务看起来就是同时在进行,这是并发。系统将多个任务分配到多核cpu上,这时多个任务就可以真正的同时运行,这是并行

Golang 中的并发

Golang中通过goroutine实现了并发。如下的几个函数,实现了对goroutine的调度。

  • runtime.Gosched() 出让cpu时间片
  • runtime.NumCPU() 返回当前机器cpu数量
  • runtime.GOMAXPROCS() 设置同时最大的CUP可用核数
  • runtime.Goexit() 退出当前goroutine

下面是官方的一个示例程序,会将hello和world交替输出,因为在say函数中runtime.Gosched()会主动让出时间片,将cpu资源让给另外一个goroutine,这就实现了并发。如果去掉runtime.Gosched()这一行,两个goroutine将会按顺序执行,因为执行时间很短,goroutine不会主动让出时间片。(本地测试发现,在go1.9.2环境下输出并不是完全交替,有时甚至不会输出world,说明最新的编译器的实现有了些变化?)

package main

import (
    "fmt"
    "time"
    "runtime"
)

func say(s string) {
    for i := 0; i < 5; i++ {
        runtime.Gosched()
        // 如果将手动切换时间片的操作改成sleep操作,输出效果一样
        // 因为sleep操作也会触发切换时间片
        // time.Sleep(time.Millisecond)
        fmt.Println(s)
    }
}

func main() {
    // 这里如果强制将最大可用cpu核数设置成1
    // 则hello和world完全交替输出
    // 说明在最新版的golang中,GOMAXPROCS默认已经不是1了 ?
    // runtime.GOMAXPROCS(1)
    go say("world")
    say("hello")
}

再看第二个示例,去除了runtime.Gosched(),同时将最大可用cpu核数设置成了当前系统cpu的核数runtime.GOMAXPROCS(runtime.NumCPU())(我本地是4核),输出结果会出现交替的情况,但不绝对,因为程序执行太快退出了,调大循环次数就能看到效果。这时两个goroutine分别在不同的cpu核心上运行,实现了并行

package main

import (
    "fmt"
    "runtime"
)

func say(s string) {
    for i := 0; i < 5; i++ {
        fmt.Println(s)
    }
}

func main() {
    runtime.GOMAXPROCS(runtime.NumCPU())
    go say("world")
    say("hello")
}

stackoverflow上的一个问题,对golang的中的并发与并行的一些知识点和原理解释得很清楚,值得一看。

实际应用

如果使用Golang,通常我们可以将一个复杂的任务拆分成n个相关性不大的个小任务,然后分别用goroutine去执行,使用channel来进行数据通信,这样就实现了并发。如果拆分得好,我们可以的得到n倍的一个执行效率。至于并行和并发,就交给runtime去管理吧。

当多个goroutine执行时,要注意在临界区我们要使用锁来保证代码的正确执行,这样才能保证线程安全

参考

标签: golang, concurrency

添加新评论