GO 从 0 到 1 系列:14 并发编程

并发编程

并发编程开发将一个执行的过程按照并行算法拆分为多个可以独立执行的代码块,从而充分利用多核和多处理器提高系统吞吐率

goroutine(协程) 和 channel(管道)

goroutine 可以做并发和并行的处理,可以将一个任务分解为多个 goroutine 去完成,让多个 cpu 去处理

需求:

要求统计 1-20000 的数字中,那些是素数

分析思路:

  1. 传统方法,使用一个循环,循环判断各个数是不是素数。
  2. 使用并发或者并行的方法,将统计素数的任务分配给多个 goroutine 去完成,这会使用到 goroutine

1 goroutine

1.1 goroutine 的调度模型

1.1.1 PGM 模式基本介绍

  1. M:操作系统的主线程(是物理线程)
  2. P:协程执行需要的上下文(可以理解为协程需要的运行环境)
  3. G:协程

1.1.2 PGM 模式运行的状态 1

  1. 当前程序有个 三个 M ,如果三个 M 都在一个 CPU 上运行,就是并发。如果在不同的 CPU 上运行就是并行
  2. M1,M2,M3 正在执行一个 G(协程),M1 的协程队列有三个,M2 的协程队列有 3 个,M3 协程队列有 2 个
  3. 从上图可以看到:Go 的协程是轻量级的线程,是逻辑态的,Go 可以容易的起上万个协程。
  4. 其他程序 C/JAVA 的多线程,往往是内核态,比教重量级,几千个线程可能耗光 CPU 资源。

1.13 PGM 模式运行的状态 2

  1. 分成两部分来看。

  2. 原来的情况是 M0 主线程正在执行 G0 协程,另外有三个协程在队列等待。

  3. 如果 G0 协程阻塞,比如读取文件或者数据库等

  4. 这时候就会 创建 M1 主线程(也可能是从已有的线程池中取出 M1),并且将原本在 M0 下面等待的 3 个协程挂到 M1 下开始执行,M0 的主线程下 G0 仍然在执行文件 io 的读写。

  5. 这样的 MPG 调度模式,可以既让 G0 执行,同时也不会让队列的其它协程一直阻塞,仍然可以并发 / 并行执行。

  6. 等到 G0 不阻塞了,M0 会被放到空闲的主线程继续执行(从已有的线程池中取),同时 G0 又被唤醒

1.2 goroutine 基本介绍

1.2.1 进程和线程的说明:

  • 进程:资源分配的基本单位
  • 线程:CPU 调度的基本单位
  1. 进程就是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位。
  2. 线程是进程的一个执行实例,是 CPU 调度的基本单位,是程序执行的最小单位,他是比进程更小的能够独立运行的基本单位
  3. 一个进程可以创建和销毁多个线程,同一个进程中的多个线程可以并发执行,而且一个进程退出后,线程也会退出和销毁
  4. 一个程序至少有一个进程,一个进程至少有一个线程或者多个线程
  5. 这是说明进程包含线程,而线程则不包含进程

1.2.2 并发和并行

顺序、并发与并行

  • 顺序是指发起执行的程序只能有一个(只占用一个 CPU 或 只占用一个核)
  • 并发是指多个程序同时发起执行(同时处理)的程序可以有多个(单车道并排只能有一辆车,可同时驶入路段多辆车)
  • 并行是指同时执行(同时做)的程序可以有多个 (多车道并排可以有多个车)

并发:

多线程,程序在单核上运行,就是并发(并发的特点是,多个任务作用在一个 cpu 上,从微观的角度上看,在一个时间点上其实只有一个任务在执行,但是 cpu 的时间片很短,所以给用户的感觉就是多个任务在”同时“进行)。

因为是在一个 cpu 上,比如有 10 个线程,每个线程执行 10 毫秒(进行轮询操作),从用户的角度看,好像 10 个线程都在运行,但是从微观上看,在某一个时间点看,其实只有一个线程在执行,这就是并发

并行:

在 go 语言中就支持并行执行。

