golang goroutine

一.goroutine基本介绍

1.进程和线程说明介绍

1)进行就是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位(比如迅雷进程)

2)线程是进程的一个执行实例,是程序执行的最小单元,它是比进程更小的能独立运行的基本单位(比如迅雷中有好几个任务正在下载)

3)一个进程可以创建和销毁多个线程,同一个进程中的多个线程可以并发执行。(并发是时间片很短,来回切换,同一时间只有一个在进行,而不是并行)

4)一个程序至少有一个进程,一个进程至少有一个线程。(比如同时打开一个程序的多个客户端就是起了多个进程)

2.并发和并行

go语言可以把并发转成并行

1)多线程程序在单核上运行,就是并发。多个任务作用在一个cpu上。从微观角度来看,在一个时间点,只有一个任务在执行,多个任务切换进行。

2)多线程程序在多核上运行,就是并行。多个任务作用在多个cpu上。从微观的角度来看,在同一时间点,有多个任务在同事执行。并行速度比并发快。

3.Go协程和Go主线程。(goroutine go中可以跑上w个协程)

1)Go线程(有程序员直接称为线程/可以理解为进程):一个线程上,可以起多个协程。协程是轻量级的线程【编译器做了优化】

2)Go协程的特点

有独立的栈空间、共享程序堆空间、调度由用户控制、协程是轻量级的线程

二.案例

1.快速入门案例

如下图所示:

1)如果主线程退出了,则协程即使还没有执行完毕,也会退出

2)协程可以在主线程没有退出前就结束

复制代码
package main

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

func test() {
    for i := 0; i < 10; i++ {
        fmt.Println("test()hello.world" + strconv.Itoa(i))
        time.Sleep(time.Second)
    }
}
func main() {
    go test() //开启协程
    for i := 0; i < 10; i++ {
        fmt.Println("main()hello.golang" + strconv.Itoa(i))
        time.Sleep(time.Second)
    }
}
复制代码

快速入门小结:

1)主线程是一个物理线程,直接作用在cpu上的,是重量级的,非常耗费cpu资源。

2)协程是从主线程开启的,是轻量级的线程,是逻辑态,对资源消耗相对小。

3)golang的协程机制是中药的特点,可以轻松开启上万的协程,其他编程语言的并发机制一般是基于线程的,开启过多的线程,资源耗费大,这里就凸显出golang在并发上的优势了。因为线程要有各自的堆栈,而协程是有自己的栈,共享堆。

二.实例

1.高效利用cpu

常用函数 runtime包:设置go程序使用多少cpu。go1.8以前需要手动设置,go1.8以后会默认设置多核运行

1)func NumCPU() int: NumCPU返回本地机器的逻辑CPU个数。

2)func GOMAXPROCS: func GOMAXPROCS(n int) int GOMAXPROCS设置可同时执行的最大CPU数,并返回先前的设置。 若 n < 1,它就不会更改当前设置。本地机器的逻辑CPU数可通过 NumCPU 查询。本函数在调度程序优化后会去掉。

 

复制代码
package main

import (
    "fmt"
    "runtime"
)
func main() {
    //查看本地机器逻辑cpu的个数
    fmt.Println(runtime.NumCPU())
    //设置go程序可同时使用cpu的个数
    runtime.GOMAXPROCS(4)
}
复制代码

 

2.不同协程之间的通讯问题(资源争夺)

主要体现在不能同时写入,会报错fatal error: concurrent map writes 

(1)全局变量加锁同步 使用同步互斥锁

package sync :import "sync"   sync包提供了基本的同步基元,如互斥锁。除了Once和WaitGroup类型,大部分都是适用于低水平程序线程,高水平的同步使用channel通信更好一些。

