go【第九篇】并发编程

Go让并发更简单

并行和并发

并行:指在同一时刻,有多条指令在多个处理器上同时执行。

并发指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。

goroutine

goroutine简介

goroutine是Go并行设计的核心。goroutine说到底其实就是协程,但是它比线程更小,十几个goroutine可能体现在底层就是五六个线程,Go语言内部帮你实现了这些goroutine之间的内存共享。
执行goroutine只需极少的栈内存(大概是4~5KB),当然会根据相应的数据伸缩。也正因为如此,可同时运行成千上万个并发任务。goroutine比thread更易用、更高效、更轻便。

在java/c++中我们要实现并发编程的时候,我们通常需要自己维护一个线程池,并且需要自己去包装一个又一个的任务和然后自己去调度线程执行任务并维护上下文切换,这一切通常会耗费程序员大量的心智。能不能有一种机制,程序员只需要定义很多个任务,让系统去帮助我们把这些任务分配到CPU上实现并发执行呢? Go语言中的goroutine就是这样一种机制,goroutine 的概念类似于线程,但 goroutine 由 Go 程序运行时的调度和管理。Go 程序会智能地将 goroutine 中的任务合理地分配给每个 CPU。Go语言之所以被称为现代化的编程语言,就是因为它在语言层面已经内置了调度和上下文切换的机制。

在Go语言编程中你不需要去自己写进程、线程、协程,你的技能包里只有一个技能–goroutinue,当你需要让某个任务并发执行的时候,你只需要起一个goroutinue就可以了,就是这么简单粗暴。

goroutine与线程

可增长的栈

OS线程是由OS内核来调度的,goroutine则是由Go运行时(runtime)自己的调度器调度的,这个调度器使用一个称为m:n调度的技术(复用/调度m个goroutine到n个OS线程)。goroutine的调度不需要切换内核语境,所以调用一个goroutine比调度一个线程成本低很多。

goroutine调度

OS线程是由OS内核来调度的,goroutine则是由Go运行时(runtime)自己的调度器调度的,这个调度器使用一个称为m:n调度的技术(复用/调度m个goroutine到n个OS线程)。goroutine的调度不需要切换内核语境,所以调用一个goroutine比调度一个线程成本低很多。

创建goroutine

goroutine理解为轻量级线程,互不影响

package main

import (
    "fmt"
    "time"
)

func newTask() {
    for {
        fmt.Println("this is a newTask")
        time.Sleep(time.Second) //延时1s
    }
}

func main() {

    go newTask() //新建一个协程, 新建一个任务

    for {
        fmt.Println("this is a main goroutine")
        time.Sleep(time.Second *5) //延时1s
    }
}
eg

启动goroutine使用关键字

sync.WaitGroup
Add(i):计数器+i
Done():计数器-1,最好用defer注册
Wait():等待

主goroutine退出后,其它的工作goroutine也会自动退出

package main

import (
    "fmt"
    "time"
)

//主协程退出了,其它子协程也要跟着退出
func main() {

    go func() {
        i := 0
        for {
            i++
            fmt.Println("子协程 i = ", i)
            time.Sleep(time.Second)
        }

    }() //别忘了()

    i := 0
    for {
        i++
        fmt.Println("main i = ", i)
        time.Sleep(time.Second)

        if i == 2 {
            break
        }
    }

}
eg1
package main

import (
    "fmt"
    "time"
)

//主协程退出了,其它子协程也要跟着退出
func main() {
    go func() {
        i := 0
        for {
            i++
            fmt.Println("子协程 i = ", i)
            time.Sleep(time.Second)
        }

    }() //别忘了()

}


// 没有输出
eg2

等待所有子goroutine

package main

import (
    "fmt"
    "sync"
)

// 启动goroutine
// 利用sync.WaitGroup 实现优雅的等待,拒绝time.Sleep SB等待
var wg sync.WaitGroup // 是一个结构体,它里面有一个计数器

func hello(i int) {
    defer wg.Done() // 计数器-1
    fmt.Println("Hello 沙河!", i)
    if i == 8 { panic("报错啦") }
}