多线程,程序在多核上运行,就是并行(并行,就是将多个任务分配到多个 cpu 上去同时执行,从微观的角度上看,在同一个时间点有多个任务在同时执行。

因为是在多个 CPU 上(比如有 10 个 CPU),比如有 10 个线程,每个线程执行 10 毫秒(各自在不同的 CPU 上执行),从人的角度上看,这 10 个线程都在运行,但是从微观上看,在某一个时间点看,也同时有 10 个线程在执行,这就是并行

总结:

并行的效率一定比并发快。

1.2.3 Go 协程和 Go 主线程

Go 语言中每个并发执行的单元叫 Goroutine,使用 go 关键字后接函数调用来创建一个 Goroutine

函数调用和函数不是一回事

go 的线程是一个用户态的实现

  1. GO 主线程(有程序员直接称为线程 / 也可以理解为 进程一个 Go 线程上,可以起多个协程,可以理解为,协程就是轻量级的线程[编译器做了优化]。

  2. Go 的协程特点:

    有独立的栈空间

    共享程序堆空间

    调度由程序员控制

    协程是轻量化的线程

1.3 goroutine 快速入门

在下面演示范例中,我们有两个函数,分别是TaskATaskB ,我们要实现并行操作,也就是同时运行这两个函数。

package main

import (
    "fmt"
    "time"
)

func TaskA() {
    for i := 1; i <= 10; i++ {
        fmt.Println(i)
    }
}

func TaskB() {
    for i := 'A'; i <= 'Z'; i++ {
        fmt.Println(string(i))
    }
}

func main() {
    fmt.Println("start")

    // go 关键字开启并发执行的线程,这是一个特定语法
    go TaskB()
    go TaskA()

    time.Sleep(time.Second * 2)
    fmt.Println("END")
}

执行:

从下图中我们可以看到 TaskB() 函数中输出的字母与 TaskA() 中输出的数字并发运行了,从而实现了 go 的并发编程


我们从这个示例上可以看到 A 和 B 两个函数不知道谁先执行的运行关系了。已经不能预知,因为这个中间涉及到一个调度问题,当然我们也可以通过人为的方式来实现他们的调度关系,有两种方式可以实现自定义执行顺序

我们在写代码的时候不会主动去实现调度关系,只是在演示的时候才需要这么做,这样更好的方便我们来理解 goroutine

方式一如下范例:

package main

import (
    "fmt"
    "time"
)

func TaskA() {
    for i := 1; i <= 10; i++ {
        fmt.Println(i)

        // 每次执行暂停一秒
        time.Sleep(1 * time.Second)
    }

}

func TaskB() {
    for i := 'A'; i <= 'Z'; i++ {
        fmt.Println(string(i))

        // 每次执行暂停一秒
        time.Sleep(1 * time.Second)
    }
}

func main() {
    fmt.Println("start")
    // go 关键字
    go TaskB()
    go TaskA()

    time.Sleep(time.Second * 10)
    fmt.Println("END")
}

输出

通过输出我们可以看到他每次打印都是在间隔一秒,释放 cpu 调度任务,从而执行另一个任务

方式二如下范例:

通过 runtime 包实习

可以通过 runtime 包中的 GoSched 让线程主动让出 CPU,也可以通过 time.Sleep 让线程休眠从而让出 CPU

package main

import (
    "fmt"
    "runtime"
    "time"
)

func TaskA() {
    for i := 1; i <= 10; i++ {
        fmt.Println(i)

        // runtime.Gosched() 该包是主动让出调度,让其他任务执行
        runtime.Gosched()
    }

}

func TaskB() {
    for i := 'A'; i <= 'Z'; i++ {
        fmt.Println(string(i))

        // runtime.Gosched() 该包是主动让出调度,让其他任务执行
        runtime.Gosched()
    }
}

func main() {
    fmt.Println("start")
    // go 关键字
    go TaskB()
    go TaskA()

    time.Sleep(time.Second * 3)
    fmt.Println("END")
}

输出我们会发现他在每次调度的时候都会主动让出调度操作,从而执行另外一个任务。

通过上面的几个案例可以看到现在 A 和 B 任务已经在并发执行了

1.3.1 主线程和协程执行的示意图

  • 程序一旦开始,我们会认为一个主线程或一个协程开始执行了
  • 然后继续执行会发现有一个 go test() 函数,开启协程
    • 如果主线程退出了,则协程即使没有执行完毕,也会退出
    • 当然协程也可以再主线程没有退出前就自己结束了,比如完成了自己的任务
  • 主线程依旧执行 for 循环
  • 等于说需要一定时间给 go 协程分配 cpu
  • 主线程退出,整个程序退出,协程也会退出

如下范例

package main

import (
    "fmt"
)

func TaskA() {
    for i := 1; i <= 10; i++ {
        fmt.Println(i)
    }

}

func TaskB() {
    for i := 'A'; i <= 'Z'; i++ {
        fmt.Println(string(i))
    }
}

// main 就是一个主线程
func main() {
    fmt.Println("start")

    // go 关键字开启协程 
    go TaskB()
    go TaskA()
    fmt.Println("END")
}

输出

[23:09:02 root@go codes]#go run goroutine.go 
start
END

# 我们会发现当我们的 main 主线程执行完了之后就会退出程序,导致 TaskB()、TaskA() 两个协程没有执行
# 验证了主线程退出,整个程序退出,协程也会退出

由此引主线程与协程之间的通信:

1.3.2 线程间通讯

我们很多时候无法预期我们的 工作线程的执行时间。

main 函数也是由一个线程来启动执行,这个线程称为主线程,其他线程叫工作线程。主线程结束后工作线程也会随之销毁,使用 sync.WaitGroup (计数信号量)来维护执行线程执行状态

type WaitGroup struct 是一个结构体

原理:

计数信号量:
启动线程之前,计数信号量添加 +1
当线程执行结束时,计数信号量 -1
当计数信号量为 0 时就表示线程已经执行结束了

查看 waitgroup 结构体的方法

[10:28:05 root@go codes]#go doc sync.waitgroup
package sync // import "sync"

type WaitGroup struct {
        // Has unexported fields.
}
    A WaitGroup waits for a collection of goroutines to finish. The main
    goroutine calls Add to set the number of goroutines to wait for. Then each
    of the goroutines runs and calls Done when finished. At the same time, Wait
    can be used to block until all goroutines have finished.

    A WaitGroup must not be copied after first use.

func (wg *WaitGroup) Add(delta int)     # 添加计数器
func (wg *WaitGroup) Done()             # 减一计数器
func (wg *WaitGroup) Wait()             # 等待,判断计数信号量值是否为 0 ,为 0 时表示工作线程执行完毕然后在执行主线程

范例:

package main

import (
    "fmt"
    "sync"
)

// 由于 sync.WaitGroup 是一个结构体只拷贝类型,我要对同一块内存地址操作所以需要传入指针
func TaskA(wg *sync.WaitGroup) {
    // 当 TaskA 工作线程执行完毕之后信号量减一,使用 defer 是为了防止程序出错后退出执行 wg.Done()
    defer wg.Done()

    for i := 1; i <= 10; i++ {
        fmt.Println(i)
    }

}

func TaskB(wg *sync.WaitGroup) {
    // 当 TaskB 工作线程执行完毕之后信号量减一
    defer wg.Done()
    for i := 'A'; i <= 'Z'; i++ {
        fmt.Println(string(i))
    }

}

func main() {
    //  计数信号量:
    // 启动线程之前,计数信号量添加 +1
    // 当线程执行结束时,计数信号量 -1
    // 当计数信号量为 0 时就表示线程已经执行结束了

    fmt.Println("start")

    // 调用 sync.WaitGroup{} 结构体,用的是 new 函数取地址操作
    wg := new(sync.WaitGroup)

    // 当前有两个工作线程,所以添加 2
    wg.Add(2)

    // go 关键字开启工作线程
    go TaskB(wg)
    go TaskA(wg)

    // 在 END 之前等待,等待计数信号量值是否为 0,为 0 时表示工作线程执行完毕
    wg.Wait()
    fmt.Println("END")
}

执行,通过执行我们会发现这种方法就已经实现了线程间的通讯

[11:03:20 root@go codes]#go run goroutine.go 
start
1
2
3
4
5
6
...
END

1.4 匿名函数开启 goroutine

在调用匿名函数的时候,一定要记住 go 关键字后面跟的是函数调用

package main

import (
    "fmt"
    "time"
)

func main() {
    fmt.Println("start")

    go func() {
        for i := 0; i <= 10; i++ {
            fmt.Println(i)
        }
    }()

    go func() {
        for i := 'A'; i <= 'Z'; i++ {
            fmt.Printf("%c\n", i)
        }
    }()

    time.Sleep(time.Microsecond * 10)
    fmt.Println("end")
}

执行

[11:27:59 root@go codes]#go run goroutine.go 
START
A
B
C
D
E
F
...
END

1.4.1 在匿名函数中使用 sync.WaitGroup

在匿名函数中调用 sync.WaitGroup 有两种方法

1.4.1.1 方法一:直接调用

注意:由于这两个匿名函数的作用域都是在 main() 主函数里面的,所以能够直接调用

因为一个变量在当前作用域下没有找到它会向父作用域去找

而且这个 wg.Done() 使用的是父作用域里面的的 sync.WaitGroup{} 没有重新赋值也就不会在内存中出现赋值拷贝的过程,都是在对同一个 sync.WaitGroup{} 进行操作,所以能够使用值类型的结构体

但是尽量使用的方式比较好。

package main

import (
    "fmt"
    "sync"
)

func main() {
    fmt.Println("start")

    // 定义 sync.WaitGroup 一个 wg 的结构体
    wg := sync.WaitGroup{}

    // 添加两个计数信号量,因为有两个匿名函数
    wg.Add(2)
    go func() {
        for i := 0; i <= 10; i++ {
            fmt.Println(i)
        }

        // 调用 wg.Done() 执行完毕之后就减掉一个计数信号量,调用父作用域中的 wg.Done() 方法
        // 而且这个 wg.Done() 使用的是父作用域里面的的  sync.WaitGroup{} 没有重新赋值也就不会在内存中出现赋值拷贝的过程,都是在对同一个 sync.WaitGroup{} 进行操作,所以能够使用值类型的结构体
        wg.Done()
    }()

    go func() {
        for i := 'A'; i <= 'Z'; i++ {
            fmt.Printf("%c\n", i)
        }

         // 调用 wg.Done() 执行完毕之后就减掉一个计数信号量,调用父作用域中的 wg.Done() 方法
        wg.Done()
    }()

    wg.Wait()
    fmt.Println("end")
}

执行

[11:27:59 root@go codes]#go run goroutine.go 
START
A
B
C
D
E
F
...
END

当然也可以直接使用指针的方式传递 wg

package main

import (
    "fmt"
    "sync"
)

func main() {
    fmt.Println("START")
    wg := new(sync.WaitGroup)
    wg.Add(2)

    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println(i)
        }
        defer wg.Done()
    }()

    go func() {
        for i := 'A'; i < 'Z'; i++ {
            fmt.Println(string(i))
        }
        defer wg.Done()
    }()

    wg.Wait()
    fmt.Println("end")
}

