golang学习笔记(并发编程)

协程goroutine

一个 goroutine 本身就是一个函数,当你直接调用时,它就是一个普通函数,如果你在调用前加一个关键字 go ,那你就开启了一个 goroutine。

//执行一个函数
func()
//开启一个协程执行这个函数
go func()

main相当于主线程,goroutine要在主线程结束前执行完,否者协程总结

package main

import "fmt"

func main() {
  go test()
  fmt.Println("hello world!")
}

func test() {
  fmt.Println("hello china")
}

创建协程需要时间,在协程创建完之前,主线程main已经执行完,协程还没来得及执行

多个协程同时执行

package main

import (
  "fmt"
  "time"
)

func main() {
  go assistance("协程1")
  go assistance("协程2")
  go assistance("协程3")
  time.Sleep(time.Second)
}

func assistance(name string)  {
  for i:=0;i<10;i++ {
    fmt.Printf("goroutine %s\n",name)
  }
  time.Sleep(time.Millisecond * 10)
}

信道channel

就是一个管道,连接多个goroutine程序,它是一种队列式的数据结构,遵循先进先出规则

var 信道实例 chan 信道类型
声明后的信道,其零值式nil,无法直接使用,必须make进行初始化
信道实例 = make(chan 信道类型)
上面两句合并成一句
信道实例 := make(chan 信道类型)
信道数据操作
a := make(chan int)
a <- 100//发送数据
<-a//取出数据
close(a)//关闭信道,避免一直等待,关闭收接收方仍然可以从信道中取到数据,知识接收到的永远是0

在这里插入代码片
package main

import "fmt"

func main() {
  //make函数接收两个参数
  //第一个参数必填,指定信道类型
  //第二个选填,不填默认为0,指定信道容量
  a := make(chan int,5)
  //信道容量
  fmt.Printf("信道可缓冲 %d 个数据\n",cap(a))
  a<-1
  //信道长度
  fmt.Printf("信道当中由 %d 个数据\n",len(a))
}

当容量为0时,说明信道中不能存放数据,在发送数据时,必须要求立马有人接收,否则会报错。此时的信道称之为无缓冲信道。

当容量为1时,说明信道只能缓存一个数据,若信道中已有一个数据,此时再往里发送数据,会造成程序阻塞。 利用这点可以利用信道来做锁。

当容量大于1时,信道中可以存放多个数据,可以用于多个协程之间的通信管道,共享资源。

信道分为双向和单项,默认是双向

import (
    "fmt"
    "time"
)

func main() {
    pipline := make(chan int)

    go func() {
        fmt.Println("准备发送数据: 100")
        pipline <- 100
    }()

    go func() {
        num := <-pipline
        fmt.Printf("接收到的数据是: %d", num)
    }()
    // 主函数sleep,使得上面两个goroutine有机会执行
    time.Sleep(time.Second)
}
//单信道可细分为只读信道和只写信道

//只读信道
  var a = make(chan int)
  type receice = <-chan int
  var receiver receice = a
//只写信道
  var b = make(chan int)
  type write = chan<- int
  var writer write = b

几个注意事项

  1. 关闭一个未初始化的 channel 会产生 panic
  2. 重复关闭同一个 channel 会产生 panic
  3. 向一个已关闭的 channel 发送消息会产生 panic
  4. 从已关闭的 channel 读取消息不会产生 panic,且能读出 channel 中还未被读取的消息,若消息均已被读取,则会读取到该类型的零值。
  5. 从已关闭的 channel 读取消息永远不会阻塞,并且会返回一个为 false 的值,用以判断该 channel 是否已关闭(x,ok := <- ch)
  6. 关闭 channel 会产生一个广播机制,所有向 channel 读取消息的 goroutine 都会收到消息
  7. channel 在 Golang 中是一等公民,它是线程安全的,面对并发问题,应首先想到 channel。

WaitGroup

var 实例名 sync.WaitGroup

实例化完成后,可以使用它 的几种方法

  • add:初始值为0,你传入的值会往计数器上加,这里直接传入你子协程的数量
  • Done:当某个子协程完成后,可调用此方法,会从计数器上减一,通常可以使用 defer 来调用。
  • Wait:阻塞当前协程,直到实例里的计数器归零。
package main

import (
  "fmt"
  "sync"
)

func main() {
  var wg sync.WaitGroup
  wg.Add(2)
  go test(&wg)
  go test(&wg)
  fmt.Println("end")
  wg.Wait()
}

func test(wg *sync.WaitGroup)  {
  defer wg.Done()
  for i:=0;i<10;i++ {
    fmt.Println(i)
  }
}

互斥锁与读写锁

Mutex:互斥锁

package main

import (
  "fmt"
  "sync"
)

func main() {
  var wg sync.WaitGroup
  wg.Add(3)
  lock := &sync.Mutex{}
  count := 0
  go test(&count,&wg,lock)
  go test(&count,&wg,lock)
  go test(&count,&wg,lock)
  fmt.Println("end")
  wg.Wait()
  fmt.Println("count:",count)
}

func test(count *int,wg *sync.WaitGroup,lock *sync.Mutex)  {
  defer wg.Done()
  for i:=0;i<1000;i++ {
    lock.Lock()
    *count = *count + 1
    lock.Unlock()
  }
}