func main() {

    defer fmt.Println("哈哈哈")
    wg.Add(10) // 计数器+10
    for i := 0; i < 10; i++ {
        go hello(i) // 1. 创建一个goroutine 2. 在新的goroutine中执行hello函数

    }
    fmt.Println("Hello main func.")
    // time.Sleep(time.Second)
    // 等hello执行完(执行hello函数的那个goroutine执行完)
    wg.Wait() // 阻塞,一直等待所有的goroutine结束
    fmt.Println("main函数结束")
}
sync.WaitGroup

Gosched

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

package main

import (
    "fmt"
    "runtime"
)

func main() {

    go func() {
        for i := 0; i < 5; i++ {
            fmt.Println("go")

        }

    }()

    for i := 0; i < 2; i++ {
        //让出时间片,先让别的协议执行,它执行完,再回来执行此协程
        runtime.Gosched()
        fmt.Println("hello")
    }
}

/*
go
go
go
go
go
hello
hello
*/
eg

Goexit

package main
 
import (
    "fmt"
    "runtime"
)
 
func test() {
    defer fmt.Println("ccccccccccccc")
 
    //return //终止此函数
    runtime.Goexit() //终止所在的协程
 
    fmt.Println("dddddddddddddddddddddd")
}
 
func main() {
 
    //创建新建的协程
    go func() {
        fmt.Println("aaaaaaaaaaaaaaaaaa")
 
        //调用了别的函数
        test()
 
        fmt.Println("bbbbbbbbbbbbbbbbbbb")
    }() //别忘了()
 
    //特地写一个死循环,目的不让主协程结束
    for {
    }
}

/*
aaaaaaaaaaaaaaaaaa
ccccccccccccc
*/
eg

GOMAXPROCS

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

Go运行时的调度器使用GOMAXPROCS参数来确定需要使用多少个OS线程来同时执行Go代码。默认值是机器上的CPU核心数。例如在一个8核心的机器上,调度器会把Go代码同时调度到8个OS线程上(GOMAXPROCS是m:n调度中的n)。

Go语言中可以通过runtime.GOMAXPROCS()函数设置当前程序并发时占用的CPU逻辑核心数。

Go1.5版本之前,默认使用的是单核心执行。Go1.5版本之后,默认使用全部的CPU逻辑核心数。

package main

import (
    "fmt"
    "runtime"
)

func main() {
    //n := runtime.GOMAXPROCS(1) //指定以1核运算
    n := runtime.GOMAXPROCS(8) //指定以4核运算
    fmt.Println("n = ", n)

    for {
        go fmt.Print(1)

        fmt.Print(0)
    }
}
eg

channel

单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。

虽然可以使用共享内存进行数据交换,但是共享内存在不同的goroutine中容易发生竞态问题。为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。

go语言的并发模型是CSP,提倡通过通信共享内存而不是通过共享内存而实现通信。

如果说goroutine是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。

Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。

channel创建和操作

1.var 变量 chan 元素类型
2.make(chan 元素类型, [缓冲大小])

 

发送
	ch <- 10 // 把10发送到ch中

接收	
	x := <- ch // 从ch中接收值并赋值给变量x
	<-ch       // 从ch中接收值,忽略结果

关闭
	close(ch)  //我们通过调用内置的close函数来关闭通道

关于关闭通道需要注意的事情是,只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭通道。通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。

关闭后的通道有以下特点:
  对一个关闭的通道再发送值就会导致panic。
  对一个关闭的通道进行接收会一直获取值直到通道为空。
  对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
  关闭一个已经关闭的通道会导致panic。

 

package main

import "fmt"

// channel

func main() {
    // 定义一个ch1变量
    // 是一个channel类型
    // 这个channel内部传递的数据是int类型
    var ch1 chan int
    var ch2 chan string
    // channel是引用类型
    fmt.Println("ch1:", ch1)
    fmt.Println("ch2:", ch2)
    // make函数初始化(分配内存):slice map channel
    ch3 := make(chan int, 10)
    // 通道的操作:发送、 接收、关闭
    // 发送和接收都用一个符号: <-
    ch3 <- 10 // 把10发送到ch3中
    // ch3 <- 20
    // <-ch3        // 从ch3中接收值,直接丢弃
    ret := <-ch3 // 从ch3中接收值,保存到变量ret中
    fmt.Println(ret)
    ch3 <- 9
    ch3 <- 8
    ch3 <- 7
    // 关闭
    close(ch3)
    // 1. 关闭的通道再接收,能取到对应类型的零值
    ret2 := <-ch3
    fmt.Println(ret2)
    // 2. 往关闭的通道中发送值 会引发panic
    // ch3 <- 20
    // 3. 关闭一个已经关闭的通道会引发panic
    // close(ch3)
}
通道的创建和操作
package main