1.4.1.2 方法二:传递 sync.WaitGroup 到匿名函数中

当然也可以将 sync.WaitGroup 直接传递到匿名结构体中,由于需要重新赋值会在内存中出现赋值拷贝的过程,所以直接传递的时候需要传递指针类型

因为他们是在不同的作用域下

package main

import (
    "fmt"
    "sync"
)

func main() {
    fmt.Println("start")
    wg := new(sync.WaitGroup)
    wg.Add(2)

    // 如果在匿名函数进行传递操作就需要传递 *sync.WaitGroup 指针类型
    // 因为重新赋值会在内存中出现赋值拷贝的过程
    go func(wg *sync.WaitGroup) {
        for i := 0; i <= 10; i++ {
            fmt.Println(i)
        }
        wg.Done()
    }(wg)

    go func(wg *sync.WaitGroup) {
        for i := 'A'; i <= 'Z'; i++ {
            fmt.Printf("%c\n", i)
        }
        wg.Done()
    }(wg)

    wg.Wait()
    fmt.Println("end")
}

执行

[11:27:59 root@go codes]#go run goroutine.go 
START
A
B
C
D
E
F
...
END

1.4.4 线程中的闭包陷阱

因为闭包使用函数外变量,当线程执行时,外部变量已经发生变化,导致打印内容不正确,可使用在创建线程时通过函数传递参数(值拷贝)方式避免