type Mutex :type Mutex struct {// 包含隐藏或非导出字段}

Mutex是一个互斥锁,可以创建为其他结构体的字段;零值为解锁状态。Mutex类型的锁和线程无关,可以由不同的线程加锁和解锁。

func (*Mutex) Lock:func (m *Mutex) Lock()

Lock方法锁住m,如果m已经加锁,则阻塞直到m解锁。

func (*Mutex) Unlock:func (m *Mutex) Unlock()

Unlock方法解锁m,如果m未加锁会导致运行时错误。锁和线程无关,可以由不同的线程加锁和解锁。

复制代码
package main

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

var (
    map1 = make(map[int]int, 10)
    //lock是一个全局互斥锁
    //sync是包:synchornized 同步
    //Mutex: 互斥
    lock sync.Mutex
)

func test(n int) {
    res := 1
    for i := 1; i <= n; i++ {
        res *= i
    }
    //写入前上锁
    lock.Lock()
    map1[n] = res
    lock.Unlock()
    //写入后解锁
}
func main() {
    for i := 1; i <= 200; i++ {
        go test(i)
    }
    //休眠5s,这个时间不能把握,所以使用channel
    time.Sleep(time.Second * 5)
    //在读之前,因为系统不能确定主线程休眠时间是否足够协程使用,所以还需要加锁解锁
    lock.Lock()
    for i, v := range map1 {
        fmt.Printf("map1[%v]=%v\n", i, v)
    }
    lock.Unlock()
}
复制代码

(2.)使用channel(管道)解决资源竞争

1)为什么需要channel

使用全局变量加锁同步来解决goroutine的通讯,但不完美

①主线程在等待所有goroutine全部完成的时间很难确定,这里设置的5s,仅是估算

②如果休眠时间长了,会加长等待时间,如果等待时间短了,可能还有goroutine处于工作状态,这是也会随主线程的退出而销毁

③通过全局变量加锁来实现通讯,也并不利用多个协程对全局变量的读写操作。

④上面种种分享都在呼唤一个新的通讯机制channel

2)channel的介绍

①channel本质就是一个数据结构-队列

②数据是先进先出(FIFO: first in first out)

③channel是线程安全,多个协程同时操作一个管道时,不会发生资源竞争问题,不用加锁

④channel时有类型的,一个string的channel只能存放string类型的数据

3)channel的定义/声明

var 变量名 chan 数据类型

举例:

var intChan chan int (intChan用于存放int数据)

var mapChan chan map[int]string (mapChan用于存放map[int]string类型)
var perChan chan Person(结构体)

var perChan2 chan *Person(结构体指针)

...

说明

①channel是引用类型

②channel必须初始化才能写入数据,即make后才能使用

③管道是有类型的,intChan只能写入整数int

4)channel使用与注意事项

①channel中只能存放指定的数据类型

②channel的数据存满后,就不能再放入了

③在没有使用协程的情况下,如果channel数据取完了,再取,就会报错dead lock

复制代码
package main

import "fmt"

type Cat struct {
    Name string
    Age  int
}

func main() {
    //1.int
    //创建一个intchan,最多可以存放3个int,演示存3数据到intChan,然后再去除三个int
    var intChan chan int
    intChan = make(chan int, 3)

    intChan <- 10
    intChan <- 20
    intChan <- 30
    n1 := <-intChan
    n2 := <-intChan
    n3 := <-intChan

    fmt.Println(n1, n2, n3)
    //2.map
    //创建一个mapChan,最多可以存放10个map[string]string的key-val,演示写入与读取
    mapChan := make(chan map[string]string, 10)
    m1 := make(map[string]string, 20)
    m1["name1"] = "李逵"
    m1["name2"] = "林冲"
    m1["name3"] = "宋江"
    m2 := make(map[string]string, 20)
    m2["city1"] = "北京"
    m2["city2"] = "上海"
    m2["city3"] = "深圳"

    mapChan <- m1
    mapChan <- m2

    mc1 := <-mapChan
    mc2 := <-mapChan
    fmt.Println(mc1, mc2)
    //3.struct
    //创建一个catChan,最多可以存放10个Cat结构体变量,演示写入和取出的用法
    catChan := make(chan Cat, 10)
    c1 := Cat{"tom", 3}
    c2 := Cat{"jack", 12}

    catChan <- c1
    catChan <- c2

    cc1 := <-catChan
    cc2 := <-catChan

    fmt.Println(cc1, cc2)

    //4.*struct
    //创建一个catChan2,最多可以存放10个*Cat结构体变量,演示写入和取出的用法
    catChan2 := make(chan *Cat, 10)
    c21 := Cat{"tom", 3}
    c22 := Cat{"jack", 12}

    catChan2 <- &c21
    catChan2 <- &c22

    cc21 := <-catChan2
    cc22 := <-catChan2

    fmt.Println(*cc21, *cc22)

    //5.interface{}
    //创建一个allChan,最多可以存放10个任意数据类型变量,演示写入和读取的用法
    allChan := make(chan interface{}, 10)
    a1 := Cat{"tom!", 15}
    a2 := Cat{"lily", 6}
    allChan <- 32
    allChan <- "张强"
    allChan <- a1
    allChan <- a2

    //希望得到管道中的第三个元素,则先将前两个数据推出
    <-allChan
    <-allChan
    a11 := <-allChan

    fmt.Printf("a11=%T,a11=%v\n", a11, a11)
    //系统认为a11是空接口,所以应该是不存在.Name这个字段的,在编译阶段会报错。
    //fmt.Printf("a11.Name=%v\n", a11.Name)
    //使用类型断言处理.Name
    a := a11.(Cat)
    fmt.Printf("a11.Name=%v\n", a.Name)
}
复制代码