import (
    "fmt"
)

func main() {
    ch := make(chan int) //创建一个无缓存channel

    //新建一个goroutine
    go func() {
        for i := 0; i < 5; i++ {
            ch <- i //往通道写数据
        }
        //不需要再写数据时,关闭channel
        close(ch)
        //ch <- 666 //关闭channel后无法再发送数据

    }() //别忘了()

    for {
        //如果ok为true,说明管道没有关闭
        if num, ok := <-ch; ok == true {
            fmt.Println("num = ", num)
        } else { //管道关闭
            break
        }
    }

}

/*
num =  0
num =  1
num =  2
num =  3
num =  4
*/
关闭channel

channel实现同步

通过通信来实现同步和共享数据

package main

import (
    "fmt"
    "time"
)

//全局变量,创建一个channel
var ch = make(chan int)

//定义一个打印机,参数为字符串,按每个字符打印
//打印机属于公共资源
func Printer(str string) {
    for _, data := range str {
        fmt.Printf("%c", data)
        time.Sleep(time.Second)
    }
    fmt.Printf("\n")
}

//person1执行完后,才能到person2执行
func person1() {
    Printer("hello")
    ch <- 666 //给管道写数据,发送
}

func person2() {
    <-ch //从管道取数据,接收,如果通道没有数据他就会阻塞
    Printer("world")
}

func main() {
    //新建2个协程,代表2个人,2个人同时使用打印机
    go person1()
    go person2()

    //特地不让主协程结束,死循环
    for {

    }
}
eg
package main

import (
    "fmt"
    "math/rand"
    "sync"
)

// 生产者消费者模型
// 使用goroutine和channel实现一个简易的生产者消费者模型

// 生产者:产生随机数  math/rand

// 消费者:计算每个随机数的每个位的数字的和     14134134123 = ?

// 1个生产者 20个消费者

var itemChan chan *item
var resultChan chan *result
var wg sync.WaitGroup

type item struct {
    id  int64
    num int64
}

type result struct {
    item *item
    sum  int64
}

// 生产者
func producer(ch chan *item) {
    // 1. 生成随机数
    var id int64
    for i := 0; i < 10000; i++ {
        id++
        number := rand.Int63() // int64正整数
        tmp := &item{
            id:  id,
            num: number,
        }
        // 2. 把随机数发送到通道中
        ch <- tmp
    }
    close(ch)
}

// 计算一个数字每个位的和
func calc(num int64) int64 {
    // 123%10=12...3  sum = 0 + 3
    // 12%10=1...2
    // 1%10=0...1
    var sum int64 // 0
    for num > 0 {
        sum = sum + num%10 // sum = 5 + 1
        num = num / 10     // num = 0
    }
    return sum
}

// 消费者
func consumer(ch chan *item, resultChan chan *result) {
    defer wg.Done()
    for tmp := range ch {
        // (*tmp).num // item.num
        sum := calc(tmp.num)
        // 构造result结构体
        retObj := &result{
            item: tmp,
            sum:  sum,
        }
        resultChan <- retObj
    } // 结构体指针 *item
}

func startWorker(n int, ch chan *item, resultChan chan *result) {
    for i := 0; i < n; i++ {
        go consumer(ch, resultChan)
    }
}

// 打印结果
func printResult(resultChan chan *result) {
    for ret := range resultChan {
        fmt.Printf("id:%v, num:%v, sum:%v\n", ret.item.id, ret.item.num, ret.sum)
        // time.Sleep(time.Second)
    }
}

func main() {
    itemChan = make(chan *item, 10000)
    resultChan = make(chan *result, 10000)
    go producer(itemChan)
    wg.Add(20)
    startWorker(20, itemChan, resultChan)

    // // 打印结果
    wg.Wait() // 等到所有的生产result的goroutine都结束 再打印
    close(resultChan)
    printResult(resultChan)

    // 给rand加随机数种子实现每一次执行都能产生真正的随机数
    // rand.Seed(time.Now().UnixNano())
    // ret1 := rand.Int63() // int64正整数
    // fmt.Println(ret1)
    // ret2 := rand.Intn(101) // [1, 101)
    // fmt.Println(ret2)

}
使用goroutine和channel实现一个简易的生产者消费者模型

