GO语言中的并发

GO语言中的并发

并发还是并行

Concurrency is about dealing with lots of things at once. Parallelism is about doing lots of things at once.1

  • 并发是在同一时间处理(dealing with)多件事情。

  • 并行是在同一时间做(doing)多件事情。

并发的目的在于把单个 CPU 的利用率使用到最高。并行则需要多核的支持

单核CPU上运行的多线程程序, 同一时间只能一个线程在跑, 系统帮你切换线程而已(cpu时间切片), 系统给每个线程分配时间片来执行, 每个时间片大概10ms左右, 看起来像是同时跑, 但实际上是每个线程跑一点点就换到其它线程继续跑,效率不会有提高的,切换线程反倒会增加开销(线程的上下文切换),宏观的可看着并行,单核里面只是并发,真正执行的一个cpu核心只在同一时刻执行一个线程(不是进程)。

假设一个单核CPU,一下三种情况哪种执行时间最短?

  • 1、只跑一个线程,该线程顺序执行每个任务

  • 2、跑n个线程(内核态线程),每个任务分配一个线程执行

  • 3、跑n个协程(用户态线程)每个任务跑一个协程

时间最短的是1,其次是3,最后是2,对于1,os不需要分配资源给线程或者协程的调度没有浪费资,并发不能提高运行效率,并发的作用在于执行一个非常耗时的任务时,不需要一直等待其返回也能做别的任务。对于2和3,他们的区别在于线程包的实现,一个是内核态,一个是用户态

线程包实现的两种方式:用户态和内核态

  • 线程包的实现可以在内核态也可以在用户态,对于在用户空间实现的线程包,内核不知道线程的存在,线程的运行由进程决策且在该进程拥有CPU时间切片的情况下不可能切换到别的进程中的线程对于在内核实现线程包,内核知道线程的存在,且可以对线程进行调度而不考虑线程属于哪个进程,简单点老说,如果有进程A,B,进程A中有线程A1A2A,进程B中有线程B1B2,如果线程包实现在用户态,线程的调度只能是,A1到A2或者B1到B2,而如果线程包实现在内核态,线程的调度可以是A1到B2,B2到A2,线程的调度可以跨进程。

  • 用户级和内核级线程的差别在于性能,用户级的线程切换只需要简单的机器指令,而内核级的线程切换需要完整的上下文切换,修改内存映射等非常耗时

Java线程在Windows及Linux平台上的实现方式,是内核线程的实现方式。在Linux平台上当thread.run就会调用一个fork产生一个线程,这种方式实现的线程,是直接由操作系统内核支持的——由内核完成线程切换,内核通过操纵调度器(Thread Scheduler)实现线程调度,并将线程任务反映到各个处理器上。而Go语言中的goroutine是用户态的线程切换

创建一个goroutine

在go语言中创建一个goroutine只需加上go关键字即可

func main() {
    fmt.Println("主线程1,主线程1")
    go func() {
        fmt.Println("子线程,子线程")
    }()
    fmt.Println("主线程2,主线程2")
    time.Sleep(time.Second * 2)
}

goroutine之间的通信

  • 两个goroutine之间使用channels进行通信,它可以让一个goroutine通过它给另一个goroutine发送值信息。
  • 两个相同类型的channel可以使用==运算符比较。如果两个channel引用的是相通的对象,那么比较的结果为真。
  • 一个channel有发送和接受两个主要操作,都是通信行为。

无缓存的channel

