第十六章 Golang中goroutine和channel

简介: 第十六章 Golang中goroutine和channel

1.goroutine-看一个需求

需求:要求统计1-900000000的数字中,那些是素数?

分析:

  1. 传统方法,就是使用一个循环,循环的判断各个数是不是素数。
  2. 使用并发或并行的方式,将统计素数的任务分配给多个goroutine去完成,这时就会使用到goroutine。

2.进程和线程介绍

  1. 进程就是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位
  2. 线程是进程的一个执行实例,是程序执行的最小单位,它是比进程更小的能独立运行的基本单位。
  3. 一个进程可以创建和销毁多个线程,同一个进程中的多个线程可以并发执行
  4. 一个程序至少有一个进程,一个进程至少有一个线程

3. 并发和并行

  1. 多线程程序在单核上运行,就是并发
  2. 多个程程序在多核上运行,就是并行

并发:因为是在一个CPU上,比如有10个线程,每个线程执行10毫秒(进行轮询操作),从人的角度看,好像这10个线程都在运行,但是从微观上看,在某一个时间点看,其实只有一个线程在执行,这就是并发。

并行:因为是在多个CPU上(比如有10个CPU),比如有10个线程,每个线程执行10毫秒(各自在不同CPU上执行),从人的角度看,这10个线程都在运行,但是从微观上看,在某一个时间点看,也同时有10个线程在执行,这就是并行

4.Go协程和Go主线程

Go主线程(有程序员直接称为线程/也可以理解成进程):一个Go线程上,可以起多个携程,你可以这样理解,携程是轻量的线程

Go协程的特点

有独立的栈空间

共享程序堆空间

调度由用户控制

携程是轻量级的线程

案例说明

请编写一个程序,完成如下功能:

1.在主线程(可以理解成进程)中,开启一个goroutine,该携程每隔1秒输出“hello,world”

2.在主线程中也每隔一秒输出“hello,golang”,输出10次后,退出程序

3.要求主线程和goroutine同时执行

4.画出主线程和协程执行流程图

代码实现

// 在主线程(可以理解成进程)中,开启一个goroutine,该协程每秒输出 “hello,world”
// 在主线程中也每隔一秒输出“hello,golang”,输出10次后,退出程序
// 要求主线程和goroutine同时执行
//编写一个函数,每隔1秒输出 “hello,world”
func test(){
   for i := 1;i<=10;i++{
    fmt.Println("test() hello,world"+strconv.Itoa(i))
    time.Sleep(time.Second)
  }
}
func main(){
    go test() // 开启了一个协程
    for i:=1;i<=10;i++{
    fmt.Println(" main() hello,golang"+strconv.Itoa(i))
    time.Sleep(time.Second)
  }
}

总结

  1. 主线程是一个物理线程,直接作用在CPU上的,是重量级的,非常耗费CPU资源。
  2. 协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对少。
  3. Golang的协程机制是重要的特点,可以轻松的开启上万个协程。其他编程语言的并发机制是一般基于线程的,开启过多的线程,资源耗费大,这里就突显Golang在并发上的优势了

MPG模式基本介绍

M:操作系统的主线程(是物理线程)

P:协程执行需要的上下文

G:协程

5.设置Golang运行的CPU数

介绍:为了充分利用多CPU的优势,在Golang程序中设置运行的CPU数目

package main
 import "fmt"
 import "runtime"
 
func main(){
  // 获取当前系统CPU的数量
  num := runtime.NumCPU()
  // 这里设置num-1的CPU运行go程序
  runtime.GOMAXPROCS(num)
  fmt.Println("num=",num)
}
  1. go1.8后,默认让程序运行在多个核上,可以不用设置了
  2. go1.8前,还是要设置一下,可以更高效的利用CPU

6.channel(管道)-看个需求

需求:现在要计算 1-200的各个数的阶乘,并且把各个数的阶乘放入到map中。最后显示出来。要求使用goroutine完成

分析思路:

使用goroutine来完成,效率高,但是会出现并发/并行安全问题

这里就提出了不同goroutine如何通信的问题

代码实现

使用goroutine来完成(看看使用gorotine并发完成会出现什么问题?然后我们会去解决)

在运行某个程序时,如何知道是否存在资源竞争问题,方法很简单,在编译该程序时,增加一个参数 -race即可

不同goroutine之间如何通讯

1.全局变量的互斥锁

