go笔记(十五):并发编程(代码片段)

无虑的小猪 无虑的小猪     2023-05-06     556

关键词:

一、协程的创建

  Go 语言支持并发,只需要通过 go 关键字来开启 goroutine(协程) 即可。

  goroutine(协程) 是轻量级线程,goroutine(协程) 的调度是由 Golang 运行时进行管理的。

goroutine 语法格式(创建协程):

go 函数名( 参数列表 )

示例代码如下:

 package main
 import (
     "fmt"
     "time"
 )
 func rest(msg string) 
     for i := 0; i < 4; i++ 
         fmt.Println(msg)
         time.Sleep(100 * time.Millisecond)
     
 
 func main() 
     // 开启一个goroutine 协程运行
     go rest(" go rest ")
     // main主线程运行
     rest(" main rest ")
 

执行结果如下:

  

二、协程间的同步

2.1、WaitGroup

  WaitGroup,和Java中的CountDownLatch实现原理相似,WaitGroup持有当前正在执行的协程数量,当某个协程执行完后,WaitGroup持有正在执行的协程数量减1,当WaitGroup中协程正执行的数量为0,wait()方法便不再阻塞。

示例代码如下:

 package main
 
 import (
     "fmt"
     "sync"
 )
 
 var wg sync.WaitGroup
 
 func routine01(num int) 
     // goroutine结束登记-1
     defer wg.Done()
     fmt.Printf("goroutine runing, %v\\n", num)
 
 
 func main() 
     for i := 0; i < 8; i++ 
         // 启动一个goroutine就加1
         wg.Add(1)
         go routine01(i)
     
     // 阻塞,等待所有登记的goroutine结束
     wg.Wait()
 

执行结果如下: 

  

2.2、Mutex互斥同步

 package main
 import (
    "fmt"
    "sync"
    "time"
 )
 var m = 100
 var n = 100
 var lock sync.Mutex
 var wagp sync.WaitGroup
 // 增量函数
 func add() 
    defer wagp.Done()
    n += 1
    time.Sleep(time.Millisecond * 8)
 
 // 减量函数
 func sub() 
    time.Sleep(time.Millisecond * 3)
    defer wagp.Done()
    n -= 1
 
 // 未用mutex同步锁
 func nomutex() 
    for i := 0; i < 100; i++ 
       go add()
       wagp.Add(1)
       go sub()
       wagp.Add(1)
    
 
    wagp.Wait()
 
 // 用mutex同步锁
 func addMetux() 
    defer wagp.Done()
    lock.Lock()
    m += 1
    time.Sleep(time.Millisecond * 10)
    lock.Unlock()
 
 // 用mutex同步锁
 func subMetux() 
    defer wagp.Done()
    lock.Lock()
    time.Sleep(time.Millisecond * 3)
    m -= 1
    lock.Unlock()
 
 // 使用mutex同步锁
 func mutex01() 
    for i := 0; i < 100; i++ 
       go addMetux()
       wagp.Add(1)
       go subMetux()
       wagp.Add(1)
    
 
    wagp.Wait()
 
 func main() 
    nomutex()
    mutex01()
    fmt.Printf("end n: %v\\n", n)
    fmt.Printf("end m: %v\\n", m)
 

执行结果如下:

 

  mutex,和Java中的Lock实现原理相似。当执行lock()方法时,基于 CAS 操作,将Mutex的 state 设置为Metux为加锁状态,同时设置当前线程持有锁资源;执行Unlock()方法时,对state的值做调整,释放锁资源,唤醒等待线程。 

  

  

三、协程管理相关包

  协程管理在api-runtime的相关包下。

1、runtime.Gosched()

  让出CPU时间片,重新等待安排任务。

 package main
 
 import (
    "fmt"
    "runtime"
 )
 
 func show(msg string) 
    for i := 0; i < 3; i++ 
       fmt.Printf("msg: %v\\n", msg)
    
 
 
 func main() 
    go show("php")
    // 主协程
    for i := 0; i < 2; i++ 
       // 让出cpu给子协程
       runtime.Gosched() // 若此处注释,则子协程可能无法执行
       fmt.Printf("%v\\n", "golang")
    
 

  执行结果如下:

  

2、runtime.Goexit()

  退出子协程。

示例如下:

 package main
 
 import (
    "fmt"
    "runtime"
 )
 
 func show01(msg string) 
    for i := 0; i < 8; i++ 
       if i == 3 
          // 退出子协程
          runtime.Goexit()
       
       fmt.Printf("msg: %v\\n", msg)
    
 
 
 func main() 
    go show01("C")
    for i := 0; i < 1; i++ 
       runtime.Gosched()
       fmt.Printf("%v\\n", "golang")
    
 

