go中的并发

go中的并发

从并发模型说起

并发目前来看比较主流的就三种:

  1. 多线程
    • 每个线程一次处理一个请求,线程越多可并发处理的请求数就越多
    • 在高并发下,多线程的调度开销会比较大。
  2. 协程
    • 无需抢占式的调度,开销小,可以有效的提高线程的并发性,从而避免了线程的缺点的部分
  3. 基于异步回调的IO模型
    • 利用Linux内核的AIO进行异步IO1
    • nginx使用的就是epoll模型,通过事件驱动的方式与异步IO回调(伪异步,实际上是程序循环等待IO的出现)

goroutine

简介

在go里面,使用goroutine进行并发操作。

goroutine的本质是协程2,也可以认为是轻量级的线程,与创建线程相比,创建成本和开销都很小。同时,Golang 在 runtime、系统调用等多方面对 goroutine 调度进行了封装和处理,当遇到长时间执行或者进行系统调用时,会主动把当前 goroutine 的CPU § 转让出去,让其他 goroutine 能被调度并执行。

Golang 从语言层面支持了协程,从语言层面支持了高并发。

使用3

(这儿不会对语法有过多的说明,大家请去官网查看,官网的教程非常详细,远比其他网页上的资料更全面)

当一个程序启动的时候,只有一个goroutine来调用main函数,称它为主goroutine。新的goroutine通过在函数或者方法前面加上关键字go进行创建。

package main

import (
    "fmt"
    "time"
)

func Hello() {
    fmt.Println("Hello")
}

func main() {
    go Hello()                 // 开启一个新的goroutine
    time.Sleep(1*time.Second)  // 大家可以把这个去掉看看会出现什么结果
    fmt.Println("World")
}
/*
Hello
World
*/

特点

终止情况

Go语言中没有任何显示方法可以从外部终止一个 goroutine 的执行。这儿说明一下Goroutine的终止情况。

  1. 当Goroutine运行错误,抛出异常,终止程序;
  2. 当Goroutine返回,可以通过别的方式进行操控4
  3. 当Main Goroutine结束,所有其他Goroutine强制终止。

之所以这么设计有很多原因。我目前理解地也不是很深刻,简单说一些我认为的原因:

对于杀死的协程占有的资源,需要进行释放与其他管理;
对于杀死的协程占有的锁,需要进行拆除;
禁止之后需要程序员更加注意考虑协程的开始与结束。

栈大小

线程的栈空间是固定分配的,虽然区别于不同的系统会有不同的大小,但是基本都是2MB。这个栈用于保存局部变量,用于在函数切换时使用。

对于goroutine这种轻量级的协程来说,一个大小固定的栈可能会导致资源浪费:比如一个协程里面只print了一个语句,那么栈基本没怎么用。当然,也有可能嵌套调用很深,那么可能也不够用。

所以go采用了动态扩张收缩的策略:初始化为2KB,最大可扩张到1GB。

没有id

每个线程都有一个id,这个在线程创建时就会返回,所以可以很方便的通过id操作某个线程。

但是在goroutine内没有这个概念,这个是go语言设计之初考虑的,防止被滥用,所以你不能在一个协程中杀死另外一个协程,编码时需要考虑到协程什么时候创建,什么时候释放。

GOMAXPROCS

GOMAXPROCS用于设置上下文个数,这个上下文用于协程间的切换,默认值是CPU的个数,也就是说这个个数是指定同时执行协程的内核线程数,即,用户线程(协程)与内核线程的数量对应关系是1:GOMAXPROCS的5(这儿说的1是一个虚指,若有多个协程同时开启,则对应是M:N,如下例子)。

for {  
    go fmt.Print(0)
    fmt.Print(1)
}
/*
$ GOMAXPROCS=1 go run example.go
11111111111111111100000000000000000000111111111...  
$ GOMAXPROCS=2 go run example.go
01010101010010101001110010101010010101010101010...  
*/

第一次执行语句指定只启动一个上下文,那么由于是2个协程映射到1个内核线程,那么1次只能跑一个协程,所以会跑一段时间再进行切换(由调度器进行判断什么时候切换,而不是内核)。第二次启动二个上下文,2个协程映射到2个内核线程,那么同一时间有2个干活的内核线程,所以能看到0和1交替打印,也就是说,此时真正实现了并发。

Channel

简介

如果说goroutine是Go并发的执行体,那么 Channel 就是他们之间的连接。

Channel是可以让一个goroutine发送特定的值到另外一个goroutine的通信机制。

go中的并发_第1张图片

使用

var ch chan int      // 声明一个传递int类型的channel
ch := make(chan int) // 使用内置函数make()定义一个channel

//=========

ch <- value          // 将一个数据value写入至channel,这会导致阻塞
                     // 直到有其他goroutine从这个channel中读取数据
value := <-ch        // 从channel中读取数据,如果channel之前没有写入数据
                     // 也会导致阻塞,直到channel中被写入数据为止

//=========

close(ch)            // 关闭channel

默认的channel是阻塞的。

四种通道类型

无缓冲通道

无缓冲通道上的发送操作将会被阻塞,直到另一个goroutine在对应的通道上执行接收操作,此时值才传送完成,两个goroutine都继续执行。

package main

import (
    "fmt"
    "time"
)
var done chan string
func Hello() {
    fmt.Println("Hello")
    time.Sleep(1*time.Second)
    done <- "World"
}
func main() {
    done = make(chan string)  // 创建一个channel
    go Hello()
    fmt.Println(<-done)
}
/*
Hello
World
*/