2.使用管道channel来解决

使用全局变量加锁同步改进程序

  1. 英文没有对全局变量m加锁,因此会出现资源争夺问题,代码会出现错误,提示concurrent map writes
  2. 解决方案:加入互斥锁
  3. 我们的数的阶乘很大,结果会越界,可以将求阶乘改成sum += uint64(i)

源码

package main
import (
  "fmt"
  "time"
  "sync"
)
// 需求:现在要计算 1-200的各个数的阶乘,并且把各个数的阶乘放入到map中
// 最后显示出来。要求使用goroutine完成
// 思路
// 1. 编写一个函数,来计算各个数的阶乘,并放入到map中
// 2. 我们启动的协程多个,统计的将结果放入到map中
// 3. map应该做出一个全局的
var (
  myMap = make(map[int]int,10)
  // 声明一个全局的互斥锁
  // lock 是一个全局的互斥锁
  //sync 是包:synchornized 同步
  // Mutex: 是互斥
  lock sync.Mutex
)
// test函数就是计算n!,让将这个结果放入到myMap
func test(n int){
  res := 1
  for i := 1;i<=n;i++{
    res *= i
  }
  // 这里我们将res放入到myMap
  // 加锁
  lock.Lock()
  myMap[n] = res  // concurrent map writes?
  // 解锁
  lock.Unlock()
}
func main(){
  // 我们这里开启多个协程完成这个任务[200个]
  for i := 1;i<=20;i++{
    go test(i)
  }
  
  // 休眠10秒钟【第二个问题】
  time.Sleep(time.Second * 10)
  lock.Lock()
  // 这里我们输出结果 变量这个结果
  for i,v := range myMap{
    fmt.Printf("map[%d]=%d\n",i,v)
  } 
  lock.Unlock()
}

channel(管道)-基本使用

channel初始化

说明:使用make进行初始化

var intChan chan int

intChan = make(chan int,10)

向channel中写入(存放)数据

var intChan chan int

intChan = make(chan int,10)

num := 999

intChan <-10

intChan <-num

管道的初始化,写入数据到管道,从管道读取数据及基本的注意事项

package main
import (
  "fmt"
)
func main(){
  // 演示一下管道的使用
  // 1.创建一个可以存放3个int类型的管道
  var intChan chan int
  intChan = make(chan int,3)
  
  // 2.看看intChannel是什么
  fmt.Printf("intChan 的值=%v intChan本身的地址=%p\n",intChan,&intChan)
  
  // 3.向管道写入数据
  intChan<- 10
  num := 211
  intChan<- num
  
  // 注意点,当我们给管写入数据时,不能超过其容量
  intChan<- 50
  // intChan<- 98
  
  //4. 看看管道的长度和cap(容量)
  fmt.Printf("channel len=%v cap=%v \n",len(intChan),cap(intChan)) // 2,3
  
  // 5.从管道中读取数据
  var num2 int 
  num2 = <-intChan
  fmt.Println("num2=",num2)
  fmt.Printf("channel len=%v cap=%v \n",len(intChan),cap(intChan)) // 2,3
  
  // 6.在没有使用协程的情况下,如果我们的管道数据已经全部取出,再取就会报告 deadlock
  num3 := <-intChan
  num4 := <-intChan
  // num5 := <-intChan
  
  fmt.Println("num3=",num3,"num4=",num4)//,"num5=",num5)
}

channel使用的注意事项

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

2.channel的数据放满后,就不能再放入了

3.如果从channel取出数据后,可以继续放入

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

示例代码

package main
import (
  "fmt"
)
type Cat struct{
  Name string
  Age int
}
func main(){
  // 定义一个存放任意数据类型的管道  3个数据
  // var callChan chan interface{}
  allChan := make(chan interface{},3)
  
  allChan<- 10
  allChan<- "tom jack"
  cat := Cat{"小花猫",4}
  allChan<- cat
  
  // 我们希望获得到管道中的第三个元素,则先将前2个推出
  <-allChan
  <-allChan
  
  newCat := <-allChan // 从管道中取出的Cat是什么?
  
  fmt.Printf("newCat=%T,newCat=%v\n",newCat,newCat)
  // 下面的写法是错误的!编译不通过
  // fmt.Printf("newCat.Name=%v",newCat.Name)
  // 使用类型断言
  a := newCat.(Cat)
  fmt.Printf("newCat.Name=%v",a.Name)
}

