文章目录

  • 并发编程
  • 1.概述
  • 1.1 并行和并发
  • 1.2 Go语言并发优势
  • 2. goroutine
  • 2.1 goroutine的定义
  • 2.2 goroutine的创建和运行
  • 2.2.1 mian goroutine
  • 2.2.2 其他goroutine
  • 2.3 runtime包
  • 2.3.1 Gosched
  • 3.2.3 Goexit
  • 2.3.3 GOMAXPROCS
  • 3 channel
  • 3.1 概述
  • 3.2 channel的创建
  • 3.3 channel中的数据的发送和接收
  • 3.4 无缓冲的channel
  • 3.4.1 概述
  • 3.4.2 无缓冲通道的两个goroutine交换数据示意
  • 3.4.3 无缓冲channel的创建
  • 3.4.4 内置函数close在channel中的应用
  • 3.4.5 用range迭代channel
  • 3.5 有缓冲的channel
  • 3.5.1 概述
  • 3.5.2 有缓冲通道的两个goroutine交换数据示意图
  • 3.5.3 有缓冲channel的创建
  • 3.6 单向的channel
  • 3.6.1
  • 3.6.2 单向channel变量的声明
  • 3.6.3 channel的转换
  • 4 select
  • 4.1 语法结构
  • 4.2 select语句执行顺序
  • 4.3 select语句中设置超时


并发编程

1.概述

1.1 并行和并发

类别

区别

并行

在多个处理器上同时执行多条指令

并发

在同一时刻只有一条指令在执行,但多个进程指令被快速轮换执行,

使得宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的。

1.2 Go语言并发优势

机制

并发优势

自动垃圾回收

自动管理内存

goroutine

创建一个goroutine代价极小,让并发编程更加轻盈;

通道channel让不同的goroutine直接同步安全地发送消息,让并发编程更加安全。

2. goroutine

2.1 goroutine的定义

Rob Pike说:”一个goroutine是一个与其他goroutines并发运行在同一地址空间的Go函数或方法。一个运行的程序由一个或更多个goroutine组成。它与线程、协程、进程等不同。它是一个goroutine。“
所以我们可以认为:goroutine就是一个函数或方法。

2.2 goroutine的创建和运行

  • a. goroutine 的创建和运行:

在函数或者方法调用语句之前添加关键字go,就可以创建一个goroutine。开发人员无需了解任何执行细节,调度器会自动将其安排到合适的系统线程上执行。

  • b.goroutine的运行:

在一个go程序里,我们将一个过程切分程几块,然后让每个goroutine各自负责一块工作。当一个 程序启动时,其主函数在一个单独的goroutine中运行,这个goroutine叫做main goroutine。新的goroutine会用go语句来 创建。

2.2.1 mian goroutine

在Go语言,主函数运行在main goroutine中,其他goroutine和main goroutine并发运行。如果main goroutine先执行完毕,那么其他的goroutine也会自动退出。

package main

import (
    "fmt"
    "time"
)

func hello() {
    //延迟1秒再执行下面的语句
    time.Sleep(1 * time.Second)
    fmt.Println("Hello world")
}

/*main goroutine先执行完,其他goroutine自动退出,不执行*/
func main() {
    go hello()
    fmt.Println("main function")
}

2.2.2 其他goroutine

如果要运行所有其他的goroutine,main goroutine必须处于运行状态,直到其它goroutine运行完毕。

package main

import (
    "fmt"
    "time"
)

func test1() {
    time.Sleep(time.Second) //延迟1秒
    fmt.Println("test1")
}
func test2() {
    fmt.Println("test2")
}

/* main goroutine延迟2秒,让其它goroutine先执行完,再执行main goroutine*/
func main() {
    go test1()
    go test2()
    time.Sleep(2 * time.Second)
    fmt.Println("main function")
}

2.3 runtime包

2.3.1 Gosched

runtime.Gosched()用于让出CPU时间片,让出当前goroutine的执行权,调度器安排其它等待的任务运行,并在下次某个时候从该位置恢复执行。

3.2.3 Goexit