package main

import (
    "fmt"
    "sync"
)

func main() {
    wg := sync.WaitGroup{}

    // 使用 for 循环启动 3 个线程
    fmt.Println("start")
    for i := 0; i < 3; i++ {
        wg.Add(1)
        go func() {
            fmt.Println(i)
            wg.Done()
        }()
    }
    wg.Wait()
    fmt.Println("end")
}

// 主线程执行完了,工作线程才执行,这时候当主线程 3 次循环完了之后 i 已经变为了 3
// 然后再执行 3 个工作线程, 这是 i 已经变为了 3 所以输出的就是 3 

执行

[12:05:51 root@go codes]#go run closer.go 
start
3
3
3
end

当然我们可以通过值传递,将 i 传递到内部函数中成为自己的局部变量

package main

import (
    "fmt"
    "sync"
)

func main() {
    wg := sync.WaitGroup{}

    // 使用 for 循环启动 3 个线程
    fmt.Println("start")
    for i := 0; i < 3; i++ {
        wg.Add(1)
        go func(i int) {        // 将 i 传到到匿名函数中
            fmt.Println(i)
            wg.Done()
        }(i)                    // 将 i 传到到匿名函数中
    }
    wg.Wait()
    fmt.Println("end")
}

执行

[15:17:33 root@go codes]#go run closer.go 
start
2
0
1
end