5)channel的遍历和关闭

(1)channel的关闭

使用内置函数close可以关闭channel,当channel关闭后,就不能再向channel写数据了,但是仍然可以从该channel读数据。

在内置包builtin的close函数

func close :func close(c chan<- Type)

内建函数close关闭信道,该通道必须为双向的或只发送的。它应当只由发送者执行,而不应由接收者执行,其效果是在最后发送的值被接收后停止该通道。在最后的值从已关闭的信道中被接收后,任何对其的接收操作都会无阻塞的成功。对于已关闭的信道,语句:x, ok := <-c 还会将ok置为false。

复制代码
func main() {
    //1.int
    //创建一个intchan,最多可以存放3个int,演示存3数据到intChan,然后再去除三个int
    var intChan chan int
    intChan = make(chan int, 3)

    intChan <- 10
    intChan <- 20
    intChan <- 30
    close(intChan)
    intChan <- 40
    n1 := <-intChan
    n2 := <-intChan
    n3 := <-intChan

    fmt.Println(n1, n2, n3)
} //panic: send on closed channel
复制代码

(2) channel的遍历

channel支持for-range的方式进行遍历(不能使用一般for循环是因为一般for循环len()为长度每次取出数据来长度会减少,所以若是100个数据,最后只能取出50个数据来),请注意两个细节。

①在遍历时,如果channel没有关闭,则会出现deadlock的错误

②在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历。

复制代码
func main() {
    intChan2 := make(chan int, 100)
    for i := 0; i < 100; i++ {
        intChan2 <- i * 2
    }
    //遍历管道不能使用普通的for循环
    //for i :=0; i < len(intChan2); i++{
    //
    //}
    //在遍历时,如果channel没有关闭,则会出现deadlock的错误
    //在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历
    close(intChan2)
    //使用for-range进行管道的遍历,管道只返回一个参数,而不会返回下标参数,因为下标是不可变的
    for v := range intChan2 {
        fmt.Println("v=", v)
    }
}
复制代码

(3) goruntine和channel结合使用

解决主进程退出时机问题,避免协程没完成而主进程退出从而引起协程也退出的问题。

思路:设置两个管道,一个管道是写入和读取,另一个管道在读取完成后写入一个true,在主进程读取这个管道,若读取到false即管道关闭了,则退出主进程,此时协程也已经完成。

判断是否完成时不看exitChan的值是否为true,而是看v,ok中的ok是否为false,当管道关闭时,会给ok赋值为false,此时标志读取协程readData完成,主进程退出

复制代码
package main

import (
    "fmt"
)

func writeData(intChan chan int) {
    for i := 0; i < 50; i++ {
        intChan <- i
        fmt.Printf("writeData写入数据=%v\n", i)
    }
    close(intChan) //关闭
}

func readData(intChan chan int, exitChan chan bool) {
    for {
        v, ok := <-intChan
        if !ok {
            break
        }
        fmt.Printf("readData读到数据=%v\n", v)
    }
    //读取完成后,即任务完成,赋值管道exitData为true,传递给主进程,使主进程关闭
    exitChan <- true
    close(exitChan)

}