channel实现同步和数据交互

package main

import (
    "fmt"
    "time"
)

func main() {

    //创建channel
    ch := make(chan string)

    defer fmt.Println("主协程也结束")

    go func() {
        defer fmt.Println("子协程调用完毕")

        for i := 0; i < 2; i++ {
            fmt.Println("子协程 i = ", i)
            time.Sleep(time.Second)
        }

        ch <- "我是子协程,要工作完毕"

    }()

    str := <-ch //没有数据前,阻塞
    fmt.Println("str = ", str)
}
eg

无缓冲的channel(默认)

无缓冲的通道(unbuffered channel)是指在接收前没有能力保存任何值的通道。

这种类型的通道要求发送 goroutine 和接收 goroutine 同时准备好,才能完成发送和接收操作。如果两个goroutine没有同时准备好,通道会导致先执行发送或接收操作的 goroutine 阻塞等待。

这种对通道进行发送和接收的交互行为本身就是同步的。其中任意一个操作都无法离开另一个操作单独存在。

 

package main

import (
    "fmt"
    "time"
)

func main() {
    //创建一个无缓存的channel
    ch := make(chan int, 0)

    //len(ch)缓冲区剩余数据个数, cap(ch)缓冲区大小
    fmt.Printf("len(ch) = %d, cap(ch)= %d\n", len(ch), cap(ch))

    //新建协程
    go func() {
        for i := 0; i < 3; i++ {
            fmt.Printf("子协程:i = %d\n", i)
            ch <- i //往chan写内容
        }
    }()

    //延时
    time.Sleep(2 * time.Second)

    for i := 0; i < 3; i++ {
        num := <-ch //读管道中内容,没有内容前,阻塞
        fmt.Println("num = ", num)
    }

}

/*
len(ch) = 0, cap(ch)= 0
子协程:i = 0  //暂停2秒(主线程暂定2s)

num =  0
子协程:i = 1
子协程:i = 2
num =  1
num =  2
*/
eg
package main

import (
    "fmt"
)

// channel 练习
func main() {
    ch1 := make(chan int)
    ch2 := make(chan int)
    // 开启goroutine将0~100的数发送到ch1中
    go func() {
        for i := 0; i < 100; i++ {
            ch1 <- i
        }
        close(ch1)
    }()
    // 开启goroutine从ch1中接收值,并将该值的平方发送到ch2中
    go func() {
        for {
            i, ok := <-ch1
            if !ok {
                break
            }
            ch2 <- i * i
        }
        close(ch2)
    }()
    // 在主goroutine中从ch2中接收值打印
    for i := range ch2 {
        fmt.Println(i)
    }
}
在接收值的时候判断通道是否被关闭

有缓冲的channel

有缓冲的通道(buffered channel)是一种在被接收前能存储一个或者多个值的通道。

这种类型的通道并不强制要求 goroutine 之间必须同时完成发送和接收。通道会阻塞发送和接收动作的条件也会不同。只有在通道中没有要接收的值时,接收动作才会阻塞。只有在通道没有可用缓冲区容纳被发送的值时,发送动作才会阻塞。

这导致有缓冲的通道和无缓冲的通道之间的一个很大的不同:无缓冲的通道保证进行发送和接收的 goroutine 会在同一时间进行数据交换;有缓冲的通道没有这种保证。
package main

import (
    "fmt"
    "time"
)

func main() {
    //创建一个有缓存的channel
    ch := make(chan int, 3)

    //len(ch)缓冲区剩余数据个数, cap(ch)缓冲区大小
    fmt.Printf("len(ch) = %d, cap(ch)= %d\n", len(ch), cap(ch))

    //新建协程
    go func() {
        for i := 0; i < 10; i++ {
            ch <- i //往chan写内容
            fmt.Printf("子协程[%d]: len(ch) = %d, cap(ch)= %d\n", i, len(ch), cap(ch))
        }
    }()

    //延时
    time.Sleep(2 * time.Second)

    for i := 0; i < 10; i++ {
        num := <-ch //读管道中内容,没有内容前,阻塞
        fmt.Println("num = ", num)
    }

}