# 执行就能够看到正常输出 0 1 2

1.5 互斥锁&原子操作(同步)

1.5.1 共享数据互斥锁机制

共享数据(同步)

多个并发程序需要对同一个资源进行访问,则需要先申请资源的访问权限,同时再使用完成后释放资源的访问权。当资源被其他程序已申请访问权后,程序应该等待访问权被释放并被申请时进行访问操作。同一时间资源只能被一个程序访问和操作


对全局变量加锁,当 协程1 对该 map 进行操作的时候就,对 map 空间先加上一把锁,然后 协程1 执行完了之后就解锁。

如果协程一还在操作这把锁,这时候 协程2 来看目前这把锁是不是加锁状态,如果是加锁状态 协程2 就去队列缓冲,这个队列其实就是一个数据结构。

这时候协程三也来了,但是还是加锁状态,协程三也就老老实实的跑去队列中

依此类推

如果 协程1 操作完了之后就会将这把锁解开,然后底层有一种机制,会从这个队列里面取出排在最前面的协程去工作,这样就好比商场排队结账。

1.5.1.1 不加锁范例

引出锁机制案例:

我们看这个示例,开启 20 个工作线程,其中 10 个线程分别给 salary += 10 加 1000 次,另外 10 个线程分别给 salary -= 10 减 1000 次。最后的结果 salary = 0

package main

import (
    "fmt"
    "runtime"
    "sync"
)

func main() {
    fmt.Println("start")
    // sync.WaitGroup 计数信号量赋值给 wg
    wg := sync.WaitGroup{}
    salary := 0

    // 循环 10 ,里面分别有两个工作线程,也就是开启了 20 个工作线程
    for i := 0; i < 10; i++ {

        // 这个 for 里面有两个工作线程,所以计数信号量为 2
        wg.Add(2)

        // 第一个工作线程 salary += 10 加 1000 次
        go func() {
            // 计数信号量减一
            defer wg.Done()
            for i := 0; i < 1000; i++ {
                salary += 10
                runtime.Gosched()   // GoSched 让线程主动让出 CPU
            }
        }()

        // 第二个工作线程 salary -= 10 减 1000 次
        go func() {
             // 计数信号量减一
            defer wg.Done()
            for i := 0; i < 1000; i++ {
                salary -= 10
                runtime.Gosched()   // GoSched 让线程主动让出 CPU
            }
        }()
    }

    wg.Wait()
    fmt.Println("end", salary)
}

执行

但是会发现每次 salary 输出的结果都不一样,和我们的预期结果是不一样的。

因为如下图,在计算机中, CPU 会把数据从内存拿到寄存器进行计算,但是计算机每次的执行顺序都是不同的,并不是说它会将第一个 for 循环中的 salary += 10 之后再执行下一个 for 循环中的数,而是有可能第一步执行把 salary 从 0 的时候放入到 寄存器中,第二把直接执行 salary -= 10 的操作,因为并发是多个协程之间同时执行

1.5.1.2 互斥锁范例

由此引出了互斥锁制,也就是当有 协程1 在对共享数据进行操作的时候,其他的协程就别操作。当 协程1 执行完毕之后其他的协程在进行操作

package main

import (
    "fmt"
    "runtime"
    "sync"
)

