第十篇:并发编程

2018-03-29  本文已影响68人  意一ineyee

Go语言区别于其它语言最核心的优势就是:Go对并发编程的大力支持。


目录

一、并发的一些基础知识

二、Go并发编程关键词之:协程goroutine和go关键字

三、Go并发编程关键词之:消息传递实现执行体之间的通信和channel作为消息通道


一、并发的一些基础知识

1、为什么需要并发编程?

2、有哪些方式可以实现并发编程(即并发编程的执行体有哪些)?

我们可以通过以下四种执行体来实现并发编程:

3、提到并发还不得不提执行体之间的通信:

执行体之间的通信,有两种方式:

前者是大多数语言采用的执行体间通信的方式,尽管Go也保留了传统的执行体通过共享资源来通信的方式,允许适度地使用,但是Go更推荐使用执行体通过消息传递来通信

二、Go并发编程关键词之:协程goroutine和go关键字

goroutine是Go语言中的协程,它是Go语言中实现并发编程的执行体。一个CPU一秒钟可以很轻松地调度上百万个协程而不会使系统资源枯竭,所以goroutine使得Go的并发性能不容置疑。

如果我们想开辟一个协程,让一个函数在一个协程中并发执行,那我们只需要在这个函数前面加上go关键字就可以了,当这个函数结束时,这条goroutine也就结束了,注意如果这个函数有返回值,那么这个返回值会被自动丢弃。非常简单吧。

如没有并发的情况下:

(清单2.1)

// goroutine
package main

import (
    "fmt"
)

func main() {

    for i := 0; i < 10; i++ {

        z := Add(i, i)
        fmt.Println(z)
    }
}

func Add(x, y int) (z int) {

    z = x + y
    return z
}


打印:
0
2
4
6
8
10
12
14
16
18
成功: 进程退出代码 0.

我们可以正常地拿到函数的返回值z,并打印。

当让Add()函数并发执行的时候:

(清单2.2)

func main() {

    for i := 0; i < 10; i++ {

        z := go Add(i, i) // 在函数前加上go关键字
        fmt.Println(z)
    }
}

这样写是会编译报错的,因为Go里并发执行的函数会自动丢弃函数的返回值。所以我们把代码改成这样:

(清单2.3)

// goroutine
package main

import (
    "fmt"
)

func main() {

    for i := 0; i < 10; i++ {

        go Add(i, i)
    }
}

func Add(x, y int) (z int) {

    z = x + y
    fmt.Println(z)
    return z
}

编译不报错了,这段代码的意思是:我们开辟了是个协程,每个协程执行一个Add()函数。

但是,运行,我们看不到控制台打印东西啊,什么情况?要回答这个问题我们需要了解下Go语言的执行机制了:

每个Go程序都是从main包的main()函数开始执行,等main()函数执行结束后,整个程序也就执行结束了,并不会等待每个goroutine都执行结束。

那么,上面的例子中,我们在主函数中开辟了十个协程,主函数就返回了,因此程序已经执行结束了,这是个协程还没来得及执行,所以不会有打印输出。

那么要想正常打印输出,我们就必须得等十个协程都执行完才能让主函数返回,那我们如何才能知道是个协程执行完了呢?这就引出了下一节的内容--Go执行体间通过消息传递来通信,消息传递的通道被称为channel。

不过在开始下一节前,我们可以先使用共享数据的执行体通信方式来解决一下这个问题,然后和下一节的消息传递的执行体通信方式来对比一下。代码如下:

(清单2.4)

// goroutine
package main

import (
    "fmt"
    "runtime"
    "sync"
)

var counter int = 0 // 创建一个变量来记录有几个协程执行完毕,十个协程会共享这个变量

func main() {

    lock := &sync.Mutex{} // 创建一把锁

    for i := 0; i < 10; i++ {

        go Add(i, i, lock)
    }

    for { // 给主函数一个死循环,保证主函数不会立即退出,而是由我们来掌控主函数的退出时间

        runtime.Gosched()
        if counter >= 10 {

            break
        }
    }
}

func Add(x, y int, lock *sync.Mutex) (z int) { // 该函数新增一个参数为互斥锁,来保证同一时间只有一个协程能访问锁之间的操作

    lock.Lock() // 加锁

    counter++

    z = x + y
    fmt.Println(z)

    lock.Unlock() // 解锁

    return z

}


