字节青训营笔记 | 青训营笔记

85 阅读4分钟

这是我参与「第六届青训营」伴学笔记创作活动的第 2 天

go语言进阶

并发与并行

并发:顾名思义,并发是同时发生,指的是在单处理器系统上线程在微观串行执行,而在宏观并发执行(多线程程序在一个核的cpu上运行),即

  • 处理器分时复用
  • 多线程交织执行

在特定某个时刻,某一个线程以排他方式独占CPU资源,而在不同时刻,不同的线程占用CPU运行,从而实现在一段时间内同时执行多个线程的表象。

并行:并行则就是同时进行,指的是在装配多个处理器的并行计算机系统上,将多个线程分配或者指定到不同的处理器上同时执行(多线程程序在多个核的cpu上运行)。

打个比方:并发是一个人同时吃三个馒头,而并行是三个人同时吃三个馒头。

协程

协程和线程的区别是:协程(用户态)避免了无意义的调度,可以提高性能,也是一种轻量级的线程,而线程则是内核态,线程可以跑多个协程。

高并发的能力在go语言中可以优雅的实现那就是Goroutine,它适应高并发场景,能更优雅方便的写出高并发程序。

Goroutine

通过一个例子来使用Goroutine:快速打印hello groutine : 0~hello groutine : 4

func hello(i int) {
    println("hello goroutine : " + fmt.Sprint(i))
}
​
func HelloGoRoutine() {
    for i := 0; i < 5; i++ {
        go func(j int) {
            hello(j)
        }(i)
    }
    time.Sleep(time.Second)
}

在go语言中要创建一个协程就是在函数前面加上go关键字,上例代码说明有5个子协程来打印快速hello groutine : 0~hello groutine : 4,当然即使没有用到go关键字也有一个协程的进行(main)函数自己)。

在上例程序中的倒数第二行用了time.sleep()这是为了防止在子协程运行过程中主协程不退出,但是在我们不知道子协程究竟运行多长时间时这样的阻塞明显不够优雅,那么怎样更优雅的的完成阻塞呢?那就是go语言中的WaitGroup

WaitGroup

它的本质是一种计数器,下面是WaitGroup的三种方法

  • Add(delta int) 计数器+delta
  • Done() 计数器-1
  • Wait() 主协程阻塞直到计数器为0

使用它就可以优化上一例中的阻塞问题,代码如下

func HelloGoRoutine() {
    var wg sync.WaitGroup
    wg.Add(5)
    for i := 0; i < 5; i++ {
        go func(j int) {
            defer wg.Done()
            hello(j)
        }(i)
    }
    wg.Wait()
}

time.Sleep(time.Second)替换为wg.Wait(),首先,它声明了在sync包里的名为wgWaitgroup变量然后使用Add方法计数器+5,之后在子协程函数内使用Done方法每个子协程都减去1,最后在主函数尾部用Wait方法等待计数器为0时结束主函数,这样就会更优雅。

在上例中还有一个未知问题(defer关键字),defer关键字用于延缓函数的执行(一般用于释放资源和连接、关闭文件、释放锁等。)本例中为在子协程的最后执行defer语句进行计数器减一。

Channel

在说明Channel之前先说明协程与协程之间的通信,在go中提倡通过通信共享内存而不是通过共享内存来实现通信,要通过通信来共享内存就需要通道(Channel), 它又分为有缓冲通道和无缓冲通道。Channel的声明由make实现:make(chan 元素类型,[缓冲大小])

  • 无缓冲通道 make(chan int)
  • 有缓冲通道 make(chan int,2)

下面有一个具体的例子来说明通道

func CalSquare() {
    src := make(chan int)
    dest := make(chan int, 3)
    go func() {
        defer close(src)
        for i := 0; i < 10; i++ {
            src <- i
        }
    }()
    go func() {
        defer close(dest)
        for i := range src {
            dest <- i * i
        }
    }()
    for i := range dest {
        println(i)
    }
}

第一个协程生成数字0~9通过Channel发送给第二个协程(src <- i)使用括号内的方式来发送数据,另外也可以用 (变量 := <-数据 )括号内方式来接受数据。之后,第二个协程将数字平方之后发送给主协程用rang遍历结果并输出。

并发安全Lock

通过一个例子来说明

var (
    x    int64
    lock sync.Mutex
)
​
func addWithoutLock() {
    for i := 0; i < 2000 ; i++ {
        x += 1
    }
}
​
func addWithLock() {
    for i := 0; i < 2000; i++ {
        lock.Lock()
        x += 1
        lock.Unlock()
    }
}
​
func add(){
    x = 0
    for i := 0; i < 5; i++ {
        go addWithoutLock()
    }
    time.Sleep(time.Second)
    println("WithoutLock:",x)
     x = 0
    for i := 0; i < 5; i++ {
        go addWithLock()
    }
    time.Sleep(time.Second)
    println("WithLock:", x)
}

在本例中通过5个协程将x的值各加2000,期望输出结果为10000.

但是运行后为withoutlock: 8382

                    `withlock: 10000`

这就是加锁和不加锁产生的结果,它的原理是在第一次调用Lock方法时,应当调用Unlock方法解锁不然当第二次调用Lock方法时会被阻塞。另外这个方法也在sync包内,所以当存在并发安全问题时应该在代码中使用并发锁,但是并发锁的使用也会导致性能降低,所以使用并发锁应该谨慎。