• Go语言学习笔记(4)——并发编程


    Golang在语言级别支持了协程,由runtime进行管理。

    在Golang中并发执行某个函数非常简单:

    func Add(x, y int) {
        fmt.Println(x + y)
    }
    
    func RunRoutine() {
        for i := 0; i < 10; i++ {
            go Add(i, i)
        }
    }

    但是输出为空。

    因为虽然新建了协程调用Add函数,但是该协程还没有来得及执行,程序就结束了。所以输出为空。

    如果想让代码按预想的方式运行,就需要让主函数等待所有goroutine退出后再结束。这就引出了goroutine间通信的问题。

    首先,我们先用最简单粗暴,也是传统的Lock来解决。

    这时的思路是:我们加一个全局的变量count。每次调用了Add,我们就count++。这样,当count=10的时候,就说明10个goroutine都执行完成了。

    但是,全局变量的访问需要加锁,这样才能保证count的访问是安全的。

    代码如下:

    var (
        lock  *sync.Mutex
        count int
    )
    
    func Add(x, y int) {
        lock.Lock()
        defer lock.Unlock()
        fmt.Println(x + y)
        count++
    }
    
    func RunRoutine() {
        lock = &sync.Mutex{}
        count = 0
        for i := 0; i < 10; i++ {
            go Add(i, i)
        }
        for {
            lock.Lock()
            temp := count
            if temp >= 10 {
                break
            }
            lock.Unlock()
        }
    }

    这样,执行结果如下:

    2
    4
    18
    10
    12
    14
    6
    16
    0
    8

    根据结果来看,10个协程都执行结束了,并且10个协程的执行顺序也是随机的。

    但是,事情貌似变得糟糕了。我们为了实现一个简单的功能,却写出了非常复杂的代码。

    如果用Golang来解决呢,这时我们考虑用channel来解决问题。

    channel是Golang提供的goroutine间的通信方式。我们可以使用channel在多个goroutine间传递消息。当然,channel是进程内的通信方式,如果需要进程间通信,可能Socket或者HTTP通信协议更合适。

    先看看,如果用channel,如果解决上面的问题。

    var (
        chs []chan int
    )
    
    func Add(x, y int) {
        fmt.Println(x + y)
        chs[x] <- 1
    }
    
    func RunRoutine() {
        chs = make([]chan int, 10)
        for i := 0; i < 10; i++ {
            chs[i] = make(chan int)
            go Add(i, i)
        }
        for _, ch := range chs {
            <-ch
        }
    }

    这里,我们定义了一个10个元素的channel数组。每次调用Add时,我们在对应的channel中写入一个数据。最后,我们在RunRoutine中遍历了整个数组,当所有的channel都读取完数据,说明10个goroutine都运行结束了。

    现在,我们看看channel的语法:

    channel的声明:

    var chanName chan ElementType

    例如: var ch chan int

    channel的初始化:

    可以利用make对channel进行初始化:

    ch = make(chan int)

    ch = make(chan int, 1)

    前者初始化了一个无缓冲的channel。无缓冲即当某个协程在channel中写入了数据,就马上被阻塞,要等到其他协程消费了该数据,协程才会继续执行。

    后者初始化了一个有缓冲的channel,缓冲长度为1。即ch为空时,当某个协程往ch中写入数据,并不会马上阻塞。当ch内有1个数据时,再往ch中写入数据,即会马上阻塞,知道协程消费了数据,使ch内的数据小于等于其缓冲量。

    有缓冲的channel可以用range进行读取。

    channel的读写:

    ch <- 1

    i := <- ch

    总之就是用箭头来进行读写操作,很直观。

    需要注意的就是读写操作带来的阻塞。

    select:

    select是类似switch的,用来处理channel异步IO的问题。

        select {
        case <- ch:
        case ch <- 1:
        default:
        }    

    需要注意的是,多个case同时符合的时候,switch是按顺序执行的,select是随机选择一个分支执行的。

    channel的超时机制:

    Golang的channel并没有自带的超时机制,但是可以用select来实现。

    考虑以下的几种方法:

        timeout := make(chan bool, 1)
        go func() {
            time.Sleep(time.Second)
            timeout <- true
        }()
        select {
        case <- ch:
        case <- timeout:
        }

    该方法提供了一个timeout,利用协程sleep 1s之后向timeout中写入一个true。当select中的ch在1秒内没有读出数据时,timeout将读出数据。

        select {
        case <- ch:
        case <-time.After(time.Second):
        }

    该方法利用了time.After。该方法的问题在于,这个计时器在select执行之后仍在在runtime中存在。这在高并发的应用场景下会产生性能问题。

    to := time.NewTimer(time.Second)
    for {
        to.Reset(time.Second)
        select {
        case <-c:
        case <-to.C:
        }
    }

    该方法算是上一种方法的改进。该方法利用了一个全局的timer,这样可以避免高并发下的计时器的滥用。

    单向channel:

        var (
            ch1 chan int
            ch2 chan<- int
            ch3 <-chan int
        )

    ch1是普通channel,ch2是只写channel,ch3是只读channel。

        ch4 := make(chan int)
        ch5 := <-chan int(ch4)
        ch6 := chan<- int(ch4)

    上面是各种channel之间的类型转换。

    关闭channel:

    close(ch)

    好像并没有什么其他可说的。唯一一点是,我们可以利用多返回值,在读取channel的时候检查channel是否被关闭。

    val, ok := <-ch

    多核并行:

    可以利用如下命令进行设置。但是Golang是否真的可以利用多个核心,还需要实际验证。

    runtime.GOMAXPROCS(16)

    同步锁:

    最开始的时候已经利用锁实现了功能。需要注意的是Lock和Unlock的对应。

    唯一性操作:

    sync.Once(funcName)

    思考了一下,大概可以用来实现单例模式。

  • 相关阅读:
    Java类加载机制
    Java内存模型
    遍历集合的常见方式,排序,用lambda表示是怎样的
    包和访问权限修饰符,.单例设计模式,.Object类常用方法,.内部类
    接口
    继承
    面向对象的四大特征 封装 继承 多态 抽象
    面向对象
    二维数组及Arrays工具类
    数组(冒泡,选择,排序)
  • 原文地址:https://www.cnblogs.com/wangzhao765/p/9768321.html
Copyright © 2020-2023  润新知