go初学篇(八)之并发,通道

前言

go语言是并发语言,至于并发与并行的区别,简单来说就是,并发就是同时处理很多事情,它强调的是处理能力,并不是同时在做,并行就是同时在做多个事情,举个简单的例子,你在敲代码,口渴了去喝水(水杯就在旁边),你能同时处理这两件事,就叫并发,你也可以边敲代码边听音乐,这就是并行,并发强调事务的交叠性,并行则强调事务的同时运作。

进程,线程,协程

进程这有什么难理解的,你电脑上每时每刻都有多个进程,简单说就是正在执行的程序,是cpu资源分配与调度的独立单位,进程创建,撤销以及切换的开销是比较大的

线程 被称为轻量级进程,是一个基本的cpu执行单元,也是程序执行过程的最小单元,一个进程包含多个线程。线程的存在减少了程序并发执行时的开销,但是线程自己并没有自己的系统资源,只存在一些运行必须的资源,在统一进程中多个线程可以同时共享进程所拥有的的系统资源,不过对于某些独占资源的锁机制,处理不当可能会产生死锁

协程是一种用户态的轻量级线程 ,之所以这样说,是因为协程的调度完全由用户控制。协程最大的优势是轻量级,可以创建上百万甚至千万的协程,而进程与线程通常不能超过1万

对于go语言对于并非的实现靠岸的是协程,Goroutine

使用Goroutine

在函数或方法调用前面加上关键字go,就会同时运行一个新的Goroutine


func hello() {  
    fmt.Println("Hello world goroutine")
}
func main() {  
    go hello()
    fmt.Println("main function")
}
main function

为什么呢,go的主goroutine运行在这里并没有等其他的goroutine,所以main运行完之后直接就结束了,相当于hello函数刚刚开始就被中断了,这与goroutine的运行机制有关,下面摘抄教程的规则:
当新的Goroutine开始时,Goroutine调用立即返回。与函数不同,go不等待Goroutine执行结束。当Goroutine调用,并且Goroutine的任何返回值被忽略之后,go立即执行到下一行代码。
main的Goroutine应该为其他的Goroutines执行。如果main的Goroutine终止了,程序将被终止,而其他Goroutine将不会运行。

修改后的代码

func hello() {  
    fmt.Println("Hello world goroutine")
}
func main() {  
    go hello()
    time.Sleep(1 * time.Second)
    fmt.Println("main function")
}

这里加了一个sleep函数,让主goroutine等待了一段时间

Hello world goroutine
main function

那么如果启动多个goroutine该怎么办呢
直接附上代码


func numbers() {  
    for i := 1; i <= 5; i++ {
        time.Sleep(250 * time.Millisecond)
        fmt.Printf("%d ", i)
    }
}
func alphabets() {  
    for i := 'a'; i <= 'e'; i++ {
        time.Sleep(400 * time.Millisecond)
        fmt.Printf("%c ", i)
    }
}
func main() {  
    go numbers()
    go alphabets()
    time.Sleep(3000 * time.Millisecond)
    fmt.Println("main terminated")
}

在上面的每个函数里都定义了一定的等待时间,这样就可以保证多个goroutine就可以正常的进行工作了

1 a 2 3 b 4 c 5 d e main terminated       

runtime包

本来在想要不要将这个,但是觉得有很有必要说一下,那我就简单的把它的用途说一下吧。
1.获取goroot和os

//获取goroot目录:
fmt.Println("GOROOT-->",runtime.GOROOT())

//获取操作系统
fmt.Println("os/platform-->",runtime.GOOS) 

2.获取,设置CPU数量

func init(){
    //1.获取逻辑cpu的数量
    fmt.Println("逻辑CPU的核数:",runtime.NumCPU())
    //2.设置go程序执行的最大的:[1,256]
    n := runtime.GOMAXPROCS(runtime.NumCPU())
    fmt.Println(n)
}

3.Goshed()

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

    }()

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



4.Goexit的使用

func main() {
    //创建新建的协程
    go func() {
        fmt.Println("goroutine开始。。。")

        //调用了别的函数
        fun()

        fmt.Println("goroutine结束。。")
    }() //别忘了()

    //睡一会儿,不让主协程结束
    time.Sleep(3*time.Second)
}

func fun() {
    defer fmt.Println("defer。。。")

    //return           //终止此函数
    runtime.Goexit() //终止所在的协程

    fmt.Println("fun函数。。。")
}

临界资源安全问题

所谓临界资源,就是并发过程中中,几个进程/线程/协程共同占有可以使用的资源。所谓并发并不难,只是对于临界资源的争夺成了一大难题,很多编程语言都是通过上锁的方式来解决的,所以我们可以通过sync包来访问
下面摘抄教程上的一段代码,注释写的很详细

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

