2019-05-23 Go语言学习四 并发

一、Go程

Go 程(goroutine)是由 Go 运行时管理的轻量级线程。

go f(x, y, z)

会启动一个新的 Go 程并执行

f(x, y, z)

f, x, y 和 z 的求值发生在当前的 Go 程中,而 f 的执行发生在新的 Go 程中。

Go 程在相同的地址空间中运行,因此在访问共享的内存时必须进行同步。sync 包提供了这种能力,不过在 Go 中并不经常用到,因为还有其它的办法(见下一页)。

package main

import (
    "fmt"
    "time"
)

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

func main() {
    go say("world")
    say("hello")
}

----------------------------------------------------------------------------
//运行结果:
hello
world
hello
world
hello
world
hello
world
hello

二、信道

(1)信道

信道是带有类型的管道,你可以通过它用信道操作符 <- 来发送或者接收值。

ch <- v    // 将 v 发送至信道 ch。
v := <-ch  // 从 ch 接收值并赋予 v。

(“箭头”就是数据流的方向。)
和映射与切片一样,信道在使用前必须创建:

ch := make(chan int)

默认情况下,发送和接收操作在另一端准备好之前都会阻塞。这使得 Go 程可以在没有显式的锁或竞态变量的情况下进行同步。
以下示例对切片中的数进行求和,将任务分配给两个 Go 程。一旦两个 Go 程完成了它们的计算,它就能算出最终的结果。

package main

import "fmt"

func sum(s []int, c chan int) {
    sum := 0
    for _, v := range s {
        sum += v
    }
    c <- sum // 将和送入 c
}

func main() {
    s := []int{7, 2, 8, -9, 4, 0}

    c := make(chan int)
    go sum(s[:len(s)/2], c)
    go sum(s[len(s)/2:], c)
    x, y := <-c, <-c // 从 c 中接收

    fmt.Println(x, y, x+y)
}

//运行结果:-5 17 12

btw,Go程序并不会去保证这些goroutine会以怎样的顺序运行
所以哪个goroutine先执行完,哪个goroutine后执行完往往是不可预知的,除非我们使用了某种Go语言提供的方式进行人为干预。

(2)带缓冲的信道

信道可以是 带缓冲的。将缓冲长度作为第二个参数提供给 make 来初始化一个带缓冲的信道:

ch := make(chan int, 100)

仅当信道的缓冲区填满后,向其发送数据时才会阻塞。当缓冲区为空时,接受方会阻塞。

package main

import "fmt"

func main() {
    ch := make(chan int, 2)
    ch <- 1
    ch <- 2
    fmt.Println(<-ch)
    fmt.Println(<-ch)
}

//运行结果:1 2

三、range和close

发送者可通过 close 关闭一个信道来表示没有需要发送的值了。接收者可以通过为接收表达式分配第二个参数来测试信道是否被关闭:若没有值可以接收且信道已被关闭,那么在执行完

v, ok := <-ch

此时 ok 会被设置为 false。
循环 for i := range c 会不断从信道接收值,直到它被关闭。

注意: 只有发送者才能关闭信道,而接收者不能。向一个已经关闭的信道发送数据会引发程序恐慌(panic)。
还要注意: 信道与文件不同,通常情况下无需关闭它们。只有在必须告诉接收者不再有需要发送的值时才有必要关闭,例如终止一个 range 循环。

package main

import (
    "fmt"
)

func fibonacci(n int, c chan int) {
    x, y := 0, 1
    for i := 0; i < n; i++ {
        c <- x
        x, y = y, x+y
    }
    close(c)
}

func main() {
    c := make(chan int, 10)
    go fibonacci(cap(c), c)
    for i := range c {
        fmt.Println(i)
    }
}

// 运行结果:
0
1
1
2
3
5
8
13
21
34

四、select语句

select 语句使一个 Go 程可以等待多个通信操作。
select 会阻塞到某个分支可以继续执行为止,这时就会执行该分支。当多个分支都准备好时会随机选择一个执行。

package main

import "fmt"

func fibonacci(c, quit chan int) {
    x, y := 0, 1
    for {
        select {
        case c <- x:
            x, y = y, x+y
        case <-quit:
            fmt.Println("quit")
            return
        }
    }
}

func main() {
    c := make(chan int)
    quit := make(chan int)
    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println(<-c)
        }
        quit <- 0
    }()
    fibonacci(c, quit)
}

//运行结果:
0
1
1
2
3
5
8
13
21
34
quit

五、默认选择

当 select 中的其它分支都没有准备好时,default 分支就会执行。
为了在尝试发送或者接收时不发生阻塞,可使用 default 分支:

select {
case i := <-c:
    // 使用 i
default:
    // 从 c 中接收会阻塞时执行
}

举个例子:

package main

