Go语言学习路

Go定时器、select、并发安全、锁、原子操作

2021-08-09  本文已影响0人  TZX_0710

Timer:只执行一次

package main

func main() {
  //只执行一次 则关闭 2秒之后执行
  //timer1 := time.NewTimer(2 * time.Second)
  //t1 := time.Now()
  //fmt.Printf("t1:%v\n", t1)
  //t2 := <-timer1.C
  //fmt.Printf("t2:%v\n", t2)

  //2.验证timer只能响应1次
  //timer2 := time.NewTimer(time.Second)
  //for {
  //  <-timer2.C
  //  fmt.Println("时间到")
  //}

  // 3.timer实现延时的功能
  //(1)
  //time.Sleep(time.Second)
  ////(2)
  //timer3 := time.NewTimer(2 * time.Second)
  //<-timer3.C
  //fmt.Println("2秒到")
  ////(3)
  //<-time.After(2 * time.Second)
  //fmt.Println("2秒到")

  // 4.停止定时器
  //timer4 := time.NewTimer(2 * time.Second)
  //go func() {
  //  <-timer4.C
  //  fmt.Println("定时器执行了")
  //}()
  //b := timer4.Stop()
  //if b {
  //  fmt.Println("timer4已经关闭")
  //}
  // 5.重置定时器
  //timer5 := time.NewTimer(3 * time.Second)
  //timer5.Reset(1 * time.Second)
  //fmt.Println(time.Now())
  //fmt.Println(<-timer5.C)
  //
  //for {
  //}
}

Ticker:可重复执行多次

package main

import (
  "fmt"
  "time"
)

func main() {
  ticker := time.NewTicker(1 * time.Second)
  i := 0
  go func() {
      for  {
          i++
          fmt.Println(<-ticker.C)
      }
  }()
  time.Sleep(time.Minute)
}

select

在某些场景下我们需要从多个通道接受数据。通道在接受数据时,如果没有数据将会发生阻塞。for循环可以实现该需求,但是性能会差很多,为了应对这种场景。Go内置了select关键词。可以同时响应多个通道的操作。

select的使用类似于switch语句,它有一系列case分支和一个默认的分支。每个case会对应一个通道的通信过程。select会一直等待,直到某个case的通信操作完成时,就会执行case分支对应的语句。

select{
    case <-chan1:
      //如果chan1成功读取到数据则执行该case
case  chan2<-1:
      //如果成功向chan2写入数据,则进行该case语句处理
default:
    //如果上面都没成功 则执行该方法
    
}

  • select可以同时监听一个或多个channel,直到其中一个channel ready
package main

import (
  "fmt"
  "time"
)

//单项写入通道
func test(ch chan<- string) {
  time.Sleep(time.Second)
  ch <- "test1"
}
//单项写入通道
func test1(ch chan<- string) {
  time.Sleep(time.Second)
  ch <- "test2"
}

func main() {
  //创建通道
  ch1 := make(chan string)
  //创建通道2
  ch2 := make(chan string)
  go test(ch1)
  go test1(ch2)
  select {
  //如果ch1被写入
  case s1 := <-ch1:
      fmt.Println(s1)
  //如果ch2先被写入
  case s2 := <-ch2:
      fmt.Println(s2)
  }
}
  • 监听通道是否存满
import (
  "fmt"
  "time"
)

func write(ch chan<- string) {
  for {
      select {
      case ch <- "hello":
          fmt.Println("write hello")
      default:
          fmt.Println("full cha")
      }
      time.Sleep(
          time.Millisecond * 500)
  }
}

func main() {
  ch1 := make(chan string, 10)
  go write(ch1)
  //输出通道数据
  for s := range ch1 {
      fmt.Println(s)
      time.Sleep(time.Second)
  }
}

并发安全和锁

有时候在Go代码中可能存在多个goroutine同时操作一个资源,这种情况会发生竞态问题。

如下代码由于两个goroutine同时去修改x的值,该参数值就会存在数据竞争,导致最后的结果与期待的不符

package main

import (
  "fmt"
  "sync"
)

var x int64
var wg sync.WaitGroup

func add() {
  for i := 0; i < 5000; i++ {
      x = x + 1
  }
  wg.Done()
}
func main() {
  wg.Add(2)
  go add()
  go add()
  wg.Wait()
  fmt.Println(x)
}
  • 互斥锁

互斥锁是一种常用的控制共享资源访问的方法,它能够保证同时只有一个goroutine可以访问共享资源。Go语言中使用sync包的Mutex类型来实现互斥锁。

package main

import (
    "fmt"
    "sync"
)

var x int64
var wg sync.WaitGroup
var lock sync.Mutex