func main() {
    salary := 0
    wg := sync.WaitGroup{}

    // 定义互斥锁,使用 sync.Mutex 结构体
    var lock sync.Mutex

    fmt.Println("start")
    for i := 0; i < 1; i++ {
        wg.Add(2)
        go func() {
            defer wg.Done()
            for i := 0; i < 1000; i++ {
                lock.Lock()   // 第一个协程操作 salary 共享数据时加锁
                salary += 10  // 对 salary 共享数据进行操作
                lock.Unlock() // 操作 salary 共享数据完毕后解锁其他协程可以对共享数据进行操作
                runtime.Gosched()

            }
        }()

        go func() {
            defer wg.Done()
            for i := 0; i < 1000; i++ {
                lock.Lock()   // 第二个协程操作 salary 共享数据时加锁
                salary -= 10  // 对 salary 共享数据进行操作
                lock.Unlock() // 操作 salary 共享数据完毕后解锁
                runtime.Gosched()   

            }
        }()
    }
    wg.Wait()
    fmt.Println("end", salary)
}

执行发现不管我们运行多少变 salary 最后的值都是 0 ,这就是通过互斥锁的机制把对 salary 共享数据的操作,让不同工作线程之间同时只有一个工作线程对 salary 进行操作。


注意:

一般在进行加锁解锁的时候,都是把锁指定到共享数据的位置,也就是把锁控制在很小的一个范围内,这也是在做互斥锁的时候防止死锁的重要的一个方式,只在关键的步骤加锁。如果不是内部匿名函数的话,在其他函数执行互斥锁操作时,也需要把锁传入到其他函数中。

1.5.1.3 全局函数实现互斥锁

由于需要在全局函数实现互斥锁所以需要传入我们的 sync.WaitGroupsync.Mutex 两个结构体,但是结构体和函数是值类型,所以需要传入这两个结构体的指针类型,才能实现对同一个互斥锁和同一个共享空间的操作

package main

import (
    "fmt"
    "sync"
)

// 传入 *sync.WaitGroup,  *sync.Mutex, salary 的指针类型,实现对同一个互斥锁和同一个共享空间操作
func TaskA(wg *sync.WaitGroup, lock *sync.Mutex, salary *int) *int {
    defer wg.Done()
    for i := 0; i < 1000; i++ {

        // 加锁
        lock.Lock()
        *salary += i

        // 解锁
        lock.Unlock()
    }
    return salary
}

func TaskB(wg *sync.WaitGroup, lock *sync.Mutex, salary *int) *int {
    defer wg.Done()
    for i := 0; i < 1000; i++ {
        lock.Lock()
        *salary -= i
        lock.Unlock()
    }
    return salary
}

func main() {
    salary := 0
    wg := sync.WaitGroup{}
    // 定义互斥锁,使用 sync.Mutex 结构体
    var lock sync.Mutex

    fmt.Println("start")

    // 循环 10 执行两个协程,也就是开启了 20 个工作线程
    for i := 0; i < 10; i++ {
        wg.Add(2)
        go TaskA(&wg, &lock, &salary)
        go TaskB(&wg, &lock, &salary)
    }
    wg.Wait()
    fmt.Println("end", salary)
}

多次输出我们的 salary 结果都为 0

1.5.2 原子操作(了解)

其实我们互斥锁的底层就是一个原子操作的过程

原子操作是指程序执行过程不能中断的操作 ,go 语言 sync/atomic 包中提供提供了五类原子操作函数,其操作对象为整数型或整数指针

  • Add:增加/减少

  • Load:载入

  • Store:存储

  • Swap:两个变量之间交换

  • Swap:更新

  • CompareAndSwap:比较第一个参数引用值是否与第二个参数值相同,若相同则将第一

    个参数值更新为第三个参数

通过原子操作不用 lock 互斥锁的方式实现对共享数据的同步操作

package main

import (
    "fmt"
    "runtime"
    "sync"
    "sync/atomic"
)

func main() {
    var salary int64 = 0
    wg := sync.WaitGroup{}

    fmt.Println("start")
    for i := 0; i < 10; i++ {
        wg.Add(2)
        go func() {
            defer wg.Done()
            for i := 0; i < 1000; i++ {

                // 使用原子锁,当有协程在操作的时候其他协程不能够对 salary 共享数据进行操作
                // 共享数据必须传入指针
                atomic.AddInt64(&salary, +10)
                runtime.Gosched()
            }
        }()
        go func() {
            defer wg.Done()
            for i := 0; i < 1000; i++ {
                atomic.AddInt64(&salary, -10)
                runtime.Gosched()
            }
        }()
    }

    wg.Wait()
    fmt.Println("end", salary)
}

不管怎么运行他的结果都是不变的

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