使用互斥锁注意点

  • 同一协程里,不要在尚未解锁时再次使加锁
  • 同一协程里,不要对已解锁的锁再次解锁
  • 加了锁后,别忘了解锁,必要时使用 defer 语句

RWMutex:读写锁
它将程序对资源的访问分为读操作和写操作

  • 为了保证数据的安全,它规定了当有人还在读取数据(即读锁占用)时,不允计有人更新这个数据(即写锁会阻塞)
  • 为了保证程序的效率,多个人(线程)读取数据(拥有读锁)时,互不影响不会造成阻塞,它不会像 Mutex 那样只允许有一个人(线程)读取同一个数据。

RWMutex 里提供了两种锁,每种锁分别对应两个方法,为了避免死锁,两个方法应成对出现,必要时请使用 defer。

读锁:调用 RLock 方法开启锁,调用 RUnlock 释放锁

写锁:调用 Lock 方法开启锁,调用 Unlock 释放锁(和 Mutex类似)

package main

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

func main() {
  lock := &sync.RWMutex{}
  lock.Lock()
  for a := 0; a < 4; a++ {
    go func(a int) {
      lock.RLock()
      fmt.Printf("协程 %d 获得读锁\n",a)
      time.Sleep(time.Second * 3)
      lock.RUnlock()
    }(a)
  }

  time.Sleep(time.Second * 2)
  fmt.Println("协程在3秒后释放读锁,将不会再堵塞")
  lock.Unlock()
  // 由于会等到读锁全部释放,才能获得写锁
  // 因为这里一定会在上面 4 个协程全部完成才能往下走
  lock.Lock()
  fmt.Println("程序退出...")
  lock.Unlock()
}

死锁的情况

package main

import "fmt"

func main() {
    pipline := make(chan string)
    pipline <- "hello world"
    fmt.Println(<-pipline)
}
解决此问题有两种方法:

使接收者代码在发送者之前执行

使用缓冲信道,而不使用无缓冲信道
package main

func hello(pipline chan string)  {
    <-pipline
}

func main()  {
    pipline := make(chan string)
    go hello(pipline)
    pipline <- "hello world"
}
这样子还是报错,虽然保证接收代码再发送者之前,
但是由于前面接收者一直再等待数据而处于阻塞状
态,所以无法执行到后面的发送数据,造成死锁,
慎用无缓冲区信道
package main

import "fmt"

func main() {
    ch1 := make(chan string, 1)

    ch1 <- "hello world"
    ch1 <- "hello China"

    fmt.Println(<-ch1)
}
每个缓冲信道都有容量,当数据量等于容量后,
再往信道发送数据,就会造成阻塞,
必须等有人从信道中消费数据后,程序才会往下执行
package main

import "fmt"

func main() {
    pipline := make(chan string)
    go func() {
        pipline <- "hello world"
        pipline <- "hello China"
        // close(pipline)
    }()
    for data := range pipline{
        fmt.Println(data)
    }
}
当程序一直在等待从信道里读取数据,
而此时并没有人会往信道中写入数据。
此时程序就会陷入死循环,造成死锁。
只要在发送完数据后,手动关闭信道,
告诉 range 信道已经关闭,无需等待就行。

协程池

池化技术就是利用复用来提升性能的
为了减少线程频繁创建销毁还来的开销,通常我们会使用线程池来复用线程。

package main

import (
  "fmt"
  "time"
)

type Pool struct {
  work chan func()//用于接收task任务
  sem chan struct{}//设置协程池大小,可同时执行的协程数量
}

//创建一个协程池对象
func New(size int) *Pool {
  return &Pool{
    work: make(chan func()),//无缓冲通道
    sem: make(chan struct{},size),//缓冲通道
  }
}
//当第一次调用 NewTask 添加任务的时候,
//由于 work 是无缓冲通道,所以一定会
//走第二个 case 的分支:使用 go worker 开启一个协程。
func (p *Pool) NewTask(task func())  {
  select{
  case p.work<-task:
  case p.sem <- struct{}{}:
    go p.worker(task)
  }
}
//如果传入的任务数大于设定的协程池数,
//并且此时所有的任务都还在运行中,
//那此时再调用 NewTask 传入 task ,
//这两个 case 都不会命中,会一直阻塞
//直到有任务执行完成,worker 函数里的 
//work 通道才能接收到新的任务,继续执行。
func (p *Pool) worker(task func())  {
  defer func() { <-p.sem}()
  for{
    task()
    task = <- p.work
  }
}

func main() {
  pool := New(2)
  for a := 1;a<5;a++ {
    pool.NewTask(func() {
      time.Sleep(time.Second * 2)
      fmt.Println(time.Now())
    })
  }
  time.Sleep(time.Second *5)
}
2021-09-12 10:41:00.299706 +0800 CST m=+2.018351501
2021-09-12 10:41:00.299706 +0800 CST m=+2.018351501
2021-09-12 10:41:02.3198102 +0800 CST m=+4.038455701
2021-09-12 10:41:02.3198102 +0800 CST m=+4.038455701
可以看到总共 4 个任务,由于协程池大小为 2,所以 4 个任务分两批执行

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值