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

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包的函数/类型实现同步更好。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,504评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,434评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,089评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,378评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,472评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,506评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,519评论 3 413
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,292评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,738评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,022评论 2 329
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,194评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,873评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,536评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,162评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,413评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,075评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,080评论 2 352

推荐阅读更多精彩内容