GoLang的并发与安全

本文介绍了Go语言在并发处理方面的特性,包括goroutine的使用,通过示例展示了如何避免goroutine中断问题的WaitGroup,原子函数在并发安全中的作用,以及互斥锁和通道在并发同步中的应用。Go语言的这些特性使得并发编程更加高效且易于管理。
摘要由CSDN通过智能技术生成

Go语言在服务端程序的开发中能够以最简单高效的方式来解决问题,它用轻量级方法来处理并发,使程序性能大幅提高。对于服务端程序的开发工作中,开发人员最在意的并发及同步安全问题,Go语言提供了自已独特的解决方案。

 

并发的单位

进程

进程是操作系统结构的基础,操作系统进行资源分配和调度的基本单位。应用程序在运行中使用和维护各类资源的容器。

线程

线程是操作系统能够进行运算调度的最小单位。线程包含在进程中,进程实际分配CPU时间片进行运作的单位。

协程

协程类似子例程,是一种比线程更轻量级的单位。它在线程分配到的CPU时间片基础上由语言(虚拟机)进行调度控制。这样就避免了线程切换过程中的资源损耗,从而大大提高了程序的性能。

Go语言的并发实现使用的是goroutine,属于协程的一种。

 

1.goroutine

goroutine的简单使用

使用go关键字调用goroutine

例1-1 goroutine的伪代码

//调用命名函数

go myFunc()

//调用匿名函数

go func() {

  // TODO

}()

 

 

例1-2 一个完整的调用:

package main

 

import (

    "fmt"

    "time"

)

 

func myGoroutine(c int) {

    for i:=0; i<5; i++ {

       fmt.Printf("myGoroutine-%c %d\n",c,i)

       time.Sleep(time.Microsecond)

    }

}

 

func main() {

    go myGoroutine('A')

    go myGoroutine('B')

    // 注意这个sleep

    time.Sleep(time.Second)

}

 

例1-2中简单的定义了一函数,每隔一毫秒打出一行日志,在main函数中由A、B两个协程并发运行,分别打印5个A、5个B,共10行日志。

值得注意的是main函数中最后的sleep,如果没有这个语句,打印的日志会不完整(实际上最多打出一行,或者完全不输出日志)。这是因为goroutine是由Go语言内部调度的,执行完main函数,程序退出,所有协程就中断了。

为了解决这个问题,先简单地让程序在此休眠1秒,让2个协程能够运行完成,就可以看到期望的效果:打印10行日志。但是这也有个问题,在双核以上CPU的计算机上,这2个协程仅需5ms就运行完成了,后面sleep的995ms就浪费了。实际生产中,也很难知道每个协程具体运行所需要的时间。那么,如何让goroutine完整执行,又避免浪费呢?

 

WaitGroup

WaitGroup相当于是带阻塞的计数器。初始为零,调用Add加上需要的值,在关键节点调用Done来减1,Wait用于阻塞,等到计数器为零时,解除阻塞。

 

    例1-3 使用WaitGroup等待goroutine完成

package main

 

import (

    "fmt"

    "sync"

    "time"

)

 

func myGoroutine(c int,wg *sync.WaitGroup) {

    for i:=0;i<5;i++ {

       fmt.Printf("myGoroutine-%c %d\n",c,i)

       time.Sleep(time.Microsecond)

    }

    (*wg).Done()

}

 

func main() {

    var wg sync.WaitGroup

    wg.Add(2)

    go myGoroutine('A',&wg)

    go myGoroutine('B',&wg)

    //改用WaitGroup

    wg.Wait()

}

 

 

2.原子函数

当程序并发运行时,经常会遇到共享的内存区域,比如页面访问量的计数。并发中的每个协程需要从内存取出当前计数值,进行+1运算,然后写回内存(其实还会涉及CPU缓存的问题,在此先不考虑)。如下例所示,程序中有个共享的变量用于存放计数的值,有1,000,000个协程并发对它进行+1操作。

运行例2-1的代码,最后的计数一般达不到1,000,000,因为并发运行过程中,有一部分自增操作的取值不是前一个操作的运算结果。

 

    例2-1 共享内存区域冲突

package main

 

import (

    "fmt"

    "sync"

)

 

