zoukankan      html  css  js  c++  java
  • Go 语言入门(三)并发

    Go 语言入门(三)并发

    写在前面

    在学习 Go 语言之前,我自己是有一定的 Java 和 C++ 基础的,这篇文章主要是基于A tour of Go编写的,主要是希望记录一下自己的学习历程,加深自己的理解

    Go 程

    「Go 程goroutine」:由 Go 运行时管理的轻量级线程

    运行「Go 程」很简单,只要执行下面代码:

    go f(x, y, z)
    

    就会启动一个新的 Go 程并执行f(x, y, z)fxyz的运算发生在当前的 Go 程中,而f的执行发生在新的 Go 程中。

    「Go 程」在相同的地址空间中运行,因此在访问共享的内存时必须进行同步sync包提供了这种能力,不过在 Go 中并不经常用到,我们用得比较多的是信道

    信道

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

    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
    }
    

    带缓冲的信道

    信道是带缓冲的,也就是我们可以指定信道的缓冲区长度:仅当信道的缓冲区填满后,「发送方」向其发送数据时会阻塞。当缓冲区为空时,「接受方」会阻塞。

    使用make函数便能够在创建信道的同时指定缓冲区长度:

    // 创建一个缓冲区长度为 2 的信道
    c := make(chan int, 2)
    

    range 和 close

    「发送者」可通过close关闭一个信道来表示没有需要发送的值了。

    「接收者」可以通过为接收表达式分配第二个参数来测试信道是否被关闭:

    v, ok := <-ch
    

    和「映射」以及接口的「类型断言」相似,如果信道已经关闭,ok会被设为false

    作为「接受者」,我们可以使用for i := range c来不断从信道c接受信息,知道它被关闭。

    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)
    	}
    }
    

    注意: 信道与文件不同,通常情况下无需关闭它们。只有在必须告诉接收者不再有需要发送的值时才有必要关闭,例如终止一个 range 循环。

    select 语句

    「select 语句」:使一个 Go 程可以等待多个通信操作。

    select会阻塞到某个分支可以继续执行为止,这时就会执行该分支;如果多个分支都准备好时,会随机选择一个执行。

    下面的例子能够演示并说明一些 select 的使用场景:

    package main
    
    import "fmt"
    
    func fibonacci(c, quit chan int) {
    	x, y := 0, 1
    	for {
    		select {
            // 实际上这个 case 一直在不断执行直到 return
    		case c <- x:
                x, y = y, x+y
            // 这里时 quit 信道的接收方
            // 直到下面的 go 程中 for 循环执行完毕后,quit 才不为空,才能够执行这个 case
    		case <-quit:
    			fmt.Println("quit")
    			return
    		}
    	}
    }
    
    func main() {
    	// 新建两个信道
    	c := make(chan int)
    	quit := make(chan int)
    	// 启动一个 go 程
    	go func() {
    		for i := 0; i < 10; i++ {
                // 当 c 缓冲区为空时,接收方(也就是这里)会阻塞
    			fmt.Println(<-c)
    		}
    		quit <- 0
    	}()
    	fibonacci(c, quit)
    }
    

    上面的例子中,我们可以修改 go 程中 for 循环的循环次数,多试几次就可以明白select语句的执行情况了。

    上面我们只为select语句指定了两个 case,实际上我们还可以像switch语句一样为它设置默认值default,当 select 中的其它分支都没有准备好时,default 分支就会执行:

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

    互斥锁: sync.Mutex

    从上面可以看到,通过「信道」我们可以方便的在各个 Go 程之间进行通信。但有时,我们希望同一时间只有一个 Go 程能够访问某个共享的变量,这就是互斥(mutual exclusion),我们通常使用互斥锁(Mutex)这一数据结构来提供这种机制。

    Go 标准库中提供了sync.Mutex互斥锁类型及其两个方法:Lock()Unlock()来实现「互斥」。

    和 Java 中一样,我们在代码执行前调用Lock(),在代码执行结束后调用Unlock()来保证代码的互斥执行。参加下面代码的Inc()方法

    我们可以用defer语句来保证互斥锁一定会被解锁,参见下面的Value()方法:

    package main
    
    import (
    	"fmt"
    	"sync"
    	"time"
    )
    
    // SafeCounter 的并发使用是安全的。
    type SafeCounter struct {
    	v   map[string]int
    	mux sync.Mutex
    }
    
    // Inc 增加给定 key 的计数器的值。
    func (c *SafeCounter) Inc(key string) {
    	c.mux.Lock()
    	// Lock 之后同一时刻只有一个 goroutine 能访问 c.v
    	c.v[key]++
    	c.mux.Unlock()
    }
    
    // Value 返回给定 key 的计数器的当前值。
    func (c *SafeCounter) Value(key string) int {
    	c.mux.Lock()
    	// Lock 之后同一时刻只有一个 goroutine 能访问 c.v
    	defer c.mux.Unlock()
    	return c.v[key]
    }
    
    func main() {
    	c := SafeCounter{v: make(map[string]int)}
    	for i := 0; i < 1000; i++ {
    		go c.Inc("somekey")
    	}
    
    	time.Sleep(time.Second)
    	fmt.Println(c.Value("somekey"))
    }
    
  • 相关阅读:
    Java正则表达式入门
    StringBuffer ,String,StringBuilder的区别
    JAVA的StringBuffer类
    容器vector的使用总结 容器stack(栈)
    c++网络通信(与服务器通信聊天)和c#网络通信
    C#与SQLite数据库
    我的vim配置文件
    在world中批量调整图片的大小
    C#判断文件及文件夹是否存在并创建(C#判断文件夹存在)
    C# Thread类的应用
  • 原文地址:https://www.cnblogs.com/Bylight/p/11967197.html
Copyright © 2011-2022 走看看