2018年10月

Golang-channel通道初识

1.通道可以被认为是Goroutines通信的管道;当多个goroutines想共享数据的时候,虽然也提供了传统的同步机制(同步等待组、同步锁),但是Go语言强烈建议的是使用Channel通道来实现goroutine之间的通信;

2.通道的声明和定义一个变量的语法一样; 通道是引用型数据

//声明通道
var 通道名 chan 数据类型
//创建通道:如果通道为nil(就是不存在),就需要先创建通道
通道名 = make(chan 数据类型)

func main(){
    var a chan int
    if a == nil {
        fmt.Println("channel 是nil的,不能使用,需要先创建通道。")
    }
    a = make(chan int)
    fmt.Printf("数据类型:%T", a)

}

//也可以简短声明:
a := make(chan int)

3.通道的注意点

  • 用于goroutine,传递消息
  • 每个通道都有相关联的数据类型
  • 使用通道传递数据 <-
  • chan <-data,发送数据到通道。向通道中写数据
  • data <-chan,从通道中获取数据。从通道中读取数据
  • 阻塞:发送数据 chan <- data,阻塞的,直到另一条goroutine,读取数据来解除阻塞;读取数据,data <- chan,也是阻塞的。直到另一条goroutine,写出数据解除阻塞;
  • 本身channel就是同步的,意味着同一时间,只能有一条goroutine来操作
func main() {
    var ch1 chan bool
    ch1 = make(chan bool)

    go func() {
        for i:=0; i<10; i++ {
            fmt.Println("子goroutine中,i:", i)
        }
        //循环结束,向通道中写数据,表示要结束了。。。
        ch1 <- true
        fmt.Println("结束")
    }()


    //如果主进程先抢占资源,会去chan中读数据,如果没有数据,会阻塞下去..
    data := <- ch1
    fmt.Println("main...data-->:", data)
    fmt.Println("main主进程结束...")
}
//阻塞式通道
func main() {
    ch1 := make(chan int)

    go func() {
        fmt.Println("子goroutine开始执行")

        //time.Sleep(2*time.Second)
        data := <- ch1 //从ch1中读取数据
        fmt.Println("data:", data)
    }()

    //time.Sleep(5*time.Second)
    ch1 <- 10
    fmt.Println("main...over...")
}
//死锁:使用通道的时候要考虑的一个重要因素;只有发送数据,没有接受数据;或者只有接受数据,没有发送数据

func main()  {
    ch1 := make(chan int)
    ch1 <- 5
}
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()

3.关闭通道和通道上范围循环

· 关闭通道,发送者可以关闭通道,来通知接受方不会有更多的数据发送到channel上: close(ch);接受者可以在接收来自通道的数据时使用额外的变量来检查通道是否已经关闭:v,ok := <-ch, 如果ok的值是true,表示成功的读取了一个数据value。如果为false,意味着正在从一个封闭的通道读取数据,从封闭通道读取的值为零值(根据ch的类型的零值)

关闭通道:close(ch)
    子goroutine:写出10个数据
        每写一个,阻塞一次,主goroutine读取一次,解除阻塞

    主goroutine,读取数据
        每次读取数据,阻塞一次,子goroutine,写出一个,解除阻塞

func main() {
    
    ch1 := make(chan int)
    go sendData(ch1)

    for{
        time.Sleep(1*time.Second)
        v, ok := <- ch1
        if !ok {
            fmt.Println("已经读取了所有的数据。。。", ok)
            break
        }
        fmt.Println("读取的数据:", v, ok)
    }
    fmt.Println("main..over...")

}

func sendData(ch1 chan int)  {
    //发送方:10条数据
    for i:=0; i <10; i++ {
        ch1 <- i
    }
    close(ch1)
}

读取的数据: 0 true
读取的数据: 1 true
读取的数据: 2 true
读取的数据: 3 true
读取的数据: 4 true
读取的数据: 5 true
读取的数据: 6 true
读取的数据: 7 true
读取的数据: 8 true
读取的数据: 9 true
已经读取了所有的数据。。。 false
main..over...
  • 通道上的范围循环