func main() {

    var wg sync.WaitGroup

    wg.Add(1000000)

    cnt := 0

    for i:=0; i<1000000; i++ {

       go func(){

           cnt++

           wg.Done()

       }()

    }

 

    wg.Wait()

    println("count=",cnt)

}

 

    这时,可以使用actomic包中的原子函数,来保证每个自增操作的运行从读取内存到写回内存是原子操作,每个自增操作读取到的值都是前一个操作的运算结果。

 

    例2-2 原子自增

package main

 

import (

    "fmt"

    "sync"

)

 

func main() {

    var wg sync.WaitGroup

    wg.Add(1000000)

    var cnt int32

    cnt = 0

    for i:=0; i<1000000; i++ {

       go func(){

           atomic.AddInt32(&cnt,1)

           wg.Done()

       }()

    }

 

    wg.Wait()

    println("count=",cnt)

}

 

例2-2中使用了atomic.AddInt32原子自增函数,所有自增的并发不会互相冲突了。

 

3.互斥锁

Go语言中还提供了互斥锁,可以创建一个临界区,用以保证同一时间只有一个协程可以访问这个临界区,从而达到并发同步的目的。

 

    例3-1 临界区代码块

package main

 

import (

    "fmt"

    "sync"

)

 

func main() {

    var wg sync.WaitGroup

    wg.Add(1000000)

    var cnt int32

    cnt = 0

    var mutexLock sync.Mutex

    for i:=0; i<1000000; i++ {

       go func(){

           mutexLock.Lock() {

              cnt++

           }

           mutexLock.Unlock()

           wg.Done()

       }()

    }

 

    wg.Wait()

    println("count=",cnt)

}

 

 

4.通道

使用原子函数或者互斥锁都可以实现并发中的同步,但是高并发中的关键资源的无序竞争会降低资源利用率,也有可能引起协程饥饿的情况发生。为了让资源的竞争有序起来,使用Go语言中的通道作为通信机制,可以提高资源利用率。

通道类似于队列,在通道的右端发送数据(channel <- sendBuff),通道的左端接收数据(recvBuff := <- channel),通信数据遵循队列的FIFO规则。

Go语言的通道默认只能往里发一个元素,在该元素被读取之前,发送端会阻塞。可以根据实际需要指定缓冲区的大小,发送端可以一直往通道发送数据,直到缓冲区被放满才会发生阻塞。

 

    例4-1 倒茶与喝茶

package main

 

import (

    "fmt"

    "sync"

)

 

func main() {

 

    var wg sync.WaitGroup

    wg.Add(2)

 

    // 缓冲区大小为3,一共可以放置4杯茶

    ch := make(chan int,3)

 

    go drinkTea(ch,&wg)

    go makeTea(ch,&wg)

 

    wg.Wait()

    fmt.Println("done.")

}

 

// 每10毫秒倒好一杯茶

func makeTea(ch chan int,wg *sync.WaitGroup) {

    for i :='A';i<='Z';i++ {

       fmt.Printf("make=%c\n",i)

       ch <- int(i)

        time.Sleep(10*time.Millisecond)

    }

    fmt.Println("make is done.")

    (*wg).Done()

    close(ch)

}

 

// 每50毫秒喝掉一杯茶

func drinkTea(ch chan int,wg *sync.WaitGroup) {

    for c := range ch {

       fmt.Printf("drink=%c\n",c)

        time.Sleep(50*time.Millisecond)

    }

    fmt.Println("drink is done.")

    (*wg).Done()

}

 

 

如例4-1所示,生产的速度远大于消费的速度,在通道缓冲区满了之后,生产协程会发生阻塞,直到通道中的元素被消费,才会再生产。

另外,值得一提的是,通道可以关闭。关闭了的通道不能接收生产,但缓冲区的元素还可以被消费直到通道中没有任何元素,才会被真正回收。

 

 

 

总结

并发间的安全一直都是开发人员最关注的问题之一,比如事务的ACID原则。在正确处理好临界资源,让各个并发单位都能正常执行,保证数据准确,并在此基础上提高程序运行效率。Go语言提供了很简单、直接的方法解决并发间同步问题,因此受到很多服务端程序的亲睐,成为Web服务端开发语言的新星。

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值