Go语言学习笔记1:并发

前言:
第一步实现一个功能,第二步理解内部实现方式,第三步与你已有的知识融合,第四步创造。

一 并发

并发的几种场景:
1 我们的应用需要灵敏响应用户界面,并且需要执行大量运算。
2 Web服务器面对大量用户请求时,需要更多的“Web 服务器工作单元”来分别响应用户。
3 分布式环境中,相同的工作单元在不同的计算机上处理着被分片的数据。
4 为了使多核CPU的硬件能力得到充分的发挥
并发的几种实现:
多进程
多线程
基于回调的非阻塞/异步IO
协程

二 协程(gorutine)

1 使用

package main

import "fmt"

func Add(x, y int) {
    z := x + y
    fmt.Print(z)

}

func main() {

    go Add(5, 2)

}

使用方式非常简单,与调用普通函数相比,就在前面多加了一个go.

2 并发中的通信
工程中有两种常见的并发模型:共享数据和消息
共享数据:
在多个并发单元分别保持对同一个数据的引用实现对数据的共享,被共享的数据可能有多种形式,比如内存数据块、磁盘文件、网络数据等,在实际工作中用的最多的是共享内存。
消息机制:
每个并发单元是自包含的独立的个体,并且都有自己的变量,但在不同并发单元间这些变量不共享。
每个并发单元的输入和输出只有一种,那就是消息。
这种方式类似于进程的概念,每个进程不会被其它进程打扰,只需要做好自己的工作,不同进程靠消息来通信。
1.1共享数据:


package main

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

var money int = 0

func Count(lock *sync.Mutex) {
    lock.Lock()
    money++
    fmt.Print(money)
    lock.Unlock()
}

func main() {
    lock := &sync.Mutex{}

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

        go Count(lock)
    }

    for {
        lock.Lock()

        c := money
        lock.Unlock()

        //这个函数的作用是让当前goroutine让出CPU,好让其它的goroutine获得执行的机会。同时,当前的goroutine也会在未来的某个时间点继续运行。
        runtime.Gosched()

        if c <= 10 {
            break
        }
    }
}

我们在10个gorountine中共享了变量money,每个money执行完成后加1,其中我们还引入锁,每次操作先将锁锁住,操作完成后,再将锁打开。但是其中有一个问题,一个简单的功能却如此复杂,接下来我们来看另一种通信模型,即以消息机制而非共享内存作为通信方式。
1.2消息机制
channel
channel是go语言在语言级别提供goroutin间通信的方式,可以使用channel在两个或多个goroutine之间传递消息,我们可以将channel理解为一种类型安全的管道。
下面我们来看看用channel重写上面的例子

package main

import "fmt"

func Bank2(ch chan int) {
    fmt.Print("Counting")
    ch <- 1
}
func main() {
    chs := make([]chan int, 10)
    for i := 0; i < 10; i++ {
        chs[i] = make(chan int)
        go Bank2(chs[i])
    }

    for _, ch := range (chs) {
        <-ch
    }
}

这个例子中我们定义了以个包含10个channel的数组(名为chs)
并把数组中的每个channel分配给10个不同的grouting。
每个goroutine的add()函数完成后,我们通过ch<-1语句从10个channel中依次读取数据。
在对应的channel写入数据前,这个操作也是阻塞的,这样我们就用channel实现了类似锁的功能,进而保证了所有goroutine完成后主函数才返回。

2.2.1channel的基本语法
定义一个传递类型为int的channel

var cha chan int

声明一个map元素是bool型的channel

var m map[string] chan bool

与定义一般变量不同的地方仅仅是在类型之前加了chan关键字

用make定义一个channel

cha:=make(chan int)

channer数据的写入:

cha<-value

channer数据的读取

value:=<-ch

注意:
向channel写入数据会导致程序阻塞,直到有其他goroutine从这个channel中读取数据
如果channel之前没有写入数据,那么从chanel中读取数据也会导致程序阻塞,直到channel中被写入数据为止。

2.2.2 select
通过调用select()函数来监控一系列的文件句柄,一旦其中一个文件句柄发生了IO动作,该select()调用就会被返回。后来该机制也被用于实现高并发的服务器程序,Go语言在语言级别支持select关键字,用于处理异步io问题。
我们我来看下它的简单使用:

package main

var chan0 chan int
var chan1 chan int
var chan2 chan int

func test() {

    select {
    case <-chan1:
        //成功读到数据,则进行case处理语句
    case chan2 <- 1:
        //如果成功向chan2写入数据,则进行case处理语句
    default:
        //如果上面的都没有成功,则进入default处理流程
    }
}

我们来看一个select的小实例:

func main() {
    ch := make(chan int, 10)
    for {
        select {
        case ch <- 0:
        case ch <- 1:
        }
        i := <-ch
        fmt.Print("Value received:", i)
    }

}

代码很简单,随机向ch中写入一个0或者1的过程,当然这是一个死循环。
2.2.3 缓冲机制
我们来看一下怎么给channel带上缓冲

ch:=make(chan int,1024)

上面这个例子就创建了一个大小为1024的int类型channel,即使没有读取方,写入方也可以一直往channel里写入,在缓冲区被填写完之前都不会阻塞。

向channel中写入数据

func main() {
    ch := make(chan int, 1024)
    for i := 0; i < 1024; i++ {
        ch <- i
    }
}

普通读取数据

    fmt.Println(<-ch)

循环读取数据

    for i := range ch {
        fmt.Print("Received:", i)
    }

    原文作者:陈兴强
    原文地址: https://www.jianshu.com/p/73a2a475e6c1
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