Go语言之初识Goroutine

作者 : 开心源码 本文共2985个字,预计阅读时间需要8分钟 发布时间: 2022-05-12 共198人阅读

1. Go的并发机制

面对前来面试Java程序员的求职者的时候我一般都会问问并发编程,毕竟并发编程总是属于Java编程的高级部分。其实Java的并发编程已经被设计的很简单了,不过要想掌握并发编程也并不是一件很轻松的事情。

作为一个DBA,面对高并发的场景算是司空见惯的了,没有并发能力的数据库(对,我说的就是MyISAM),还不如玩具。

那么数据库并发的时候最重要的几个点是什么呢?

  • 锁,分为排他锁和共享锁,我认为这是个天才的设计;
  • 连接池,使用了连接池可以大大降低高并发时的负载;
  • 索引,索引能大幅提升查询效率,降低锁持有的时间。

其实说了这三样,还是锁最重要,锁保护了并发时出现竞态时的资源安全。实际上在Java编程的时候也总是说什么线程安全之类的概念,无非也就是对竞争状态下资源的保护。

Go作为一个现代的语言,其并发模型也是很简单的,这一点的确比Python好,很多人认为Python是没有并发编程的,这也不能算是全错,至少Python的并发能力和Java比起来还是差不少。

扯了这么多闲话,画一张图来说明一下Go的并发:

调度器

  • G4到G7代表着goroutine,一旦创立出来就会被分配给一个逻辑解决器,这里记为P0;
  • 逻辑解决器P0被绑定到一个系统线程上;
  • 逻辑解决器是可以在代码里分配的。

这个模型看起来是还是怪怪的,由于我们知道计算机里有个很重要的概念叫做中断,大致意思是CPU每个时间片只能解决一件事,但是CPU可以分配时间片,即解决一会儿任务A,而后中断,分配少量时间片给任务B,由于切换时间太快,反应迟钝的人是反应不过来的,还以为计算机是并行解决的。

那么逻辑解决器要如何进行并发呢,这看起来还是不像并发的样子。事实上Go提供了这样的能力,不然还谈什么并发编程。

Go遇到需要阻塞的goroutine,就会分配一个新的线程,将这个goroutine和原来的线程分离,直到该阻塞线程有返回。这段时间里,逻辑解决器P0继续做队列里其余的事情。

2. 编码实现

下面来写一段代码展现如何实现并发:

package mainimport (    "fmt"    "runtime"    "sync")var wg sync.WaitGroupfunc main() {    runtime.GOMAXPROCS(1)    wg.Add(2)    fmt.Println("Start........")    go printPrime("A")    go printPrime("B")    fmt.Println("Waiting to finish")    wg.Wait()    fmt.Println("Done!")}func printPrime(prefix string) {    defer wg.Done()    next:        for outer := 2; outer < 50000; outer++ {            for inner := 2; inner < outer; inner++ {                if outer % inner == 0 {                    continue next                }            }            fmt.Printf("%s: %d\n", prefix, outer)        }        fmt.Printf("Finish %s\n", prefix)}

这是一段找素数的代码,可以找到50000以内的素数并打印出来。下面来分析分析:

  • var wg sync.WaitGroup,顾名思义,这是等待组,后面会给这个值add 2,代表着要等待两个goroutine,这有点像Java的线程池;

  • runtime.GOMAXPROCS(1),分配一个逻辑解决器;

  • go printPrime(“A”),创立一个goroutine,这里就能看出来goroutine是函数级别的。

写过Java并发的人都应该能反应过来,这段代码的打印结果肯定是混乱的,A和B组会反复的交替出现,事实上也就是这样的。并发编程原本就是在互相争抢资源,包括解决器资源,谁先抢到了时间片,谁就先解决,其余人等待时间片。

不过在我的电脑上试验的时候发现,现在的电脑太快了,当我决定打印5000以内素数的时候,看起来就像是顺序执行的,只有将范围扩大到50000才能看出来效果:

打印素数

这里的并发实现,比Java简单多了,尽管Java已经很简单了,但是我还是更中意Go语言。

诚如我之前所说,数据库利用了锁来实现并发时的资源保护。这也就是说只需有并发编程就肯定会遇到资源竞争的事情。比方下面一段代码, 这段代码被改造成了无数种形式来说明并发编程时需要注意的资源问题:

package mainimport (    "fmt"    "runtime"    "sync")var (    wg      sync.WaitGroup    counter int)func main() {    wg.Add(2)    go printPrime(1)    go printPrime(2)    wg.Wait()    fmt.Println(counter)}func printPrime(id int) {    defer wg.Done()    for count := 1; count < 3; count++ {        value := counter        runtime.Gosched()        value++        counter = value    }}

猜猜打印结果是多少?答案是不确定,反复进行资源的覆盖导致了结果的不确定。

这就是由于没有进行资源保护造成的,加把锁其实就处理这个问题了。

package mainimport (    "fmt"    "runtime"    "sync")var (    wg      sync.WaitGroup    counter int    mutex sync.Mutex)func main() {    wg.Add(2)    go printPrime(1)    go printPrime(2)    wg.Wait()    fmt.Println(counter)}func printPrime(id int) {    defer wg.Done()    for count := 1; count < 3; count++ {        mutex.Lock()        {            value := counter            runtime.Gosched()            value++            counter = value        }        mutex.Unlock()    }}

将可能造成覆盖的区域锁住即可以了,这个时候就相当于给资源加上了一个X锁,即互斥锁,这种情况下,同一时刻只允许一个goroutine进入该区域。

我们也都知道,Java里有一种AtomicInteger类,是线程安全的原子类,Go也有相似的原子操作:

package mainimport (    "fmt"    "runtime"    "sync"    "sync/atomic")var (    wg      sync.WaitGroup    counter int32)func main() {    wg.Add(2)    go printPrime(1)    go printPrime(2)    wg.Wait()    fmt.Println(counter)}func printPrime(id int) {    defer wg.Done()    for count := 1; count < 3; count++ {        atomic.AddInt32(&counter, 1)        runtime.Gosched()    }}

这种方式也能写出线程安全的函数了。

3. 小结

不得不感叹一句,现在的语言真是好学啊,尽管说学精通需要代码量的训练和深入的思考,但是上手的确容易。

有了Go语言,连指针都变得不那么难以接受了。

参考资料:《Go IN ACTION》

顺便推荐一本提升程序员内力的好书:《码农翻身》。

喜欢或者者帮助到了你,记得点个赞哦。

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » Go语言之初识Goroutine

发表回复