执行结果如下:

  

四、select

  select是Go中并发编程的控制语句,用于处理异步IO操作。select会监听case语句中channel的读写操作,当channel为非阻塞状态(可读写),会触发select中的case。

1、select的语法结构

select 
    case 读:
        // do something
    case 写:
        // do something
    default:
         // do something

  若多个case都可运行,select随机选择一个执行,其他不会执行。没有可运行的case语句,有default语句,会执行default。

2、示例

 package main
 
 import "fmt"
 
 // 创建通道
 var chint = make(chan int, 5)
 var chstr = make(chan string, 5)
 
 func main() 
    go func() 
       // 关闭通道
       defer close(chint)
       defer close(chstr)
       // 往通道里写数据
       chint <- 10
       chstr <- "chstr"
    ()
 
    for i := 0; i < 10; i++ 
       // 监听case语句中channel的读写操作,当channel为非阻塞状态(可读写),会触发select中的case
       select 
          case i := <-chint:
             fmt.Printf("i: %v\\n", i)
          case s := <-chstr:
             fmt.Printf("s: %v\\n", s)
          default:
             fmt.Printf("default...\\n")
          
    
 

  执行结果如下:

  

3、select注意事项

  没有可运行的case语句,没有default语句,select会阻塞直到某个case通信可运行;

  select中的case语句必须是一个channel操作;

  select的default总是可运行的。

五、定时器Timer

  定时器Timer只执行一个。

1、timer的创建

timer := time.NewTimer(time.Second)
timer.C // 阻塞,直到指定时间到了

示例如下:

 package main
 import (
    "fmt"
    "sync"
    "time"
 )
 var wait sync.WaitGroup
 // 使用定时器
 func time01() 
    defer wait.Done()
    fmt.Printf("before: %v\\n", time.Now())
    timer := time.NewTimer(time.Second * 2)
    <-timer.C // 阻塞,直到指定时间到达
    fmt.Printf("after: %v\\n", time.Now())
 
 func main() 
    wait.Add(1)
    go time01()
    // 同步等待协程执行完成
    wait.Wait()
 

执行结果如下:

  

2、time.After

  time.After,和Java中的Sleep功能相似,阻塞指定时间再运行。

示例如下:

 package main
 import (
    "fmt"
    "sync"
    "time"
 )
 var wait sync.WaitGroup
 
 // 使用定时器
 func time02() 
    defer wait.Done()
    fmt.Printf("before: %v\\n", time.Now())
    // 阻塞2s
    <-time.After(time.Second * 2)
    fmt.Printf("after: %v\\n", time.Now())
 
 
 func main() 
    wait.Add(1)
    go time02()
    // 同步等待协程执行完成
    wait.Wait()
 

执行结果如下:

  

3、timer.stop

  停止定时器事件。

示例如下:

 package main
 import (
    "fmt"
    "time"
 )
 
 func main() 
    // 创建定时器
    timer := time.NewTimer(time.Second * 2)
    // 协程,匿名函数
    go func() 
       <-timer.C
       fmt.Println("timer func...")
    ()
 
    // 停止定时器,阻止timer事件发生
    stopFlag := timer.Stop()
    if stopFlag 
       fmt.Println("timer stoped...")
    
 

执行结果如下:

  

4、timer.Reset

  重置定时器。

示例如下:

 package main
 import (
    "fmt"
    "time"
 )
 
 func main() 
    // 重置定时器 reset
    fmt.Printf("before: %v\\n", time.Now())
    timer := time.NewTimer(time.Second * 2)
    timer.Reset(time.Second * 1)
    <-timer.C
    fmt.Printf("after: %v\\n", time.Now())
 

执行结果如下:

  

六、周期执行器Ticker

  与timer只执行一次相比,Ticker可周期执行。

示例如下:

 package main
 import (
    "fmt"
    "time"
 )
 func main() 
    // 创建周期执行器
    ticker := time.NewTicker(time.Second * 2)
 
    // 周期执行
    for _ = range ticker.C 
       fmt.Printf("time: %v\\n", time.Now())
    
 