调用runtime.Goexit(),会立即终止当前goroutine的执行,调度器确保所有已注册defer延迟调用被执行。

2.3.3 GOMAXPROCS

掉头runtime.GOMAXPROCS()用来设置可以并行计算的CPU核数的最大值,并返回之前的值。

  • runtime.Gosched()
package main

import (
    "fmt"
    "runtime"
)

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

func main() {
    go say("world")
    /*暂停当前main goroutine,执行其它goroutine,等其它goroutine执行完,再执行main goroutine*/
    runtime.Gosched() //注释掉这一行,会直接输出hello,子 goroutine还没执行main已经结束了
    fmt.Println("hello")
}
  • runtime.Goexit()
package main

import (
    "fmt"
    "runtime"
)

func main() {
    go func() {
        defer fmt.Println("A.defer")
        func() {
            defer fmt.Println("B.defer")
            runtime.Goexit()
            fmt.Println("B")
        }()
        fmt.Println("A")
    }()
    for {
    }
}
  • runtime. GOMAXPROCS()
package main

import (
    "fmt"
    "runtime"
)

func main() {
    //runtime.GOMAXPROCS(1)
    runtime.GOMAXPROCS(2)
    for {
        go fmt.Print(0)
        fmt.Print(1)
    }
}

分别传入1和2 ,在一个cpu时间片内处处的1和0的连续性有明显区别,前提是你的计算机是双核以上。

3 channel

3.1 概述

当一个资源需要在goroutine直接共享时,channel在goroutine之间架起一个管道,并提供了确保同步交换数据的机制。

3.2 channel的创建

make(chan Type) //等价于make(chan Type, 0)
make(chan Type, capacity)
  • a. 定义一个channel,需要定义发送到channel的值的类型;
  • b.当capacity=0时,channel是无缓冲阻塞读写的;当capacity>0时,channel是有缓冲、非阻塞的,直到写满capacity个元素才阻塞读写。

3.3 channel中的数据的发送和接收

功能

格式

将value发送到channel

channel <- value

接收channel中的值并丢弃

<- channel

从channel中接收value并赋值给x

x:= <-channel

从channel中接收value,并赋值给x;同时

检查通道是否已关闭或者是否为空(ok为false)

x,ok := <-channel

3.4 无缓冲的channel

3.4.1 概述

  • a. 无缓冲的通道(unbuffered channel)是指在接收前没有能力保存任何值的通道
  • b. 同时准备好发送goroutine和接收goroutine,才能完成发送和接收操作
  • c. 如果两个goroutine没有同时准备好,通道会导致先执行发送或者接收的goroutine阻塞等待。
  • d. 对通道进行发送和接收的交互行为是同步的,其中任意一个操作都无法离开另一个操作单独存在。

3.4.2 无缓冲通道的两个goroutine交换数据示意

go语言网络编程底层 go语言高级编程_数据

  • 1 两个goroutine都到达通道两端,但两个都没有开始执行发送数据或者接收数据。
  • 2 左侧的goroutine将它的手伸进通道,这模拟了向通道发送数据的行为。此时,这个goroutine会再通道中被锁住,直到交换完成。
  • 3 右侧的goroutine都将它的手放入通道,这模拟了从通道里接收数据.这个goroutine也一样会在通道中被锁住,直到交换完成。
  • 4.5 第四步和第五步进行数据交换。
  • 6 两个goroutine都将他们的手从通道里拿出来,这模拟了被锁住的两个goroutine得到释放。两个goroutine现在都可以去做别的事情了。

3.4.3 无缓冲channel的创建

无缓冲channel没有指定缓冲区容量,那么无缓冲的channel中的数据在发送和接收需要同步,其创建格式如下:

make(chan Type)//等价于make(chan Type, 0)
package main

import (
    "fmt"
)