/*
len(ch) = 0, cap(ch)= 3
子协程[0]: len(ch) = 1, cap(ch)= 3
子协程[1]: len(ch) = 2, cap(ch)= 3
子协程[2]: len(ch) = 3, cap(ch)= 3  //子协程写满channel后,子协程阻塞,此时主子协暂停中

num =  0
num =  1
num =  2
num =  3
子协程[3]: len(ch) = 1, cap(ch)= 3
子协程[4]: len(ch) = 0, cap(ch)= 3
子协程[5]: len(ch) = 1, cap(ch)= 3
子协程[6]: len(ch) = 2, cap(ch)= 3
子协程[7]: len(ch) = 3, cap(ch)= 3
num =  4
num =  5
num =  6
num =  7
num =  8
子协程[8]: len(ch) = 0, cap(ch)= 3
子协程[9]: len(ch) = 0, cap(ch)= 3
num =  9
*/
eg

 

无缓冲的通道又称为阻塞的通道。我们来看一下下面的代码:

func main() {
	ch := make(chan int)
	ch <- 10
	fmt.Println("发送成功")
}

上面这段代码能够通过编译,但是执行的时候会出现以下错误:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
        .../src/github.com/Q1mi/studygo/day06/channel02/main.go:8 +0x54

为什么会出现deadlock错误呢?
因为我们使用ch := make(chan int)创建的是无缓冲的通道,无缓冲的通道只有在有人接收值的时候才能发送值。就像你住的小区没有快递柜和代收点,快递员给你打电话必须要把这个物品送到你的手中,简单来说就是无缓冲的通道必须有接收才能发送。
上面的代码会阻塞在ch <- 10这一行代码形成死锁,那如何解决这个问题呢?


一种方法是启用一个goroutine去接收值(还有一种就是使用有缓冲区的通道),例如:

func recv(c chan int) {
	ret := <-c
	fmt.Println("接收成功", ret)
}
func main() {
	ch := make(chan int)
	go recv(ch) // 启用goroutine从通道接收值
	ch <- 10
	fmt.Println("发送成功")
}

无缓冲通道上的发送操作会阻塞,直到另一个goroutine在该通道上执行接收操作,这时值才能发送成功,两个goroutine将继续执行。相反,如果接收操作先执行,接收方的goroutine将阻塞,直到另一个goroutine在该通道上发送一个值。

使用无缓冲通道进行通信将导致发送和接收的goroutine同步化。因此,无缓冲通道也被称为同步通道。

单向channel

默认情况下,通道是双向的,也就是,既可以往里面发送数据也可以同里面接收数据。
有的时候我们会将通道作为参数在多个任务函数间传递,很多时候我们在不同的任务函数中使用通道都会对其进行限制,比如只能发送或只能接收。
Go语言中提供了单向通道来处理这种情况。

package main

//"fmt"

func main() {
    //创建一个channel, 双向的
    ch := make(chan int)

    //双向channel能隐式转换为单向channel
    var writeCh chan<- int = ch //只能写,不能读
    var readCh <-chan int = ch  //只能读,不能写

    writeCh <- 666 ////<-writeCh //err,  invalid operation: <-writeCh (receive from send-only type chan<- int)

    <-readCh ////readCh <- 666 //写, err,  invalid operation: readCh <- 666 (send to receive-only type <-chan int)

    //单向无法转换为双向
    //var ch2 chan int = writeCh //cannot use writeCh (type chan<- int) as type chan int in assignment

}
单向channel的特性
func counter(out chan<- int) {
    for i := 0; i < 100; i++ {
        out <- i
    }
    close(out)
}

func squarer(out chan<- int, in <-chan int) {
    for i := range in {
        out <- i * i
    }
    close(out)
}
func printer(in <-chan int) {
    for i := range in {
        fmt.Println(i)
    }
}

func main() {
    ch1 := make(chan int)
    ch2 := make(chan int)
    go counter(ch1)
    go squarer(ch2, ch1)
    printer(ch2)
}
View Code

其中,chan<- int是一个只能发送的通道,可以发送但是不能接收;<-chan int是一个只能接收的通道,可以接收但是不能发送。在函数传参及任何赋值操作中将双向通道转换为单向通道是可以的,但反过来是不可以的。

 