//全局变量
var ticket = 10 // 100张票

var wg sync.WaitGroup
var matex sync.Mutex // 创建锁头

func main() {
    /*
    4个goroutine,模拟4个售票口,4个子程序操作同一个共享数据。
     */
    wg.Add(4)
    go saleTickets("售票口1") // g1,100
    go saleTickets("售票口2") // g2,100
    go saleTickets("售票口3") //g3,100
    go saleTickets("售票口4") //g4,100
    wg.Wait()              // main要等待。。。

    //time.Sleep(5*time.Second)
}

func saleTickets(name string) {
    rand.Seed(time.Now().UnixNano())
    defer wg.Done()
    //for i:=1;i<=100;i++{
    //  fmt.Println(name,"售出:",i)
    //}
    for { //ticket=1
        matex.Lock()
        if ticket > 0 { //g1,g3,g2,g4
            //睡眠
            time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
            // g1 ,g3, g2,g4
            fmt.Println(name, "售出:", ticket) // 1 , 0, -1 , -2
            ticket--                         //0 , -1 ,-2 , -3
        } else {
            matex.Unlock() //解锁
            fmt.Println(name, "售罄,没有票了。。")
            break
        }
        matex.Unlock() //解锁
    }
}
售票口1 售出: 10
售票口1 售出: 9
售票口3 售出: 8
售票口2 售出: 7
售票口4 售出: 6
售票口1 售出: 5
售票口3 售出: 4
售票口2 售出: 3
售票口4 售出: 2
售票口1 售出: 1
售票口3 售罄,没有票了。。
售票口4 售罄,没有票了。。
售票口2 售罄,没有票了。。
售票口1 售罄,没有票了。。

在go语言中一直提倡**不要以共享内存的方式去通信,而要以通信的方式去共享内存。**所以其实在go中并不提倡上锁,而是通过channel

channel通道

channel一般被认为是Goroutine通信的管道。一般来说goroutine会把自己数据封装成一个对象,把这个数据对象的指针存到channel,这样另一个goroutine就可以通过channel读出这个指针,并处理它指向的内存对象
声明通道

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

当然了,也可以简单的神明a:=make(chan int)
通道的数据类型
通道是引用类型,作为参数时,传递的是内存地址

func main() {
    ch1 := make(chan int)
    fmt.Printf("%T,%p\n",ch1,ch1)

    test1(ch1)

}

func test1(ch chan int){
    fmt.Printf("%T,%p\n",ch,ch)
}

通道的使用方法

发送和接收
一般格式
data := <-a
a <-data
v,ok := <-a
一般来说发送和接收的默认值是阻塞的
直接看代码

func main() {
    var ch1 chan bool       //声明,没有创建
    fmt.Println(ch1)        //
    fmt.Printf("%T\n", ch1) //chan bool
    ch1 = make(chan bool)   //0xc0000a4000,是引用类型的数据
    fmt.Println(ch1)

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

        fmt.Println("结束。。")

    }()

    data := <-ch1 // 从ch1通道中读取数据
    fmt.Println("data-->", data)
    fmt.Println("main。。over。。。。")
}
<nil>
chan bool           
0xc0000160c0        
子goroutine中,i: 0
子goroutine中,i: 1
子goroutine中,i: 2
子goroutine中,i: 3
子goroutine中,i: 4
子goroutine中,i: 5
子goroutine中,i: 6
子goroutine中,i: 7
子goroutine中,i: 8
子goroutine中,i: 9
结束。。            
data--> true        
main。。over。。。。

注意:如果一个goroutine在通道上发送数据,那么对应的其他通道应该接收数据,如果这种情况没有发生就会死锁,类似,如果goroutine正在等待从通道接收数据,那么另一些goroutine应该在通道上写入数据,否则还会死锁

关闭通道
发送者可以通过关闭通道close(ch),来通知接收方不会有更多的数据发送到channel
接收者可以在接收数据时增设额外的变量来检查通道是否关闭
v,ok:=<-ch

通道上的范围循环
直接上代码

import (
    "time"
    "fmt"
)

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)  {
    for i:=0;i<10 ; i++ {
        time.Sleep(1*time.Second)
        ch1 <- i
    }
    close(ch1)//通知对方,通道关闭
}
读取数据: 0
读取数据: 1
读取数据: 2
读取数据: 3
读取数据: 4
读取数据: 5
读取数据: 6
读取数据: 7
读取数据: 8
读取数据: 9
main..over.....

缓冲通道与定向通道