func main() {
    c := make(chan int, 0) //无缓冲通道
    /*内置函数len返回未被读取的缓冲元素数量,cap返回缓冲区大小*/
    fmt.Printf("len(c)=%d, cap(c)=%d\n", len(c), cap(c))
    go func() {
        defer fmt.Println("子协程结束")
        for i := 0; i < 3; i++ {
            c <- i
            fmt.Printf("子协程正在运行[%d]:len(c)=%d,cap(c)=%d\n", i, len(c),   (c))
        }
    }()
    for i := 0; i < 3; i++ {
        num := <-c //从c中接收数据并赋值给num
        fmt.Println("num = ", num)
    }
    fmt.Println("main协程结束")
}

3.4.4 内置函数close在channel中的应用

  • a. 如果发送这知道没有更多的值发送到channel,那么让接收者也能及时知道没有多余的值可接收将是有用的。因为接收者可以停止不必要的接收等待,这可以通过内置的close函数来实现channel的关闭。
  • b. channel不像文件一样需要经常关闭,只有确实没有任何数据发送才关闭channel后,无法向 channel再发送数据。
package main

import "fmt"

func main() {
    c := make(chan int)
    go func() {
        for i := 0; i < 5; i++ {
            c <- i
        }
        /*把close(c)注释掉,程序会一直阻塞再 if data,ok:= <-c; ok 那一行*/
        close(c)
    }()

    for {
        /*ok为true说明channel没有关闭,为false说明通道已经关闭*/
        if data, ok := <-c; ok {
            fmt.Println(data)
        } else {
            break
        }
    }
    fmt.Println("Finished")
}

3.4.5 用range迭代channel

类似与range在数组切片和字典中的应用,range也可以迭代操作channel。

package main

import "fmt"

func main() {
    c := make(chan int)
    go func() {
        for i := 0; i < 5; i++ {
            c <- i
        }
        /*把close(c)注释掉,程序会一直阻塞再 if data,ok:= <-c; ok 那一行*/
        close(c)
    }()

    for data := range c {
        fmt.Println(data)
    }
    fmt.Println("Finished")
}

3.5 有缓冲的channel

3.5.1 概述

  • a. 有缓冲的通道(buffered channel)是一种在被接收前能存储一个或多个值的通道。这种类型的通道并不强制要求goroutine之间必须同时完成发送和接收,通道会阻塞发送和接收动作的条件也不同:只有在通道中没有接手值时,接收动作才会阻塞;只有在通道没有可用缓冲区容纳被发送的值时,发送动作才会阻塞。
  • b. 有缓冲通道和无缓冲通道之间最大的不同在于:无缓冲通道保证发送和接收的goroutine会在同一时间进行数据交换;有缓冲的通道没有这种保证。

3.5.2 有缓冲通道的两个goroutine交换数据示意图

go语言网络编程底层 go语言高级编程_go语言网络编程底层_02

  • 1 右侧的goroutine正在从通道中接收一个值
  • 2 右侧的这个goroutine独立完成了接收值的操作,而左侧的goroutine正在发送一个新的值到通道里
  • 3 左侧的goroutine向通道发送新值,右侧的goroutine正在从通道j接收另外一个值。这个步骤里的两个操作既不同步,也不会相互阻塞
  • 4 所有的发送和接收都完成,而通道里还有几个值,也有一些空间k而已存更多的值。

3.5.3 有缓冲channel的创建

如果给定了一个缓冲区容量,通常就是异步的。只要缓冲区有未使用空间用于发送数据,或还包含可以接收的数据,那么其通信就会无阻塞的进行。有缓冲区的channel创建格式如下:

make(chan Type, capacity)
package main

import (
    "fmt"
)

func main() {
    c := make(chan int, 3) //带缓冲的通道
    /*内置函数len返回未被读取的缓冲元素数量,cap返回缓冲区大小*/
    fmt.Printf("len(c)=%d, cap(c)=%d\n", len(c), cap(c))
    go func() {
        defer fmt.Println("子协程结束")
        for i := 0; i < 3; i++ {
            c <- i
            fmt.Printf("子协程正在运行[%d]:len(c)=%d,cap(c)=%d\n", i, len(c),   (c))
        }
    }()
    for i := 0; i < 3; i++ {
        num := <-c //从c中接收数据并赋值给num
        fmt.Println("num = ", num)
    }
    fmt.Println("main协程结束")
}