channel可分为无缓存和有缓存两种

  • 一个基于无缓存Channels的发送操作将导致发送者goroutine阻塞,直到另一个goroutine在相同的Channels上执行接收操作,当发送的值通过Channels成功传输之后,两个goroutine可以继续执行后面的语句。反之,如果接收操作先发生,那么接收者goroutine也将阻塞,直到有另一个goroutine在相同的Channels上执行发送操作。
    func main() {
    done := make(chan struct{})
    fmt.Println("主线程阻塞")
    go func() {
        fmt.Println("子线程运行")
        time.Sleep(time.Second * 2)
        done <- struct{}{} //发送数据
        close(done)//关闭channel
    }()
    <-done //阻塞
    fmt.Println("主线正常运行")

}
  • 基于无缓存Channels的发送和接收操作将导致两个goroutine做一次同步操作。因为这个原因,无缓存Channels有时候也被称为同步Channels。
  • 不管一个Channel是否被关闭,当它没有被引用时将会被Go语言的垃圾自动回收器回收
  • 视图重复关闭一个channel将导致panic异常,视图关闭一个nil值的channel也将导致panic异常,当一个channel被关闭后,再向该channel发送数据将导致panic异常
  • 没有办法直接测试一个channel是否被关闭,但是接收操作有一个变体形式:它多接收一个结果,多接收的第二个结果是一个布尔值ok,ture表示成功从channels接收到值,false表示channels已经被关闭并且里面没有值可接收。
    x,ok:=<-done 
    fmt.Println("222",x,ok)

如果你在main goroutine 中持续去接收一个channel,但是该channel被不会被写入,那么会导致all goroutines are asleep - deadlock! 的错误,比如下面的代码

     func main() {
    sizes := make(chan int64)
    for i := 0; i < 10; i++ {
        go func() {
            sizes <- 1
            fmt.Println("正在执行。。。")
        }()
    }
    var total int64
    for size := range sizes {
        time.Sleep(time.Second * 1)
        fmt.Println("从chan中拿出数据  ", size)
        total += size
    }
    fmt.Println("total--->  ", total)
}

上面的例子是多个goroutine 使用同一个channel向main goroutine 中发送数据,main goroutine 一直在接收,当所有的线程都发生完数据后,主线线程还在接收,会一直阻塞,最后error
要解决该问题,可以使用sync.WaitGroup,sync.WaitGroup提共一种特殊的计数器,该计数器可以在多个goroutine操作时做到安全并且提供提供在其减为零之前一直等待的一种方法,改写后的代码

    func main() {
    sizes := make(chan int64)
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()  //该goroutine执行完以后减一
            sizes <- 1
            fmt.Println("正在执行。。。")
        }()
    }

    // close
    go func() {
        wg.Wait()  //计数器不为0则一直阻塞
        fmt.Println("这里要关闭chan")
        close(sizes)
    }()

    var total int64
    for size := range sizes {
        time.Sleep(time.Second * 1)
        fmt.Println("从chan中拿出数据  ", size)
        total += size
    }

    fmt.Println("total--->  ", total)

}

带缓存的channel

  • 带缓存的Channel内部持有一个元素队列。
  • 向缓存Channel的发送操作就是向内部缓存队列的尾部插入原因,接收操作则是从队列的头部删除元素。如果内部缓存队列是满的,那么发送操作将阻塞直到因另一个goroutine执行接收操作而释放了新的队列空间。相反,如果channel是空的,接收操作将阻塞直到有另一个goroutine执行发送操作而向队列插入元素。

func main() {
    chan1 := make(chan string, 3)
    chan2 := make(chan string, 5)
    time.Sleep(time.Second * 5)

    for x := 0; x < 100; x++ {
        go func() {
            chan1 <- "我是A,我生产了一个cake"
            fmt.Println("我是A,我生产了一个cake")
        }()

        go func() {
            s := <-chan1
            chan2 <- s + "   我是B,我装饰了一个cake"
            fmt.Println("我是B,我装饰了一个cake")
        }()

        go func() {
            s := <-chan2
            fmt.Println(s + "   我是C,我卖出了一个cake")
        }()
    }

    time.Sleep(time.Second * 10)

}
无缓存channel更强地保证了每个发送操作与相应的同步接收操作;但是对于带缓存channel,这些操作是解耦的。

基于Select的多路复用