缓冲通道
之前学的任何通道都是非缓冲的,发送和接收到一个未缓冲的通道是阻塞的
那么对于缓冲通道,就是指一个通道带有一个缓冲区,发送到一个缓冲通道只有缓冲区满的时候才被阻塞,同理,从缓冲通道接收的信息,只有在缓冲区为空的时候才会被阻塞
直接上代码

import (
    "fmt"
    "strconv"
    "time"
)

func main() {
    /*
    非缓存通道:make(chan T)
    缓存通道:make(chan T ,size)
        缓存通道,理解为是队列:

    非缓存,发送还是接受,都是阻塞的
    缓存通道,缓存区的数据满了,才会阻塞状态。。

     */
    ch1 := make(chan int)           //非缓存的通道
    fmt.Println(len(ch1), cap(ch1)) //0 0
    //ch1 <- 100//阻塞的,需要其他的goroutine解除阻塞,否则deadlock

    ch2 := make(chan int, 5)        //缓存的通道,缓存区大小是5
    fmt.Println(len(ch2), cap(ch2)) //0 5
    ch2 <- 100                      //
    fmt.Println(len(ch2), cap(ch2)) //1 5

    //ch2 <- 200
    //ch2 <- 300
    //ch2 <- 400
    //ch2 <- 500
    //ch2 <- 600
    fmt.Println("--------------")
    ch3 := make(chan string, 4)
    go sendData3(ch3)
    for {
        time.Sleep(1*time.Second)
        v, ok := <-ch3
        if !ok {
            fmt.Println("读完了,,", ok)
            break
        }
        fmt.Println("\t读取的数据是:", v)
    }

    fmt.Println("main...over...")
}

func sendData3(ch3 chan string) {
    for i := 0; i < 10; i++ {
        ch3 <- "数据" + strconv.Itoa(i)
        fmt.Println("子goroutine,写出第", i, "个数据")
    }
    close(ch3)
}
0 0
0 5
1 5
--------------
子goroutine,写出第 0 个数据
子goroutine,写出第 1 个数据
子goroutine,写出第 2 个数据
子goroutine,写出第 3 个数据
        读取的数据是: 数据0
子goroutine,写出第 4 个数据
        读取的数据是: 数据1
子goroutine,写出第 5 个数据
        读取的数据是: 数据2
子goroutine,写出第 6 个数据
        读取的数据是: 数据3
子goroutine,写出第 7 个数据
        读取的数据是: 数据4
子goroutine,写出第 8 个数据
        读取的数据是: 数据5
子goroutine,写出第 9 个数据
        读取的数据是: 数据6
        读取的数据是: 数据7
        读取的数据是: 数据8
        读取的数据是: 数据9
读完了,, false
main...over...

双向通道
就是截止到如今我们所学的通道既可以发送数据,又可以读取数据,我们称之为双向通道
单向通道
直接上代码

func main()  {
    /*
        单向:定向
        chan <- T,
            只支持写,
        <- chan T,
            只读

        用于参数传递:
     */
    ch1 := make(chan int)//双向,读,写
    //ch2 := make(chan <- int) // 单向,只写,不能读
    //ch3 := make(<- chan int) //单向,只读,不能写
    //ch1 <- 100
    //data :=<-ch1
    //ch2 <- 1000
    //data := <- ch2
    //fmt.Println(data)
    //  <-ch2 //invalid operation: <-ch2 (receive from send-only type chan<- int)
    //ch3 <- 100
    //  <-ch3
    //  ch3 <- 100 //invalid operation: ch3 <- 100 (send to receive-only type <-chan int)

    //go fun1(ch2)
    go fun1(ch1)
    data:= <- ch1
    fmt.Println("fun1中写出的数据是:",data)

    //fun2(ch3)
    go fun2(ch1)
    ch1 <- 200
    fmt.Println("main。。over。。")
}
//该函数接收,只写的通道
func fun1(ch chan <- int){
    // 函数内部,对于ch只能写数据,不能读数据
    ch <- 100
    fmt.Println("fun1函数结束。。")
}

func fun2(ch <-chan int){
    //函数内部,对于ch只能读数据,不能写数据
    data := <- ch
    fmt.Println("fun2函数,从ch中读取的数据是:",data)
}
fun1函数结束。。
fun1中写出的数据是: 100
main。。over。。    

其他的time包之类的通道相关函数我就不说了,找度娘吧
到这里go的基础篇就更完了,下面我就要具体写一个项目(简单的小游戏之类),博客也会更新这个项目的相关内容,我也会把我的代码发到我的github上,还有建议在用github的伙伴尽快把自己的相关资源copy一份到gitee上,因为漂亮国估计又要耍无赖了,下面附上操作链接,希望大家未雨绸缪
https://mp.weixin.qq.com/s/daGQBccYVdD07otdQrt9AA

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值