func main() {

    ch1 := make(chan int)
    go sendData(ch1)

    //for循环的 for range形式可用于从通道接收值,直到它关闭为止,不需要单独判断
    for v := range ch1{//从通道读取数据,如果没值,则阻塞
        fmt.Println("读取数据:", v)
    }
    fmt.Println("main..over.....")

}

func sendData(ch1 chan int)  {
    //发送方:10条数据
    for i:=0; i <10; i++ {
        time.Sleep(1*time.Second)
        ch1 <- i
    }
    close(ch1)//通知对方,通道关闭;否则会发生死锁
}

Golang-临界资源

001#1. 临界资源:指并发环境中多个进程、线程、协程共享的资源;但是在并发编程中对临界资源的处理不当,往往会导致数据不一致的问题

func main() {
    /*
    临界资源
    */
    a := 1
    go func() {
        a = 2
        fmt.Println("goroutine...", a)
    }()


    a = 3
        
    fmt.Println("main goroutine", a)
    
}
因为主的协程执行完之后,关闭了资源,所以子协程没有来的及执行:
main goroutine 3
func main() {
    /*
    临界资源
    */
    a := 1
    go func() {
        a = 2//14行
        fmt.Println("goroutine...", a)
    }()

    a = 3//18行
    time.Sleep(1)
    fmt.Println("main goroutine", a)
    
}
主协程先把赋值为3,在要打印还没打印的时候,睡去之后,子协程持有资源又将其赋值为2
goroutine... 2
main goroutine 2
通过命令行来看一下:
$ go run -race demo10_race.go
==================
//警告:有临界资源
WARNING: DATA RACE
//通过goroutine 6在0x00c00000a0e0处写入:
Write at 0x00c00000a0e0 by goroutine 6:
  main.main.func1()
      E:/GoPath/src/Go_Advanced/demo10_race.go:14 +0x43

//通过主goroutine在0x00c00000a0e0上一次写入:
Previous write at 0x00c00000a0e0 by main goroutine:
  main.main()
      E:/GoPath/src/Go_Advanced/demo10_race.go:18 +0x8f

Goroutine 6 (running) created at:
  main.main()
      E:/GoPath/src/Go_Advanced/demo10_race.go:13 +0x81
==================
main goroutine 2
goroutine... 2
Found 1 data race(s)
exit status 66

2. 并发本身并不复杂,但是因为有了资源竞争的问题,就会有许多莫名其妙的事情

var ticket = 10
func main() {

    go sale("售票口1")
    go sale("售票口2")
    go sale("售票口3")
    go sale("售票口4")

    time.Sleep(5*time.Second)
}

func sale(name string){
    rand.Seed(time.Now().UnixNano())
    for  {
        if ticket > 0  {
            time.Sleep(time.Duration(rand.Intn(1000))*time.Microsecond)
            fmt.Println(name,"售出:", ticket)
            ticket--
        } else {
            fmt.Println("售完票了")
            break
        }
    }

}

售票口4 售出: 10
售票口2 售出: 10
售票口1 售出: 10
售票口3 售出: 10
售票口4 售出: 6
售票口1 售出: 6
售票口2 售出: 5
售票口3 售出: 5
售票口1 售出: 2
售票口2 售出: 1
售完票了
售票口4 售出: 1
售完票了
售票口3 售出: 0
售完票了
售票口1 售出: -2
售完票了

3. 解决:很多其他语言都是同步方式,通过上锁的方式,及某一段时间段,只能允许一个协程来访问这个共享数据, sync同步包;

import (
    "fmt"
    "sync"
)

var wg sync.WaitGroup