可以参考上文不用channel的代码,我们在main中使用了sleep来阻止main goroutine过早结束。而这儿就算main goroutine执行到了最后一行,因为信道阻塞的缘故,只有在Hello goroutine运行到往信道里输入一个东西之后,它才会继续工作。

管道

通道可以用来连接goroutine,这样一个的输出是另一个输入。这就叫做管道。

go中的并发_第2张图片

package main

import (
    "fmt"
    "time"
)
var echo chan string
var receive chan string

// 定义goroutine 1 
func Echo() {
    time.Sleep(1*time.Second)
    echo <- "Hello World"
}

// 定义goroutine 2
func Receive() {
    temp := <- echo // 阻塞等待echo的通道的返回
    receive <- temp
}


func main() {
    echo = make(chan string)
    receive = make(chan string)

    go Echo()
    go Receive()

    getStr := <-receive   // 接收goroutine 2的返回

    fmt.Println(getStr)
}

在这里不一定要去关闭channel,因为底层的垃圾回收机制会根据它是否可以访问来决定是否自动回收它。(这里不是根据channel是否关闭来决定的)

单向通道类型

当程序则够复杂的时候,为了代码可读性更高,拆分成一个一个的小函数是需要的。

此时go提供了单向通道的类型,来实现函数之间channel的传递。

package main

import (
    "fmt"
    "time"
)

// 定义goroutine 1
func Echo(out chan<- string) {   // 定义输出通道类型
    time.Sleep(1*time.Second)
    out <- "Hello World"
    close(out)
}

// 定义goroutine 2
func Receive(out chan<- string, in <-chan string) { // 定义输出通道类型和输入类型
    temp := <-in // 阻塞等待echo的通道的返回
    out <- temp
    close(out)
}


func main() {
    echo := make(chan string)
    receive := make(chan string)

    go Echo(echo)
    go Receive(receive, echo)

    getStr := <-receive   // 接收goroutine 2的返回

    fmt.Println(getStr)
}

缓冲管道

goroutine的通道默认是阻塞的,加一个缓冲区就可以缓解阻塞问题

ch := make(chan string, 3) // 创建了缓冲区为3的通道

//=========
len(ch)   // 长度计算
cap(ch)   // 容量计算

go中的并发_第3张图片

select

简介

在golang里头select的功能与epoll(nginx)/poll/select的功能类似,都是监听IO操作,当IO操作发生的时候,触发相应的动作。通过这类方式能够实现异步IO。

使用

  1. 如果有多个case都可以运行,select会随机公平地选出一个执行,其他不会执行
package main

import "fmt"

func main() {
    ch := make (chan int, 1)

    ch<-1
    select {
    case <-ch:
        fmt.Println("1")
    case <-ch:
        fmt.Println("2")
    }
}
/*
输出是随机2选1
*/
  1. case后面必须是channel操作,否则报错。
package main

import "fmt"

func main() {
    ch := make (chan int, 1)
    ch<-1
    select {
    case <-ch:
        fmt.Println("1")
    case 2:
        fmt.Println("2")
    }
}
/*
编译错误:
2 evaluated but not used
select case must be receive, send or assign recv
*/
  1. select中的default子句总是可运行的。所以没有default的select才会阻塞等待事件
package main

import "fmt"

func main() {
    ch := make (chan int, 1)
    select {
    case <-ch:
        fmt.Println("1")
    default:
        fmt.Println("2")
    }
}
/*
2
*/
  1. 没有运行的case,那么将会有阻塞事件发生报错(死锁)
package main

import "fmt"

func main() {
    ch := make (chan int, 1)
    // ch<-1   <= 注意这里备注了。
    select {
    case <-ch:
        fmt.Println("咖啡色的羊驼")
    }
}
/*
fatal error: all goroutines are asleep - deadlock!
*/

使用场景

timeout 机制(超时判断)

package main

import (
    "fmt"
    "time"
)

func main() {
    ch := make (chan int)
    select {
    case <-ch:
    case <-time.After(time.Second * 1): // 利用time来实现,After代表多少时间后执行输出东西
        fmt.Println("超时啦!")
    }
}

判断channel是否阻塞

package main

import (
    "fmt"
)

func main() {
    ch := make (chan int, 1)  // 注意这里给的容量是1
    ch <- 1
    select {
    case ch <- 2:
    default:
        fmt.Println("通道channel已经满啦,塞不下东西了!")
    }
}

退出机制

package main

import (
    "fmt"
    "time"
)

func main() {
    i := 0
    ch := make(chan string, 0)
    defer func() {
        close(ch)
    }()

    go func() {
        DONE: 
        for {
            time.Sleep(1*time.Second)
            fmt.Println(time.Now().Unix())
            i++

            select {
            case m := <-ch:
                println(m)
                break DONE // 跳出 select 和 for 循环
            default:
            }
        }
    }()

    time.Sleep(time.Second * 4)
    ch<-"stop"
}
/*
1532390471
1532390472
1532390473
stop
1532390474
*/

参考文献


  1. Linux中的异步I/O模型 ↩︎

  2. 进程和线程、协程的区别 ↩︎

  3. A Tour of Go: Goroutines ↩︎

  4. 深入golang之—goroutine并发控制与通信 ↩︎

  5. 操作系统概念(第四章) 线程 ↩︎

你可能感兴趣的:(Go语言)