select

在某些场景下我们需要同时从多个通道接收数据。通道在接收数据时,如果没有数据可以接收将会发生阻塞。你也许会写出如下代码使用遍历的方式来实现:

for{
    // 尝试从ch1接收值
    data, ok := <-ch1
    // 尝试从ch2接收值
    data, ok := <-ch2
    …
}

这种方式虽然可以实现从多个通道接收值的需求,但是运行性能会差很多。为了应对这种场景,Go内置了select关键字,可以同时响应多个通道的操作。select的使用类似于switch语句,它有一些列case分支和一个默认的分支。

每个case会对应一个通道的通信(接收或发送)过程。select会一直等待,直到某个case的通信操作完成时,就会执行case分支对应的语句。

与switch语句可以选择任何可使用相等比较的条件相比, select有比较多的限制,其中最大的一条限制就是每个case语句里必须是一个IO操作,大致的结构如下: 

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

在一个select语句中,Go语言会按顺序从头至尾评估每一个发送和接收的语句。

如果其中的任意一语句可以继续执行(即没有被阻塞),那么就从那些可以执行的语句中任意选择一条来使用。

如果没有任意一条语句可以执行(即所有的通道都被阻塞),那么有两种可能的情况:

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

举个小例子来演示下select的使用:  

func main() {
	ch := make(chan int, 1)
	for i := 0; i < 10; i++ {
		select {
		case x := <-ch:
			fmt.Println(x)
		case ch <- i:
		}
	}
}

使用select语句能提高代码的可读性。如果多个case同时满足,select会随机选择一个。对于没有case的select{}会一直等待。  

package main

import (
    "fmt"
    "math"
    "time"
)

// select多路复用

var ch1 = make(chan string, 100)
var ch2 = make(chan string, 100)

func f1(ch chan string) {
    for i := 0; i < math.MaxInt64; i++ {
        ch <- fmt.Sprintf("f1:%d", i)
        time.Sleep(time.Millisecond * 50)
    }
}

func f2(ch chan string) {
    for i := 0; i < math.MaxInt64; i++ {
        ch <- fmt.Sprintf("f2:%d", i)
        time.Sleep(time.Millisecond * 100)
    }
}

func main() {
    go f1(ch1) // 往ch1这个通道中放f1开头的字符串
    go f2(ch2) // 往ch2这个通道中放f2开头的字符串

    for {
        select {
        case ret := <-ch1:
            fmt.Println(ret)
        case ret := <-ch2:
            fmt.Println(ret)
        default:
            fmt.Println("暂时取不到值")
            time.Sleep(time.Millisecond * 500)
        }
    }
}
View Code

并发安全和锁

有时候在Go代码中可能会存在多个goroutine同时操作一个资源(临界区),这种情况会发生竞态问题(数据竞态)。类比现实生活中的例子有十字路口被各个方向的的汽车竞争;还有火车上的卫生间被车厢里的人竞争。 举个例子:

package main

import "fmt"
import "sync"

var x int
var wg sync.WaitGroup

func add() {  # 协程可以直接修改全局变量
	for i := 0; i < 5000; i++ {
		x = x + 1
	}
	wg.Done()
}
func main() {
	wg.Add(2)
	go add()
	go add()
	wg.Wait()
	fmt.Println(x)
}

  

上面的代码中我们开启了两个goroutine去累加变量x的值,这两个goroutine在访问和修改x变量的时候就会存在数据竞争,导致最后的结果与期待的不符。x最后的结果每次运行可能都不一样。

互斥锁

互斥锁是一种常用的控制共享资源访问的方法,它能够保证同时只有一个goroutine可以访问共享资源。Go语言中使用sync包的Mutex类型来实现互斥锁。 使用互斥锁来修复上面代码的问题:

package main

import (
	"fmt"
	"sync"
)

var x int // 定义全局变量x
var wg sync.WaitGroup

// 定义一个互斥锁
var lock sync.Mutex

// 定义一个函数 对全局的变量x做累加的操作
func add() {
	for i := 0; i < 5000; i++ {
		lock.Lock() // 把厕所门锁上
		// 1. 从内存中找到x的值
		// 2. 执行+1操作
		// 3. 把结果赋值给x写到内存
		x = x + 1
		lock.Unlock() // 把厕所门打开
	}
	wg.Done()
}
func main() {
	wg.Add(2)
	go add()
	go add()
	wg.Wait()
	fmt.Println(x)
}