打印:
2
18
4
6
8
10
12
14
16
0
成功: 进程退出代码 0.

可见,通过共享数据的通信方式我们解决了这个问题,但这才是这么简单的一个例子,那么当我们遇到高并发的场景时,就需要无数的共享数据、无数的锁,再加上业务逻辑,无疑是个噩梦。那并发编程作为Go语言的招牌,它会提倡怎么解决呢?下一节,Go执行体间通过消息传递来通信,消息传递的通道被称为channel。

三、Go并发编程关键词之:消息传递实现执行体之间的通信和channel作为消息通道

Go语言推荐使用消息传递的方式来实现执行体间的通信,通信通道为channel。当然消息传递只是一种方式,具体实现是靠channel来实现的,所以接下来我们会学习channel。

在具体讲解之前,我们先用消息传递和channel实现上面的例子,对比一下,方便我们对消息传递和channel有个直观的认识。

(清单3.1)

// goroutine
package main

import (
    "fmt"
)

func main() {

    chs := make([]chan int, 10) // 创建一个数组切片,用来存放是个channel

    for i := 0; i < 10; i++ {

        chs[i] = make(chan int) // 创建channel
        go Add(i, i, chs[i])    // 为每一个协程添加一个channel
    }

    for _, ch := range chs {

        <-ch // 这里是读取channel里的数据,同样的,一个channel在写入数据之前,读这个操作也是阻塞的,也就是说如果一个channel没有写入数据,那它的读取操作会一直挂在那阻塞,因此这就保证了channel必须是写入了数据,即该routine执行完了Add()
    }
}

func Add(x, y int, ch chan int) (z int) { // 该函数新增一个参数为channel,以便给每个协程一个通信的消息通道

    z = x + y
    fmt.Println(z)

    ch <- 1 // 每有一个协程执行Add()函数,就像这个协程对应的channel里写一个1,当然这个channel在读取前,写这个操作是是被阻塞的,也就是说如果一个channel没被读取,那它的写入操作会一直挂在那阻塞,注意要写在代码的后面,保证代码执行完了才写入

    return z
}


打印:
18
10
12
14
16
0
4
6
8
2
成功: 进程退出代码 0.

可见,这种方式消息传递要比共享数据的通信方式简洁多了,而且不必再去考虑锁的问题,因此实现并发编程也更安全。

1、channel的定义

channel是类型相关的,也就是说channel只能传递某一种类型的值,所以我们在声明channel的时候,就指定好它可以传递哪种类型的值。如:

声明一个可以传递int类型的channel

var ch chan int

我们其实可以把chan int看做是一个channel的类型,即整型channel。

又如,我们创建了一个字典,其value的类型为bool型channel

var dict map[string] chan bool
2、channel的创建与赋值
var ch chan int     // 声明channel
ch = make(chan int) // 创建channel并赋值

ch1 := make(chan int)
3、channel的操作
ch <- value

这个就是channel的写入操作。一个channel在读取前,写这个操作是是被阻塞的,也就是说如果一个channel没被读取,那它的写入操作会一直挂在那阻塞。

value <- ch

这个就是channel的读取操作。同样的,一个channel在写入数据之前,读这个操作也是阻塞的,也就是说如果一个channel没有写入数据,那它的读取操作会一直挂在那阻塞,因此这就保证了channel必须是写入了数据。

close(ch) // 关闭channel

_, ok := <-ch // 判断channel是否关闭
4、带缓冲的channel

上面的例子中,我们演示的是不带缓冲的channel,即只能写入单个数据,但有的场景是需要持续输入大量数据的,因此就需要用到带缓冲的channel。如:

ch := make(chan int, 1024)

上面的代码就创建了一个整型channel,这个整型channel可以写入1024个字节大小的整型数据。当然这种带缓冲的channel,就是连续不断地写入数据的,所以在写入数据写完之前是不会被阻塞的,整体的写入和读取的阻塞关系还是和单个数据的是一样的,只不过这个数据长一点而已嘛。

Go的并发编程还有很多别的知识,如果需要,逐步深入。

上一篇 下一篇

猜你喜欢

热点阅读