func main() {
    intChan := make(chan int, 50)
    exitChan := make(chan bool, 1)
    go writeData(intChan)
    go readData(intChan, exitChan)
    for {
        _, ok := <-exitChan
        if !ok {
            break
        }
    }
}
复制代码

(4)管道阻塞

当管道设置的个数比较小,而写入的数据比较多,那么当有读取的动作,即使读的很慢也会进行等待,如果没有读取的动作而只有写入的动作,那么就会发生死锁

如果,编译器(运行),发现一个管道只有写没有读,则该管道会阻塞。如果管道有写有°,但是写管道和读管道的频率不一致,不会发生阻塞。 
案例:
需求:要求统计1-8000的数字中,哪些是素数。要求使用一个协程写入8000个数,使用4个协程来判断统计素数。

 

复制代码
package main

import (
    "fmt"
    "time"
)

func writeData(intChan chan int) {
    for i := 1; i <= 8000; i++ {
        intChan <- i
    }
    close(intChan)
}
func primeData(intChan chan int, primeChan chan int, exitChan chan bool) {
    for {
        flag := true
        v, ok := <-intChan
        if !ok {
            break
        }
        for i := 2; i < v; i++ {
            if v%i == 0 {
                flag = false
                break
            }
        }
        if flag {
            primeChan <- v
        }
    }
    fmt.Println("有一个primeData协程因为取不到数据,退出")
    //这里还不能关闭primeChan,因为有4个协程
    exitChan <- true
}
func main() {
    intChan := make(chan int, 1000)
    primeChan := make(chan int, 2000)
    exitChan := make(chan bool, 4)
    start := time.Now().UnixMicro()
    //开启写入数据的协程
    go writeData(intChan)
    //开启判断素数的协程
    for i := 0; i < 4; i++ {
        go primeData(intChan, primeChan, exitChan)
    }
    //使用匿名函数起一个协程来判断4个primeChan协程是否完成
    go func() {
        for i := 0; i < 4; i++ {
            <-exitChan
        }
        close(primeChan)
        end := time.Now().UnixMicro()
        fmt.Println("使用协程的时间是", end-start)
    }()

    //遍历primeNum,把结果取出
    //channel如果只存不取,管道满了,存后面的数据会报错
    //需要在这判断primeChan是否完成,进行主线程的等待
    for {
        _, ok := <-primeChan
        if !ok {
            break
        }
        //fmt.Printf("primeData()v=%v\n", res)
    }

    fmt.Println("主线程结束")
}
复制代码

(5)注意事项和细节

①管道可以声明为只读或者只写

在默认情况下,管道是双向的

car chan1 chan int //可读可写

声明为只写

var chan2 chan<- int

声明为只读

var chan3 <-chan int 

②使用select可以解决从管道取数据的阻塞问题

复制代码
    label := false
    for {
        select {
        case v := <-intChan:
            fmt.Printf("intChan v=%v\n", v)
        case v := <-stringChan:
            fmt.Printf("stringChan v=%v\n", v)
        default:
            fmt.Printf("找不到数据,退出")
            label = true
            //return
        }
        if label == true {
            break
        }
    }
复制代码

③goruntine中使用recover,解决协程中出现的panic,导致程序崩溃问题

复制代码
package main

import (
    "fmt"
    "time"
)

func test1() {
    for i := 0; i < 10; i++ {
        fmt.Printf("test1()v=%v\n", i)
    }
}
func test2() {
    //这里可以使用defer + recover
    defer func() {
        //捕获test2抛出的panic
        if err := recover(); err != nil {
            fmt.Println("test()发生错误", err)
        }
    }()
    var m1 map[int]string
    m1[0] = "hello" //没有make,erro
}
func main() {
    go test1()
    go test2()
    time.Sleep(time.Second)
}
复制代码

 

 

 

 

 

posted @   潇潇暮鱼鱼  阅读(24)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
历史上的今天:
2022-03-16 java项目报错javax.xml.bind不存在
点击右上角即可分享
微信分享提示