func main() {

    /**
    WaitGroup: 同步等待组
        Add(), 设置等待组中要执行的子 goroutine的数量

        Wait(), 让主goroutine处于等待


        Done(),让等待组中的计数器减一,=在子goroutine中结束等待
     */

    wg.Add(2)

    go fun1()
    go fun2()

    fmt.Println("main 进入阻塞状态,等待wg子goroutine结束")

    wg.Wait() //表示goroutine进入等待,意味着阻塞

    fmt.Println("main..结束阻塞")
}

func fun1()  {
    for i:= 1; i<10; i++ {
        fmt.Println("fun1()函数中打印...A", i)
    }

    wg.Done()//执行完会后,将计数器减1 wg.Add(-1)
}
func fun2()  {

    defer wg.Done()
    for j:= 1; j<10; j++ {
        fmt.Println("\tfun2()函数中打印...A", j)
    }
}

4. go并不鼓励用锁来保护临界资源,而是鼓励通过channel将共享状态的变化在各个协程之间传递;要以通信的方式去共享内存;

  • 互斥锁(同步):最简单的,也是最暴力的;降低了性能
var ticket = 10

var mutex sync.Mutex

var wg sync.WaitGroup
func main() {


    wg.Add(4)
    go sale("售票口1")
    go sale("售票口2")
    go sale("售票口3")
    go sale("售票口4")

    wg.Wait()//main要等待
    fmt.Println("程序结束了")
    //time.Sleep(5*time.Second)
}

func sale(name string){
    rand.Seed(time.Now().UnixNano())
    defer wg.Done()
    for  {

        mutex.Lock()
        if ticket > 0  {
            time.Sleep(time.Duration(rand.Intn(1000))*time.Microsecond)
            fmt.Println(name,"售出:", ticket)
            ticket--
        } else {
            mutex.Unlock()
            fmt.Println("售完票了")
            break
        }

        mutex.Unlock()
    }

}
  • 读写锁:针对读或写的互斥锁,锁可以由任意数量的读取器或单个编写器持有;基本遵循两大原则:可以随便读,多个goroutine同时读;写的时候啥也不能干,不能读也不能写。
var rwMutex *sync.RWMutex
var wg *sync.WaitGroup
func main() {
    rwMutex = new(sync.RWMutex)
    wg = new(sync.WaitGroup)

    wg.Add(2)

    go readData(1)
    go readData(2)

    wg.Wait()
    fmt.Println("main over...")

}

func readData(i int){
    defer wg.Done()
    fmt.Println(i, "开始读: read start...")

    rwMutex.RLock()//读操作上锁
    fmt.Println(i, "正在读取数据:reading...")
    time.Sleep(1*time.Second)
    rwMutex.RUnlock()//读操作解锁
    fmt.Println(i, "读结束:read over...")

}
2 开始读: read start...
2 正在读取数据:reading...
1 开始读: read start...
1 正在读取数据:reading...
2 读结束:read over...
1 读结束:read over...
main over...

读操作可以同时进行
var rwMutex *sync.RWMutex
var wg *sync.WaitGroup
func main() {
    rwMutex = new(sync.RWMutex)
    wg = new(sync.WaitGroup)

    wg.Add(3)

    go writeData(1)
    go readData(2)
    go writeData(3)

    wg.Wait()
    fmt.Println("main over...")

}

func writeData(i int)  {
    defer wg.Done()
    fmt.Println(i, "开始写: write start...")

    rwMutex.Lock()//读操作上锁
    fmt.Println(i, "正在写数据:writing...")
    time.Sleep(3*time.Second)
    rwMutex.Unlock()//读操作解锁
    fmt.Println(i, "写结束:write over...")

}
3 开始写: write start...
3 正在写数据:writing...
2 开始读: read start...
1 开始写: write start...
明显卡顿:必须等3写完,解锁之后才可以;这个时候其他两个协程(1,2)是不能操作的
3 写结束:write over...
2 正在读取数据:reading...
2 读结束:read over...
1 正在写数据:writing...
1 写结束:write over...
main over...