执行结果如下:

  

 

 package main
 import (
    "fmt"
    "time"
 )
 
 func ticker() 
    // 创建一个无缓冲的整型channel
    chint := make(chan int)
    // 关闭通道
    defer close(chint)
    // 创建定时器
    ticker := time.NewTicker(time.Second * 2)
    // 创建一个协程,利用周期定时器每2s向通道中写入数据
    go func() 
       for _ = range ticker.C 
          select 
             case chint <- 2:
             case chint <- 4:
             case chint <- 6:
          
       
    ()
    sum := 0
    // 遍历通道,若通道无数据,会阻塞
    for v := range chint 
       fmt.Printf("receive: %v\\n", v)
       sum += v
       if sum > 20 
          fmt.Printf("sum: %v\\n", sum)
          break
       
    
 
 
 func main() 
    ticker()
 

执行结果如下:

  

七、atomic

  atomic的原子操作可以保证任一时刻只有一个goroutine协程对变量做修改。

1、加减

 package main
 import (
    "fmt"
    "sync"
    "sync/atomic"
 )
 var i int32 = 100
 var waitg sync.WaitGroup
 // 原子加操作
 func atomicAdd() 
    atomic.AddInt32(&i, 1)
    waitg.Done()
 
 // 原子减操作
 func atomicSub() 
    atomic.AddInt32(&i, -1)
    waitg.Done()
 
 func main() 
    for i := 0; i < 100; i++ 
       // 子线程
       waitg.Add(1)
       go atomicAdd()
       // 子线程
       waitg.Add(1)
       go atomicSub()
    
    waitg.Wait()
    fmt.Printf("i = %v\\n", i)
 

  执行结果如下:

  

2、载入 -> 读取操作、存储 -> 写入操作

 package main
 import (
    "fmt"
    "sync/atomic"
 )
 // 载入与存储
 func loadAndStore() 
    var i int64 = 64
    // 载入 -> 读取,原子操作
    atomic.LoadInt64(&i)
    fmt.Printf("i: %v\\n", i)
 
    // 存储 -> 写入,原子操作
    atomic.StoreInt64(&i, 128)
    fmt.Printf("i: %v\\n", i)
 
 func main() 
    loadAndStore()
 

  执行结果如下:

  

3、CAS -> 比较并交换

  进行交换前变量的值未被修改,与参数old记录的值一致,满足此前提下才会进行交换。

 package main
 import (
    "fmt"
    "sync/atomic"
 )
 // 比较和交换
 func cas() 
    var i int64 = 256
    // 旧的值与变量i的值相同,则交换
    result := atomic.CompareAndSwapInt64(&i, 256, 64)
    if result 
       fmt.Println("cas success")
     else 
       fmt.Println("cas fail")
    
    fmt.Printf("i: %v\\n", i)
 
    // 旧的值与变量i的值不同,则不交换
    result01 := atomic.CompareAndSwapInt64(&i, 256, 8)
    if result01 
       fmt.Println("cas success")
     else 
       fmt.Println("cas fail")
    
    fmt.Printf("i: %v\\n", i)
 
 func main() 
    cas()
 

  执行结果如下:

  

 

go语言学习笔记—进阶—并发编程:调整并发的运行性能(gomaxprocs)(代码片段)

go语言的运行时(runtime)实现了一个小型任务调度器,它类似于操作系统的线程调度,可以高效地把CPU资源分配给每一个任务。通过runtime.GOMAXPROCS()函数,可以指定线程池中线程与CPU核心数量的对应关系,如下:... 查看详情

go语言学习笔记—进阶—并发编程:同步sync,竞态检测——检测代码在并发环境下出现的问题(代码片段)

使用channel在多个goroutine之间交换数据,只是数据同步方法的一种。通道内部的实现依然使用各种锁。优雅代码的代行是性能。在某些轻量级场合,原子访问(atomic包)、互斥锁(sysnc.Mutex)以及等待组࿰... 查看详情

go语言学习笔记—进阶—并发编程:通道(channel)——在多个goroutine之间通信的管道(代码片段)

单纯地并发执行函数是无意义的,函数之间需要交换数据才能体现并发执行的意义。虽然可以使用“共享内存”的方式交换数据,但共享内存在不同goroutine间易发生竞态问题。为了保证数据交换的正确性,必须使用互... 查看详情

go语言学习笔记—进阶—并发编程:通道(channel)——使用channel收发数据(代码片段)

创建channel之后,我们可以使用channel进行数据收发操作。使用channel发送(填充)数据使用特殊的操作符<-,把数据通过channel发送。格式通道变量<-值通道变量是上文通过make创建的通道实例;值可以是变量... 查看详情

go语言学习笔记—进阶—并发编程:通道(channel)——各种各样的通道(代码片段)

单向通道在声明通道时,我们可以设置只发送或只接收。这种被约束操作方向的通道称为单向通道。声明单向通道只发送:chan<-,只接收:<-chanvar通道实例chan<-元素类型//只发送数据var通道实例<-chan元素... 查看详情