func add() {
    for i := 0; i < 5000; i++ {
        lock.Lock()
        x = x + 1
        lock.Unlock()
    }
    wg.Done()
}
func main() {
    wg.Add(2)
    go add()
    go add()
    wg.Wait()
    fmt.Println(x)
}

  • 读写锁

互斥锁是完全互斥的,但是有很多实际的场景下是读多血少,当我们并发去读取一个资源没有必要去枷锁,这种场景下使用读写锁的读更好一些。

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

var x int64
var wg sync.WaitGroup
var rw sync.RWMutex

func write() {
    rw.Lock()
    x = x + 1
    rw.Unlock()
    wg.Done()
}
func read()  {
    rw.RLock()
    //读取x的值
    fmt.Println(x)
    rw.RUnlock()
    wg.Done()
}
func main() {
    start := time.Now()
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go write()
    }

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go read()
    }
    wg.Wait()
    end := time.Now()
    fmt.Println(end.Sub(start))
}

sync

在代码中使用time.sleep用来同步肯定是不合适的,Go语言中可以使用sync.WaitGroup来实现并发任务的同步。sync.WaitGroup有几个方法

方法名 功能
(wg *WaitGroup)Add(detail int) 计数器+delta
(wg *WaitGroup)Done() 计数器-1
(wg *WaitGroup)Wait() 阻塞到计数器变为0

sync.WaitGroup内部维护着一个计数器,计数器的值可以增加和减少。例如当我们启动了N个并发任务就把计数器增加N。每个任务完成时调用Done()方法将计数器减一。通过调用wait()来等待

var wg sync.WaitGroup

func hello() {
  //完成
  defer wg.Done()
  fmt.Println("完成内容")
}
func main() {
  wg.Add(1)
  go hello()
  fmt.Println("main goroutine done!")
  wg.Wait()
}

  • sync.Once

在编程的很多场景下我们需要确保某些操作在高并发场景下只执行一次,例如加载一次配置文件,只关闭一次通道等

Go语言当中sync提供了一个针对一次性使用场景的解决方案-sync.Once

sync.Once只有一个Do方法,其签名如下

func(o *Once)Do(f func()){}
//示例
var icons map[string]image.Image

var loadIconsOnce sync.Once

func loadIcons() {
    icons = map[string]image.Image{
        "left":  loadIcon("left.png"),
        "up":    loadIcon("up.png"),
        "right": loadIcon("right.png"),
        "down":  loadIcon("down.png"),
    }
}

// Icon 是并发安全的
func Icon(name string) image.Image {
    loadIconsOnce.Do(loadIcons)
    return icons[name]
}
  • sync.Map

Go语言中内置的map不是并发安全的。所以在高并发的使用情况下就需要为map加锁来保证安全了。Go语言的sync包中提供了一个开箱即用的并发安全版map-sync.Map。开箱即用不需要像内置的map初始化才能使用。

var m = sync.Map{}

func main() {
    wg := sync.WaitGroup{}
    for i := 0; i < 20; i++ {
        wg.Add(1)
        go func(n int) {
            key := strconv.Itoa(n)
            m.Store(key, n)
            value, _ := m.Load(key)
            fmt.Printf("k=:%v,v:=%v\n", key, value)
            wg.Done()
        }(i)
    }
    wg.Wait()
}

原子操作(atomic)包

代码中的加锁操作因为涉及内核上下文切换较高,针对基本数据我们可以使用原子操作来保证并发安全,因为原子操作时Go语言提供的方法它在用户态就可以完成,因此性能比加锁操作更好

var x int64
var l sync.Mutex
var wg sync.WaitGroup

// 普通版加函数
func add() {
    // x = x + 1
    x++ // 等价于上面的操作
    wg.Done()
}

// 互斥锁版加函数
func mutexAdd() {
    l.Lock()
    x++
    l.Unlock()
    wg.Done()
}

// 原子操作版加函数
func atomicAdd() {
    atomic.AddInt64(&x, 1)
    wg.Done()
}

func main() {
    start := time.Now()
    for i := 0; i < 10000; i++ {
        wg.Add(1)
        // go add()       // 普通版add函数 不是并发安全的
        // go mutexAdd()  // 加锁版add函数 是并发安全的,但是加锁性能开销大
        go atomicAdd() // 原子操作版add函数 是并发安全,性能优于加锁版
    }
    wg.Wait()
    end := time.Now()
    fmt.Println(x)
    fmt.Println(end.Sub(start))
}

atomic包提供了底层的原子级内存操作,对于同步算法的实现很有用。这些函数必须谨慎地保证正确使用。除了某些特殊的底层应用,使用通道或者sync包的函数/类型实现同步更好。

上一篇下一篇

猜你喜欢

热点阅读