channel的关闭

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

channel的遍历

channel支持for-range的方式进行遍历,请注意两个细节

  1. 在遍历时,如果channel没有关闭,则会出现deadlock的错误
  2. 在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历。

代码演示:

package main
import (
  "fmt"
)
func main(){
  intChan := make(chan int,3)
  intChan<- 100
  intChan<- 200
  close(intChan) // close
  // 这是不能够再写入到channel
  // intChan<-300
  fmt.Println("okook~")
  // 当管道关闭后,读取数据是可以的
  n1 := <-intChan
  fmt.Println("n1=",n1)
  
  // 遍历管道
  intChan2 := make(chan int,100)
  for i := 0; i< 100;i++{
    intChan2<-i*2 // 放入100个数据到管道
  }
  
  // 遍历管道不能使用普通的for循环
  
  // 在遍历时,如果channel没有关闭,则会出现deadlock的错误
  // 在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历
  close(intChan2)
  for  v := range intChan2{
    fmt.Println("v=",v)
  }
  
}

感谢大家观看,我们下次见

目录
相关文章
|
7月前
|
存储 监控 Linux
Golang 语言的 goroutine 调度器模型 GPM
Golang 语言的 goroutine 调度器模型 GPM
43 0
|
9天前
|
监控 Go 开发者
Golang深入浅出之-Goroutine泄漏检测与避免:pprof与debug包
【5月更文挑战第2天】本文介绍了Go语言并发编程中可能遇到的Goroutine泄漏问题,以及如何使用`pprof`和`debug`包来检测和防止这种泄漏。常见的问题包括忘记关闭channel和无限制创建goroutine。检测方法包括启动pprof服务器以监控Goroutine数量,使用`debug.Stack()`检查堆栈,以及确保每个Goroutine有明确的结束条件。通过这些手段,开发者可以有效管理Goroutine,维持程序性能。
33 7
|
10天前
|
Java Go
Golang深入浅出之-Goroutine泄漏检测与避免:pprof与debug包
【5月更文挑战第1天】本文介绍了Go语言中goroutine泄漏的问题及其影响,列举了忘记关闭通道、无限循环和依赖外部条件等常见泄漏原因。通过引入`net/http/pprof`和`runtime/debug`包,可以检测和避免goroutine泄漏。使用pprof的HTTP服务器查看goroutine堆栈,利用`debug`包的`SetGCPercent`和`FreeOSMemory`函数管理内存。实践中,应使用`sync.WaitGroup`、避免无限循环和及时关闭通道来防止泄漏。理解这些工具和策略对维护Go程序的稳定性至关重要。
23 4
|
16天前
|
安全 Go 开发者
Golang深入浅出之-Go语言并发编程面试:Goroutine简介与创建
【4月更文挑战第22天】Go语言的Goroutine是其并发模型的核心,是一种轻量级线程,能低成本创建和销毁,支持并发和并行执行。创建Goroutine使用`go`关键字,如`go sayHello(&quot;Alice&quot;)`。常见问题包括忘记使用`go`关键字、不正确处理通道同步和关闭、以及Goroutine泄漏。解决方法包括确保使用`go`启动函数、在发送完数据后关闭通道、设置Goroutine退出条件。理解并掌握这些能帮助开发者编写高效、安全的并发程序。
23 1
|
7月前
|
Go
Golang 语言怎么控制并发 goroutine?
Golang 语言怎么控制并发 goroutine?
19 0
|
7月前
|
存储 SQL 安全
Golang 语言标准库 context 包控制 goroutine
Golang 语言标准库 context 包控制 goroutine
29 0
|
10月前
|
Go 调度
Golang的goroutine原理介绍
Golang的goroutine原理介绍
|
10月前
|
缓存 并行计算 算法
Golang面试前三夜准备:Goroutine调度器GMP为何需要P?不需要会怎样?
Golang面试前三夜准备:Goroutine调度器GMP为何需要P?不需要会怎样?
|
10月前
|
Go
golang踩坑 2.goroutine闭包和遍历字符串字符
golang踩坑 2.goroutine闭包和遍历字符串字符
|
12月前
|
存储 Go
Golang中的管道(channel) 、goroutine与channel实现并发、单向管道、select多路复用以及goroutine panic处理
Golang中的管道(channel) 、goroutine与channel实现并发、单向管道、select多路复用以及goroutine panic处理
399 0