go语言学习笔记—进阶—并发编程(11):同步sync,等待组(sync.waitgroup)——保证在并发环境中完成指定数量的任务(代码片段)

除了使用通道channel和互斥锁sync.Mutex进行两个并发程序间的同步,我们还可以使用等待组(sync.WaitGroup)进行多个任务之间的同步。方法名功能(wg*WaitGroup)Add(deltaint)等待组的计数器+1(wg*WaitGroup)Done()等待组的计数器-1(... 查看详情

go基础并发编程(代码片段)

并发编程并发编程Go并发的设计相关概念启动协程同步通道channel创建channelchannel的读写单方向channel定时器相关资料Go并发的设计  Go语言最大的特色是并发,而且Go的并发并不像线程或进程那样,受CPU核心数的限制,... 查看详情

go语言学习笔记—进阶—并发编程:互斥锁(sync.mutex)——保证同时只有一个goroutine可以访问共享资源(代码片段)

互斥锁(英语:Mutualexclusion,缩写Mutex)是一种在多线程编程中,防止两条线程同时对同一共享资源(比如全局变量)进行读写的机制。互斥锁通过把代码切片成一个个的临界区域(criticalsection࿰... 查看详情

go语言系列之并发编程(代码片段)

Go语言中的并发编程并发与并行并发:同一时间段内执行多个任务(你在用微信和两个女朋友聊天)。并行:同一时刻执行多个任务(你和你朋友都在用微信和女朋友聊天)。Go语言的并发通过goroutine实现。goroutine类似于线程,... 查看详情

go并发编程模型(代码片段)

一、前言Go语言中实现了两种并发模型,一种是依赖于共享内存实现的线程-锁并发模型,另一种则是CSP(CommunicationingSequentialProcesses,通信顺序进程)并发模型。大多数编程语言(比如C++、Java、Python... 查看详情

学习笔记hadoop(十五)——mapreduce编程进阶(代码片段)

文章目录一、输出文件格式及序列化文件生成1.1、输出文件格式1.2、设置输出SequenceFileOutputFormat文件格式二、输入文件格式及序列化文件读取2.1、输入数据文件类型2.2、设置输入SequenceFileInputFormat文件格式三、使用Partitioner优化... 查看详情

068-go并发编程(代码片段)

并发编程的难点在于异常处理。今天我们继续研究缩略图的并发编程,还记得之前留下的问题吗?我们的程序没有对程序返回的错误做特殊照顾。在服务器开发领域,这样的程序的显然不够健壮。1.让程序能够处理错... 查看详情

go并发编程基础-channel(代码片段)

...runtime所调度,这一点和线程不一样。也就是说,Go语言的并发是由Go自己所调度的,自己决定同时执行多少个goroutine,什么时候执行哪几个。这些对于我们开发者来说完全透明,只需要在编码的时候告诉Go语 查看详情

go语言并发编程(代码片段)

并发编程基本概念学习并发编程之前我们需要脑补几个基础知识和思考一个问题什么是串行?什么是并行?什么是并发?什么是程序?什么是进程?什么是线程?什么是协程?什么是串行?串行就是按顺序执行,就好比银行只有1个窗口,有3个... 查看详情

使用go语言实现高效的并发编程(代码片段)

文章目录概述举个例子使用并发编程来实现简单的任务处理使用同步锁来避免竞态条件使用信道来协调多个goroutine之间交互总结概述Go语言支持并发编程。你可以通过创建多个并发单元(称为goroutines)来实现多线程编程... 查看详情

使用go语言实现高效的并发编程(代码片段)

文章目录概述举个例子使用并发编程来实现简单的任务处理使用同步锁来避免竞态条件使用信道来协调多个goroutine之间交互总结概述Go语言支持并发编程。你可以通过创建多个并发单元(称为goroutines)来实现多线程编程... 查看详情

java并发编程(十五):countdownlatch源码逐行深度分析(代码片段)

前言  CountDownLatch维护了一个计数器(还是是state字段),调用countDown方法会将计数器减1,调用await方法会阻塞线程直到计数器变为0。可以用于实现一个线程等待所有子线程任务完成之后再继续执行的逻辑,... 查看详情

067-go并发编程(代码片段)

接下来了几篇文章,我们会重点讨论有关Go的并发编程以及常见的技术手段。当然,所有问题我们都需要从实例出发,所有的实例均来源于《TheGoProgrammingLanguage》一书的第8章。1.缩略图计算程序这个例子的目的很简单&... 查看详情