import (
    "fmt"
    "time"
)

func main() {
    tick := time.Tick(100 * time.Millisecond)
    boom := time.After(500 * time.Millisecond)
    for {
        select {
        case <-tick:
            fmt.Println("tick.")
        case <-boom:
            fmt.Println("BOOM!")
            return
        default:
            fmt.Println("    .")
            time.Sleep(50 * time.Millisecond)
        }
    }
}

// 运行结果: 
    .
    .
tick.
    .
    .
tick.
    .
    .
tick.
    .
    .
tick.
    .
    .
BOOM!

六、练习:等价二叉树

不同二叉树的叶节点上可以保存相同的值序列。例如,以下两个二叉树都保存了序列 1,1,2,3,5,8,13

图片.png

在大多数语言中,检查两个二叉树是否保存了相同序列的函数都相当复杂。 我们将使用 Go 的并发和信道来编写一个简单的解法。
本例使用了 tree 包,它定义了类型:

type Tree struct {
    Left  *Tree
    Value int
    Right *Tree
}
  1. 实现 Walk 函数。
  2. 测试 Walk 函数。
    函数 tree.New(k) 用于构造一个随机结构的已排序二叉查找树,它保存了值 k, 2k, 3k, ..., 10k。
    创建一个新的信道 ch 并且对其进行步进:
go Walk(tree.New(1), ch)

然后从信道中读取并打印 10 个值。应当是数字 1, 2, 3, ..., 10。

  1. 用 Walk 实现 Same 函数来检测 t1 和 t2 是否存储了相同的值。
  2. 测试 Same 函数。
    Same(tree.New(1), tree.New(1)) 应当返回 true,而 Same(tree.New(1), tree.New(2)) 应当返回 false。
    Tree 的文档可在这里找到
package main

import (
    "fmt"

    "golang.org/x/tour/tree"
)

// Walk 步进 tree t 将所有的值从 tree 发送到 channel ch。
func Walk(t *tree.Tree, ch chan int) {
    if t.Left != nil {
        Walk(t.Left, ch)
    }
    ch <- t.Value
    if t.Right != nil {
        Walk(t.Right, ch)
    }
    return
    //close(ch)
}

// Same 检测树 t1 和 t2 是否含有相同的值。
func Same(t1, t2 *tree.Tree) bool {
    ch1, ch2 := make(chan int), make(chan int)
    go Walk(t1, ch1)
    go Walk(t2, ch2)
    if <-ch1 == <-ch2 {
        return true
    } else {
        return false
    }
}

func main() {
    ch := make(chan int)
    t1 := tree.New(1)
    t2 := tree.New(2)

    //从信道中打印10个值
    go Walk(t1, ch)
    for i := 0; i < 10; i++ {
        fmt.Println(<-ch)
    }

    //对tree1和tree2进行比较
    fmt.Println("tree 1 == tree 1:", Same(t1, t1))
    fmt.Println("tree 1 == tree 2:", Same(t1, t2))
}

-------------------------------------------------------------------------------------
// 运行结果:
tree 1 == tree 1: true
tree 1 == tree 2: false

七、sync.Mutex

八、练习:Web爬虫

习题讲解:

插曲:

由于等价二叉树程序代码的实现 需要用到"golang.org/x/tour/tree"

这个包,而golang的官网需要翻墙才能进去。故后来采用的是在github找到相关来源的包,然后在linux环境下进行安装下载。(方法一: 这是稍微老一点的方法,新版本添加了mod的功能,可以不用设置GOPATH直接安装,这将在方法二中记录)
方法一:

使用命令为:

go get -x -v github.com/golang/sys

//输入github账号用户名和密码

  • btw,命令 go get 可以根据要求和实际情况从互联网上下载或更新指定的代码包及其依赖包,并对它们进行编译和安装。
    在上面这个示例中,我们从代码托管站点Github上下载了一个项目(或称代码包),并安装到了环境变量GOPATH中包含的第一个工作区中。
    使用 go get 安装命令,系统会将下载好的安装包自动放到环境变量GOPATH值中的第一个目录路径。
    实际上,go get命令所做的动作也被叫做代码包远程导入,而传递给该命令的作为代码包导入路径的那个参数又被叫做代码包远程导入路径。
    more about go get

方法二:
明天学习内容:熟悉go的基本命令和mod功能 以及第二种安装方法 以及go语言小结
btw,CRT中文乱码问题还没解决(编码已经改至 UTF-8,在vim编辑中扔有乱码现象)
可能的解决方法一

go的标准命令详解
build install run install get mod

[Go语言之讲解GOROOT、GOPATH、GOBIN

golang包管理解决之道——go modules初探

再探go modules:使用与细节

你可能感兴趣的:(2019-05-23 Go语言学习四 并发)