使用互斥锁能够保证同一时间有且只有一个goroutine进入临界区,其他的goroutine则在等待锁;当互斥锁释放后,等待的goroutine才可以获取锁进入临界区,多个goroutine同时等待一个锁时,唤醒的策略是随机的。

读写互斥锁

这个是为了优化锁用的,提高程序性能。

互斥锁是完全互斥的,但是有很多实际的场景下是读多写少的,当我们并发的去读取一个资源不涉及资源修改的时候是没有必要加锁的,这种场景下使用读写锁是更好的一种选择。读写锁在Go语言中使用sync包中的RWMutex类型。

读写锁分为两种:读锁和写锁。当一个goroutine获取读锁之后,其他的goroutine如果是获取读锁会继续获得锁,如果是获取写锁就会等待;当一个goroutine获取写锁之后,其他的goroutine无论是获取读锁还是写锁都会等待。

package main

import (
	"fmt"
	"sync"
	"time"
)

// 读比写多的时候要使用读写锁 能够提高性能

var x int
var wg sync.WaitGroup
var lock sync.Mutex     // 互斥锁
var rwLock sync.RWMutex // 读写互斥锁 : 多个goroutine同时读加的是读锁 写的时候加的是写锁

func read() {
	defer wg.Done()
	// lock.Lock() // 互斥锁
	rwLock.RLock()                   // 加读锁
	time.Sleep(time.Millisecond * 1) // 模拟读操作耗费1毫秒
	// lock.Unlock()                    // 释放互斥锁
	rwLock.RUnlock() // 释放读锁

}

func write() {
	defer wg.Done()
	rwLock.Lock() // 加写锁
	// lock.Lock() // 加互斥锁
	x = x + 1
	time.Sleep(time.Millisecond * 5) // 模拟写操作耗费了5毫秒
	// lock.Unlock()                    // 释放互斥锁
	rwLock.Unlock() // 释放写锁

}

func main() {
	start := time.Now()
	// 写10次
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go write()
	}
	// 读1000次
	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go read()
	}
	wg.Wait()
	end := time.Now()
	fmt.Printf("耗费了%v.", end.Sub(start))
}

  

Timer

只执行一次

Timer是一个定时器,代表未来的一个单一事件,你可以告诉timer你要等待多长时间,它提供一个channel,在将来的那个时间那个channel提供了一个时间值。

package main

import (
    "fmt"
    "reflect"
    "time"
)

func main02() {
    num := <-time.After(2 * time.Second) //定时2s,阻塞2s, 2s后产生一个事件,往channel写内容
    fmt.Println("时间到", num)
}

func main() {
    time.Sleep(2 * time.Second)
    fmt.Println("时间到")
}

func main01() {
    //延时2s后打印一句话
    timer := time.NewTimer(2 * time.Second)
    <-timer.C
    fmt.Println("时间到")
}
通过Timer实现延时功能
package main

import (
    "fmt"
    "time"
)

func main() {
    timer := time.NewTimer(3 * time.Second)
    ok := timer.Reset(1 * time.Second) //重新设置为1s
    fmt.Println("ok = ", ok)

    <-timer.C
    fmt.Println("时间到")

}

func main01() {
    timer := time.NewTimer(3 * time.Second)

    go func() {
        <-timer.C
        fmt.Println("子协程可以打印了,因为定时器的时间到")
    }()

    timer.Stop() //停止定时器

    for {

    }
}
停止和重置定时器

Ticker

循环执行

Ticker是一个定时触发的计时器,它会以一个间隔(interval)channel发送一个事件(当前时间),而channel的接收者可以以固定的时间间隔从channel中读取事件。

package main

import (
    "fmt"
    "time"
)

func main() {
    ticker := time.NewTicker(1 * time.Second)

    i := 0
    for {
        <-ticker.C

        i++
        fmt.Println("i = ", i)

        if i == 5 {
            ticker.Stop()
            break
        }
    }

}
View Code

 

posted @ 2019-01-10 11:27  沐风先生  阅读(289)  评论(0编辑  收藏  举报