考虑这么一个情况,如果有两个线程A、B通过不同的channel向同一个线程C发送数据,对于C只要从任意channel中取出数据就可以继续后面的步骤,我们无法做到从每一个channel中接收信息,如果我们这么做的话,如果第一个channel中没有事件发过来那么程序就会立刻被阻塞,这样我们就无法收到第二个channel中发过来的事件。
使用select则可以解决这样的问题


func main() {

    chan1 := make(chan int)
    chan2 := make(chan int)

    go func() {
        time.Sleep(time.Second * 2)
        chan1 <- 1
    }()

    go func() {
        time.Sleep(time.Second * 2)
        chan2 <- 2
    }()

    select {
    case <-chan1:
        fmt.Println("从channe  1中拿数据")
    case <-chan2:
        fmt.Println("从channe  2中拿数据")
    }
    fmt.Println("继续执行")

}
  • 如果多个case同时就绪时,select会随机地选择一个执行,这样来保证每一个channel都有平等的被select的机会
  • channel的零值是nil。也许会让你觉得比较奇怪,nil的channel有时候也是有一些用处的。因为对一个nil的channel发送和接收操作会永远阻塞,在select语句中操作nil的channel永远都不会被select到。

并发的退出

  • Go语言并没有提供在一个goroutine中终止另一个goroutine的方法,由于这样会导致goroutine之间的共享变量落在未定义的状态上。

基于共享变量的并发

数据竞争会在两个以上的goroutine并发访问相同的变量且至少其中一个为写操作时发生。

根据上述定义,有三种方式可以避免数据竞争

  • 不要去写变量
  • 避免从多个goroutine访问变量。
  • 允许很多goroutine去访问变量,但是在同一个时刻最多只有一个goroutine在访问。这种方式被称为“互斥”

sync.Mutex互斥锁

使用sync.Mutex互斥锁保证最多只有一个goroutine在同一时刻访问一个共享变量。Go的互斥量不能重入

对一个已经锁上的mutex来再次上锁–这会导致程序死锁

var (
    mu sync.Mutex
    balan int
)

func deposit(amount int){
    mu.Lock()
    balan+=amount
    mu.Unlock()
}

func getBalance()int{
    defer func() {mu.Unlock()}()
    mu.Lock()
    return balan
}

sync.RWMutex读写锁

sync.RWMutex允许多个只读操作并行执行,但写操作会完全互斥

  • goroutine们必须等待才能获取到锁的时候,RWMutex才是最能带来好处的。RWMutex需要更复杂的内部记录,所以会让它比一般的无竞争锁的mutex慢一些。
var(
    mu sync.RWMutex
    balance int
)

func Deposit(amount int){
    mu.Lock()
    balance+=amount
    mu.Unlock()
}

func Balance()int{
    mu.RLock()
    defer  mu.Unlock()
    return balance
}

“同步”不仅仅是一堆goroutine执行顺序的问题;同样也会涉及到内存的问题。(可视性,新的视图能被其他线程获得,而不是得到旧值)

sync.Once初始化

sync.Once用于解决多线程访问一个只需初始化一次的资源(类似于Java中支持并发操作 的单例模式),且sync.Once能保证可视性

一次性的初始化需要一个互斥量mutex和一个boolean变量来记录初始化是不是已经完成了;互斥量用来保护boolean变量和客户端数据结构。

var icons map[string]image.Image
var loadIconsOnce sync.Once

func Icon(name string) image.Image {
    loadIconsOnce.Do(loadIcons)
    return icons[name]
}

func loadIcons() {
    icons = make(map[string]image.Image)
    icons["spades.png"] = loadIcon("spades.png")
    icons["hearts.png"] = loadIcon("hearts.png")
    icons["diamonds.png"] = loadIcon("diamonds.png")
    icons["clubs.png"] = loadIcon("clubs.png")
}

func loadIcon(iconName string) image.Image  {
    //该函数未加载图片
    img:=new(image.Image)
    return  img
}

猜你喜欢

转载自blog.csdn.net/fuckluy/article/details/79072923