3.6 单向的channel

3.6.1

channel默认双向,即可以向channel里发送数据也可以从channel里接收数据。但是,要使channel作为参数传递且被单向使用,即只向该channel发送数据,或此channel只接收数据,那么需要对此channel指定方向。

3.6.2 单向channel变量的声明

类型

格式

单向写入channel

var ch1 chan<- int

单向读取channel

var ch2 <-chan int

注:

  • a. chan<- 表示把数据写入管道
  • b. <-chan 表示将数据从管道中读取

3.6.3 channel的转换

普通的channel可以隐式转换为单向的channel,只接收或只发送数据。但是单向的channel不能转换为普通的channel。

c := make(chan int, 3)
var send chan<- int = c //只能向管道发送数据,即写入
var recv <-chan int = c //只能从管道接收数据,即读取
package main

import "fmt"

//chan<-:只写
func counter(write chan<- int) {
    defer close(write)
    for i := 0; i < 5; i++ {
        write <- i //如果对方不读会阻塞
    }
}

//<-chan:只读
func printer(read <-chan int) {
    for num := range read {
        fmt.Println(num)
    }
}

func main() {
    c := make(chan int) //chan:默认双向——读写
    //将普通channel隐式转换为单向channel——数据写入
    go counter(c)
    /*将普通channel隐式转换为单向channel——数据从channel里读取*/
    printer(c)
    fmt.Println("done")
}

4 select

4.1 语法结构

  • a. 关键字select可以监听channel上的数据流动。
  • b. 用法与switch语句相似,但select语句中的每个case必须是一个IO(输入/输出)操作。
select {
    case <-chan1:
    /*如果chan1成功读取到数据,则进行该case处理语句*/
    case chan2 <- 1:
    /*如果chan2成功读取到数据,则进行该case处理语句*/
    default:
    /*如果上面都没有成功,则进入default处理流程*/
}

4.2 select语句执行顺序

在select语句中,Go语言按顺序从头职位评估每一个发送和接收的语句:

  • a. 如果其中任意一条语句可以继续执行(即没有被阻塞),那么就从那些可以执行的语句中任意选择一条来使用。
  • b. 如果没有任意一条语句可以执行(即所有的通道都被阻塞),那么有两种可能:

第一种,如果有default语句,那么就会执行default语句,同时程序执行会从select语句后的语句中恢复
第二种,如果没有default语句,那么select语句将被阻塞,直到至少有一个通信可以进行下去。

package main

import (
    "fmt"
    "time"
)

func server1(ch chan string) {
    time.Sleep(6 * time.Second)
    ch <- "from server1"
}
func server2(ch chan string) {
    time.Sleep(3 * time.Second)
    ch <- "from server2"
}
func main() {
    output1 := make(chan string)
    output2 := make(chan string)
    go server1(output1)
    go server2(output2)
    select {
    case s1 := <-output1:
        fmt.Println(s1)
    case s2 := <-output2:
        fmt.Println(s2)
    }
}
package main

import (
    "fmt"
    "time"
)

func process(ch chan string) {
    time.Sleep(3 * time.Second)
    ch <- "process successful"
}
func main() {
    ch := make(chan string)
    go process(ch)
    for {
        time.Sleep(time.Second)
        select {
        case v := <-ch:
            fmt.Println("received value: ", v)
            return
        default:
            fmt.Println("no value received")
        }
    }
}

4.3 select语句中设置超时

当goroutine阻塞时,为避免整个程序进入阻塞,可以在select语句设置超时,具体通过如下方式实现:

case <-time.After(5 * time.Second)://这个case延时5秒执行
package main

import (
    "fmt"
    "time"
)

func main() {
    c := make(chan int)
    o := make(chan bool)
    go func() {
        for {
            select {
            case v := <-c:
                fmt.Println(v)
            case <-time.After(5 * time.Second):
                fmt.Println("timeout")
                o <- true
            }
        }
    }()
    c <- 666
    <-o
}