Go语言基础之并发

并发是编程里面一个非常重要的概念,Go语言在语言层面天生支持并发,这也是Go语言流行的一个很重要的原因。

Go语言中的并发编程

并发与并行

并发:同一时间段内执行多个任务(你在用微信和两个女朋友聊天)。

并行:同一时刻执行多个任务(你和你朋友都在用微信和女朋友聊天)。

Go语言的并发通过 goroutine实现。 goroutine类似于线程,属于用户态的线程,我们可以根据需要创建成千上万个 goroutine并发工作。 goroutine是由Go语言的运行时(runtime)调度完成,而线程是由操作系统调度完成。

Go语言还提供 channel在多个 goroutine间进行通信。 goroutinechannel是 Go 语言秉承的 CSP(Communicating Sequential Process)并发模式的重要实现基础。

goroutine

在java/c++中我们要实现并发编程的时候,我们通常需要自己维护一个线程池,并且需要自己去包装一个又一个的任务,同时需要自己去调度线程执行任务并维护上下文切换,这一切通常会耗费程序员大量的心智。那么能不能有一种机制,程序员只需要定义很多个任务,让系统去帮助我们把这些任务分配到CPU上实现并发执行呢?

Go语言中的 goroutine就是这样一种机制, goroutine的概念类似于线程,但 goroutine是由Go的运行时(runtime)调度和管理的。Go程序会智能地将 goroutine 中的任务合理地分配给每个CPU。Go语言之所以被称为现代化的编程语言,就是因为它在语言层面已经内置了调度和上下文切换的机制。

在Go语言编程中你不需要去自己写进程、线程、协程,你的技能包里只有一个技能– goroutine,当你需要让某个任务并发执行的时候,你只需要把这个任务包装成一个函数,开启一个 goroutine去执行这个函数就可以了,就是这么简单粗暴。

使用goroutine

Go语言中使用 goroutine非常简单,只需要在调用函数的时候在前面加上 go关键字,就可以为一个函数创建一个 goroutine

一个 goroutine必定对应一个函数,可以创建多个 goroutine去执行相同的函数。

启动单个goroutine

启动goroutine的方式非常简单,只需要在调用的函数(普通函数和匿名函数)前面加上一个 go关键字。

举个例子如下:

func hello() {
    fmt.Println("Hello Goroutine!")
}
func main() {
    hello()
    fmt.Println("main goroutine done!")
}

这个示例中hello函数和下面的语句是串行的,执行的结果是打印完 Hello Goroutine!后打印 main goroutine done!

接下来我们在调用hello函数前面加上关键字 go,也就是启动一个goroutine去执行hello这个函数。

func main() {
    go hello()

这一次的执行结果只打印了 main goroutine done!,并没有打印 Hello Goroutine!。为什么呢?

在程序启动时,Go程序就会为 main()函数创建一个默认的 goroutine

当main()函数返回的时候该 goroutine就结束了,所有在 main()函数中启动的 goroutine会一同结束, main函数所在的 goroutine就像是权利的游戏中的夜王,其他的 goroutine都是异鬼,夜王一死它转化的那些异鬼也就全部GG了。

所以我们要想办法让main函数等一等hello函数,最简单粗暴的方式就是 time.Sleep了。

func main() {
    go hello()

执行上面的代码你会发现,这一次先打印 main goroutine done!,然后紧接着打印 Hello Goroutine!

首先为什么会先打印 main goroutine done!是因为我们在创建新的goroutine的时候需要花费一些时间,而此时main函数所在的 goroutine是继续执行的。

启动多个goroutine

在Go语言中实现并发就是这样简单,我们还可以启动多个 goroutine。让我们再来一个例子: (这里使用了 sync.WaitGroup来实现goroutine的同步)

var wg sync.WaitGroup

func hello(i int) {
    defer wg.Done()

多次执行上面的代码,会发现每次打印的数字的顺序都不一致。这是因为10个 goroutine是并发执行的,而 goroutine的调度是随机的。

goroutine与线程

可增长的栈

OS线程(操作系统线程)一般都有固定的栈内存(通常为2MB),一个 goroutine的栈在其生命周期开始时只有很小的栈(典型情况下2KB), goroutine的栈不是固定的,他可以按需增大和缩小, goroutine的栈大小限制可以达到1GB,虽然极少会用到这个大。所以在Go语言中一次创建十万左右的 goroutine也是可以的。

goroutine调度

GPM是Go语言运行时(runtime)层面的实现,是go语言自己实现的一套调度系统。区别于操作系统调度OS线程。

  • G很好理解,就是个goroutine的,里面除了存放本goroutine信息外 还有与所在P的绑定等信息。
  • P管理着一组goroutine队列,P里面会存储当前goroutine运行的上下文环境(函数指针,堆栈地址及地址边界),P会对自己管理的goroutine队列做一些调度(比如把占用CPU时间较长的goroutine暂停、运行后续的goroutine等等)当自己的队列消费完了就去全局队列里取,如果全局队列里也消费完了会去其他P的队列里抢任务。
  • M(machine)是Go运行时(runtime)对操作系统内核线程的虚拟, M与内核线程一般是一一映射的关系, 一个groutine最终是要放到M上执行的;

P与M一般也是一一对应的。他们关系是: P管理着一组G挂载在M上运行。当一个G长久阻塞在一个M上时,runtime会新建一个M,阻塞G所在的P会把其他的G 挂载在新建的M上。当旧的G阻塞完成或者认为其已经死掉时 回收旧的M。

P的个数是通过 runtime.GOMAXPROCS设定(最大256),Go1.5版本之后默认为物理线程数。 在并发量大的时候会增加一些P和M,但不会太多,切换太频繁的话得不偿失。

单从线程调度讲,Go语言相比起其他语言的优势在于OS线程是由OS内核来调度的, goroutine则是由Go运行时(runtime)自己的调度器调度的,这个调度器使用一个称为m:n调度的技术(复用/调度m个goroutine到n个OS线程)。 其一大特点是goroutine的调度是在用户态下完成的, 不涉及内核态与用户态之间的频繁切换,包括内存的分配与释放,都是在用户态维护着一块大的内存池, 不直接调用系统的malloc函数(除非内存池需要改变),成本比调度OS线程低很多。 另一方面充分利用了多核的硬件资源,近似的把若干goroutine均分在物理线程上, 再加上本身goroutine的超轻量,以上种种保证了go调度方面的性能。

点我了解更多

GOMAXPROCS

Go运行时的调度器使用 GOMAXPROCS参数来确定需要使用多少个OS线程来同时执行Go代码。默认值是机器上的CPU核心数。例如在一个8核心的机器上,调度器会把Go代码同时调度到8个OS线程上(GOMAXPROCS是m:n调度中的n)。

Go语言中可以通过 runtime.GOMAXPROCS()函数设置当前程序并发时占用的CPU逻辑核心数。

Go1.5版本之前,默认使用的是单核心执行。Go1.5版本之后,默认使用全部的CPU逻辑核心数。

我们可以通过将任务分配到不同的CPU逻辑核心上实现并行的效果,这里举个例子:

func a() {
    for i := 1; i < 10; i++ {
        fmt.Println("A:", i)
    }
}

func b() {
    for i := 1; i < 10; i++ {
        fmt.Println("B:", i)
    }
}

func main() {
    runtime.GOMAXPROCS(1)
    go a()
    go b()
    time.Sleep(time.Second)
}

两个任务只有一个逻辑核心,此时是做完一个任务再做另一个任务。 将逻辑核心数设为2,此时两个任务并行执行,代码如下。

func a() {
    for i := 1; i < 10; i++ {
        fmt.Println("A:", i)
    }
}

func b() {
    for i := 1; i < 10; i++ {
        fmt.Println("B:", i)
    }
}

func main() {
    runtime.GOMAXPROCS(2)
    go a()
    go b()
    time.Sleep(time.Second)
}

Go语言中的操作系统线程和goroutine的关系:

  1. 一个操作系统线程对应用户态多个goroutine。
  2. go程序可以同时使用多个操作系统线程。
  3. goroutine和OS线程是多对多的关系,即m:n。

channel

单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。

虽然可以使用共享内存进行数据交换,但是共享内存在不同的 goroutine中容易发生竞态问题。为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。

Go语言的并发模型是 CSP&#xFF08;Communicating Sequential Processes&#xFF09;,提倡通过通信共享内存而不是通过共享内存而实现通信。

如果说 goroutine是Go程序并发的执行体, channel就是它们之间的连接。 channel是可以让一个 goroutine发送特定值到另一个 goroutine的通信机制。

Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。

channel类型

channel是一种类型,一种引用类型。声明通道类型的格式如下:

var 变量 chan 元素类型

举几个例子:

var ch1 chan int

创建channel

通道是引用类型,通道类型的空值是 nil

var ch chan int
fmt.Println(ch)

声明的通道后需要使用 make函数初始化之后才能使用。

创建channel的格式如下:

make(chan 元素类型, [缓冲大小])

channel的缓冲大小是可选的。

举几个例子:

ch4 := make(chan int)
ch5 := make(chan bool)
ch6 := make(chan []int)

channel操作

通道有发送(send)、接收(receive)和关闭(close)三种操作。

发送和接收都使用 <-< code>&#x7B26;&#x53F7;&#x3002;<!---<-->

现在我们先使用以下语句定义一个通道:

ch := make(chan int)

发送

将一个值发送到通道中。

ch 10

接收

从一个通道中接收值。

x :=

关闭

我们通过调用内置的 close函数来关闭通道。

close(ch)

关于关闭通道需要注意的事情是,只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭通道。通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。

关闭后的通道有以下特点:

  1. 对一个关闭的通道再发送值就会导致panic。
  2. 对一个关闭的通道进行接收会一直获取值直到通道为空。
  3. 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
  4. 关闭一个已经关闭的通道会导致panic。

无缓冲的通道

无缓冲的通道又称为阻塞的通道。我们来看一下下面的代码:

func main() {
    ch := make(chan int)
    ch 10
    fmt.Println("发送成功")
}

上面这段代码能够通过编译,但是执行的时候会出现以下错误:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
        .../src/github.com/Q1mi/studygo/day06/channel02/main.go:8 +0x54

为什么会出现 deadlock错误呢?

因为我们使用 ch := make(chan int)创建的是无缓冲的通道,无缓冲的通道只有在有人接收值的时候才能发送值。就像你住的小区没有快递柜和代收点,快递员给你打电话必须要把这个物品送到你的手中,简单来说就是无缓冲的通道必须有接收才能发送。

上面的代码会阻塞在 ch <- 10< code>&#x8FD9;&#x4E00;&#x884C;&#x4EE3;&#x7801;&#x5F62;&#x6210;&#x6B7B;&#x9501;&#xFF0C;&#x90A3;&#x5982;&#x4F55;&#x89E3;&#x51B3;&#x8FD9;&#x4E2A;&#x95EE;&#x9898;&#x5462;&#xFF1F;<!----->

一种方法是启用一个 goroutine去接收值,例如:

func recv(c chan int) {
    ret := .Println("接收成功", ret)
}
func main() {
    ch := make(chan int)
    go recv(ch)

无缓冲通道上的发送操作会阻塞,直到另一个 goroutine在该通道上执行接收操作,这时值才能发送成功,两个 goroutine将继续执行。相反,如果接收操作先执行,接收方的goroutine将阻塞,直到另一个 goroutine在该通道上发送一个值。

使用无缓冲通道进行通信将导致发送和接收的 goroutine同步化。因此,无缓冲通道也被称为 &#x540C;&#x6B65;&#x901A;&#x9053;

有缓冲的通道

解决上面问题的方法还有一种就是使用有缓冲区的通道。我们可以在使用make函数初始化通道的时候为其指定通道的容量,例如:

func main() {
    ch := make(chan int, 1)

只要通道的容量大于零,那么该通道就是有缓冲的通道,通道的容量表示通道中能存放元素的数量。就像你小区的快递柜只有那么个多格子,格子满了就装不下了,就阻塞了,等到别人取走一个快递员就能往里面放一个。

我们可以使用内置的 len函数获取通道内元素的数量,使用 cap函数获取通道的容量,虽然我们很少会这么做。

for range从通道循环取值

当向通道中发送完数据时,我们可以通过 close函数来关闭通道。

当通道被关闭时,再往该通道发送值会引发panic,从该通道里接收的值一直都是类型零值。那如何判断一个通道是否被关闭了呢?

我们来看下面这个例子:

从上面的例子中我们看到有两种方式在接收值的时候判断该通道是否被关闭,不过我们通常使用的是 for range的方式。使用 for range遍历通道,当通道被关闭的时候就会退出 for range

单向通道

有的时候我们会将通道作为参数在多个任务函数间传递,很多时候我们在不同的任务函数中使用通道都会对其进行限制,比如限制通道在函数中只能发送或只能接收。

Go语言中提供了单向通道来处理这种情况。例如,我们把上面的例子改造如下:

func counter(out chanint) {
    for i := 0; i < 100; i++ {
        out }
    close(out)
}

func squarer(out chanint, in chan int) {
    for i := range in {
        out * i
    }
    close(out)
}
func printer(in chan int) {
    for i := range in {
        fmt.Println(i)
    }
}

func main() {
    ch1 := make(chan int)
    ch2 := make(chan int)
    go counter(ch1)
    go squarer(ch2, ch1)
    printer(ch2)
}

其中,

  • chan<- int< code>&#x662F;&#x4E00;&#x4E2A;&#x53EA;&#x80FD;&#x53D1;&#x9001;&#x7684;&#x901A;&#x9053;&#xFF0C;&#x53EF;&#x4EE5;&#x53D1;&#x9001;&#x4F46;&#x662F;&#x4E0D;&#x80FD;&#x63A5;&#x6536;&#xFF1B;<!----->
  • <-chan int< code>&#x662F;&#x4E00;&#x4E2A;&#x53EA;&#x80FD;&#x63A5;&#x6536;&#x7684;&#x901A;&#x9053;&#xFF0C;&#x53EF;&#x4EE5;&#x63A5;&#x6536;&#x4F46;&#x662F;&#x4E0D;&#x80FD;&#x53D1;&#x9001;&#x3002;<!---chan-->

在函数传参及任何赋值操作中将双向通道转换为单向通道是可以的,但反过来是不可以的。

通道总结

channel常见的异常总结,如下图:

Go语言基础之并发

关闭已经关闭的 channel也会引发 panic

worker pool(goroutine池)

在工作中我们通常会使用可以指定启动的goroutine数量– worker pool模式,控制 goroutine的数量,防止 goroutine泄漏和暴涨。

一个简易的 work pool示例代码如下:

func worker(id int, jobs chan int, results chanint) {
    for j := range jobs {
        fmt.Printf("worker:%d start job:%d\n", id, j)
        time.Sleep(time.Second)
        fmt.Printf("worker:%d end job:%d\n", id, j)
        results * 2
    }
}

func main() {
    jobs := make(chan int, 100)
    results := make(chan int, 100)

select多路复用

在某些场景下我们需要同时从多个通道接收数据。通道在接收数据时,如果没有数据可以接收将会发生阻塞。你也许会写出如下代码使用遍历的方式来实现:

for{

这种方式虽然可以实现从多个通道接收值的需求,但是运行性能会差很多。为了应对这种场景,Go内置了 select关键字,可以同时响应多个通道的操作。

select的使用类似于switch语句,它有一系列case分支和一个默认的分支。每个case会对应一个通道的通信(接收或发送)过程。 select会一直等待,直到某个 case的通信操作完成时,就会执行 case分支对应的语句。具体格式如下:

select{
    case :
        ...

    case data := :
        ...

    case ch3:
        ...

    default:
        默认操作
}

举个小例子来演示下 select的使用:

func main() {
    ch := make(chan int, 1)
    for i := 0; i < 10; i++ {
        select {
        case x := :
            fmt.Println(x)
        case ch :
        }
    }
}

使用 select语句能提高代码的可读性。

  • 可处理一个或多个channel的发送/接收操作。
  • 如果多个 case同时满足, select会随机选择一个。
  • 对于没有 caseselect{}会一直等待,可用于阻塞main函数。

并发安全和锁

有时候在Go代码中可能会存在多个 goroutine同时操作一个资源(临界区),这种情况会发生 &#x7ADE;&#x6001;&#x95EE;&#x9898;(数据竞态)。类比现实生活中的例子有十字路口被各个方向的的汽车竞争;还有火车上的卫生间被车厢里的人竞争。

举个例子:

var x int64
var wg sync.WaitGroup

func add() {
    for i := 0; i < 5000; i++ {
        x = x + 1
    }
    wg.Done()
}
func main() {
    wg.Add(2)
    go add()
    go add()
    wg.Wait()
    fmt.Println(x)
}

上面的代码中我们开启了两个 goroutine去累加变量x的值,这两个 goroutine在访问和修改 x变量的时候就会存在数据竞争,导致最后的结果与期待的不符。

互斥锁

互斥锁是一种常用的控制共享资源访问的方法,它能够保证同时只有一个 goroutine可以访问共享资源。Go语言中使用 sync包的 Mutex类型来实现互斥锁。 使用互斥锁来修复上面代码的问题:

var x int64
var wg sync.WaitGroup
var lock sync.Mutex

func add() {
    for i := 0; i < 5000; i++ {
        lock.Lock()

使用互斥锁能够保证同一时间有且只有一个 goroutine进入临界区,其他的 goroutine则在等待锁;当互斥锁释放后,等待的 goroutine才可以获取锁进入临界区,多个 goroutine同时等待一个锁时,唤醒的策略是随机的。

读写互斥锁

互斥锁是完全互斥的,但是有很多实际的场景下是读多写少的,当我们并发的去读取一个资源不涉及资源修改的时候是没有必要加锁的,这种场景下使用读写锁是更好的一种选择。读写锁在Go语言中使用 sync包中的 RWMutex类型。

读写锁分为两种:读锁和写锁。当一个goroutine获取读锁之后,其他的 goroutine如果是获取读锁会继续获得锁,如果是获取写锁就会等待;当一个 goroutine获取写锁之后,其他的 goroutine无论是获取读锁还是写锁都会等待。

读写锁示例:

var (
    x      int64
    wg     sync.WaitGroup
    lock   sync.Mutex
    rwlock sync.RWMutex
)

func write() {

需要注意的是读写锁非常适合读多写少的场景,如果读和写的操作差别不大,读写锁的优势就发挥不出来。

sync.WaitGroup

在代码中生硬的使用 time.Sleep肯定是不合适的,Go语言中可以使用 sync.WaitGroup来实现并发任务的同步。 sync.WaitGroup有以下几个方法:

方法名功能 (wg * WaitGroup) Add(delta int) 计数器+delta (wg WaitGroup) Done() 计数器-1 (wg WaitGroup) Wait() 阻塞直到计数器变为0

sync.WaitGroup内部维护着一个计数器,计数器的值可以增加和减少。例如当我们启动了N 个并发任务时,就将计数器值增加N。每个任务完成时通过调用Done()方法将计数器减1。通过调用Wait()来等待并发任务执行完,当计数器值为0时,表示所有并发任务已经完成。

我们利用 sync.WaitGroup将上面的代码优化一下:

var wg sync.WaitGroup

func hello() {
    defer wg.Done()
    fmt.Println("Hello Goroutine!")
}
func main() {
    wg.Add(1)
    go hello()

需要注意 sync.WaitGroup是一个结构体,传递的时候要传递指针。

sync.Once

说在前面的话:这是一个进阶知识点。

在编程的很多场景下我们需要确保某些操作在高并发的场景下只执行一次,例如只加载一次配置文件、只关闭一次通道等。

Go语言中的 sync包中提供了一个针对只执行一次场景的解决方案– sync.Once

sync.Once只有一个 Do方法,其签名如下:

func (o *Once) Do(f func()) {}

备注:如果要执行的函数 f 需要传递参数就需要搭配闭包来使用。

加载配置文件示例

延迟一个开销很大的初始化操作到真正用到它的时候再执行是一个很好的实践。因为预先初始化一个变量(比如在init函数中完成初始化)会增加程序的启动耗时,而且有可能实际执行过程中这个变量没有用上,那么这个初始化操作就不是必须要做的。我们来看一个例子:

var icons map[string]image.Image

func loadIcons() {
    icons = map[string]image.Image{
        "left":  loadIcon("left.png"),
        "up":    loadIcon("up.png"),
        "right": loadIcon("right.png"),
        "down":  loadIcon("down.png"),
    }
}

多个 goroutine并发调用Icon函数时不是并发安全的,现代的编译器和CPU可能会在保证每个 goroutine都满足串行一致的基础上自由地重排访问内存的顺序。loadIcons函数可能会被重排为以下结果:

func loadIcons() {
    icons = make(map[string]image.Image)
    icons["left"] = loadIcon("left.png")
    icons["up"] = loadIcon("up.png")
    icons["right"] = loadIcon("right.png")
    icons["down"] = loadIcon("down.png")
}

在这种情况下就会出现即使判断了 icons不是nil也不意味着变量初始化完成了。考虑到这种情况,我们能想到的办法就是添加互斥锁,保证初始化 icons的时候不会被其他的 goroutine操作,但是这样做又会引发性能问题。

使用 sync.Once改造的示例代码如下:

var icons map[string]image.Image

var loadIconsOnce sync.Once

func loadIcons() {
    icons = map[string]image.Image{
        "left":  loadIcon("left.png"),
        "up":    loadIcon("up.png"),
        "right": loadIcon("right.png"),
        "down":  loadIcon("down.png"),
    }
}

sync.Once其实内部包含一个互斥锁和一个布尔值,互斥锁保证布尔值和数据的安全,而布尔值用来记录初始化是否完成。这样设计就能保证初始化操作的时候是并发安全的并且初始化操作也不会被执行多次。

sync.Map

Go语言中内置的map不是并发安全的。请看下面的示例:

var m = make(map[string]int)

func get(key string) int {
    return m[key]
}

func set(key string, value int) {
    m[key] = value
}

func main() {
    wg := sync.WaitGroup{}
    for i := 0; i < 20; i++ {
        wg.Add(1)
        go func(n int) {
            key := strconv.Itoa(n)
            set(key, n)
            fmt.Printf("k=:%v,v:=%v\n", key, get(key))
            wg.Done()
        }(i)
    }
    wg.Wait()
}

上面的代码开启少量几个 goroutine的时候可能没什么问题,当并发多了之后执行上面的代码就会报 fatal error: concurrent map writes错误。

像这种场景下就需要为map加锁来保证并发的安全性了,Go语言的 sync包中提供了一个开箱即用的并发安全版map– sync.Map。开箱即用表示不用像内置的map一样使用make函数初始化就能直接使用。同时 sync.Map内置了诸如 StoreLoadLoadOrStoreDeleteRange等操作方法。

var m = sync.Map{}

func main() {
    wg := sync.WaitGroup{}
    for i := 0; i < 20; i++ {
        wg.Add(1)
        go func(n int) {
            key := strconv.Itoa(n)
            m.Store(key, n)
            value, _ := m.Load(key)
            fmt.Printf("k=:%v,v:=%v\n", key, value)
            wg.Done()
        }(i)
    }
    wg.Wait()
}

原子操作

代码中的加锁操作因为涉及内核态的上下文切换会比较耗时、代价比较高。针对基本数据类型我们还可以使用原子操作来保证并发安全,因为原子操作是Go语言提供的方法它在用户态就可以完成,因此性能比加锁操作更好。Go语言中原子操作由内置的标准库 sync/atomic提供。

atomic包

方法解释 func LoadInt32(addr *int32) (val int32)

func LoadInt64(addr *int64) (val int64)

func LoadUint32(addr *uint32) (val uint32)

func LoadUint64(addr *uint64) (val uint64)

func LoadUintptr(addr *uintptr) (val uintptr)

func LoadPointer(addr unsafe.Pointer) (val unsafe.Pointer) 读取操作 func StoreInt32(addr int32, val int32)

func StoreInt64(addr *int64, val int64)

func StoreUint32(addr *uint32, val uint32)

func StoreUint64(addr *uint64, val uint64)

func StoreUintptr(addr *uintptr, val uintptr)

func StorePointer(addr unsafe.Pointer, val unsafe.Pointer) 写入操作 func AddInt32(addr int32, delta int32) (new int32)

func AddInt64(addr *int64, delta int64) (new int64)

func AddUint32(addr *uint32, delta uint32) (new uint32)

func AddUint64(addr *uint64, delta uint64) (new uint64)

func AddUintptr(addr uintptr, delta uintptr) (new uintptr) 修改操作 func SwapInt32(addr int32, new int32) (old int32)

func SwapInt64(addr *int64, new int64) (old int64)

func SwapUint32(addr *uint32, new uint32) (old uint32)

func SwapUint64(addr *uint64, new uint64) (old uint64)

func SwapUintptr(addr *uintptr, new uintptr) (old uintptr)

func SwapPointer(addr unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer) 交换操作 func CompareAndSwapInt32(addr int32, old, new int32) (swapped bool)

func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool)

func CompareAndSwapUint32(addr *uint32, old, new uint32) (swapped bool)

func CompareAndSwapUint64(addr *uint64, old, new uint64) (swapped bool)

func CompareAndSwapUintptr(addr *uintptr, old, new uintptr) (swapped bool)

func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool) 比较并交换操作

示例

我们填写一个示例来比较下互斥锁和原子操作的性能。

var x int64
var l sync.Mutex
var wg sync.WaitGroup

atomic包提供了底层的原子级内存操作,对于同步算法的实现很有用。这些函数必须谨慎地保证正确使用。除了某些特殊的底层应用,使用通道或者sync包的函数/类型实现同步更好。

练习题

  1. 使用 goroutinechannel实现一个计算int64随机数各位数和的程序。
  2. 开启一个 goroutine循环生成int64类型的随机数,发送到 jobChan
  3. 开启24个 goroutinejobChan中取出随机数计算各位数的和,将结果发送到 resultChan
  4. goroutineresultChan取出结果并打印到终端输出
  5. 为了保证业务代码的执行性能将之前写的日志库改写为异步记录日志方式。

Original: https://www.cnblogs.com/saolv/p/12008763.html
Author: 扫驴
Title: Go语言基础之并发

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/535754/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • 新作:轻量级Golang IoC容器——iocgo

    习惯于Java或者C#开发的人应该对控制反转与依赖注入应该再熟悉不过了。在Java平台有鼎鼎大名的Spring框架,在C#平台有Autofac,Unity,Windsor等,我当年…

    Go语言 2023年5月25日
    063
  • Go学习【02】:理解Gin,搭一个web demo

    Go Gin 框架 说Gin是一个框架,不如说Gin是一个类库或者工具库,其包含了可以组成框架的组件。这样会更好理解一点。 举个🌰 端口监听 用于监听请求,也就是服务 请求处理 请…

    Go语言 2023年5月25日
    065
  • 【Go实战基础】数组实战,程序员的基本功

    数组实战,程序员的基本功。 实战需求: 输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有奇数位于数组的前半部分,所有偶数位于数组的后半部分。 实战思路: 1、先声明…

    Go语言 2023年5月25日
    026
  • go语言学习笔记

    最近一直在学习go语言,因此打算学习的时候能够记录一下笔记。我这个人之前是从来没有记录笔记的习惯,一直以来都是靠强大的记忆力去把一些要点记住。读书的时候因为一直都是有一个很安静和很…

    Go语言 2023年5月29日
    043
  • Go微服务框架go-kratos学习05:分布式链路追踪 OpenTelemetry 使用

    一、分布式链路追踪发展简介 1.1 分布式链路追踪介绍 关于分布式链路追踪的介绍,可以查看我前面的文章 微服务架构学习与思考(09):分布式链路追踪系统-dapper论文学习(ht…

    Go语言 2023年5月25日
    041
  • Go语言中单个字符char rune

    Go 语言的字符使用UTF-8 编码 *英文字母 1&#x4E2A; 字节, 汉子 3&#x4E2A; 字节 golang的字符称为rune,等价于C中的char,…

    Go语言 2023年5月29日
    042
  • Go语言之高级篇beego框架之配置beego环境

    1、配置beego环境 进入部署目录中 软件部署目录: 把要部署的软件代码,放在src目录下面。 启动项目 posted @2019-02-18 15:31 努力哥 阅读(938 …

    Go语言 2023年5月29日
    063
  • Go Micro Dashboard – 简介

    前言 使用Go Micro开发微服务系统很久了,但是一直没有很好的可视化工具用于开发和监控微服务系统。 所以基于go-micro和ng-alain开发了Go Micro Dashb…

    Go语言 2023年5月25日
    040
  • 浅谈MatrixOne如何用Go语言设计与实现高性能哈希表

    目录 MatrixOne数据库是什么? 哈希表数据结构基础 哈希表基本设计与对性能的影响 碰撞处理 链地址法 开放寻址法 Max load factor Growth factor…

    Go语言 2023年5月25日
    045
  • Go sort包

    sort包简介 官方文档Golang的sort包用来排序,二分查找等操作。本文主要介绍sort包里常用的函数,通过实例代码来快速学会使用sort包 sort包内置函数 sort.I…

    Go语言 2023年5月25日
    091
  • Operator 示例:使用 Redis 部署 PHP 留言板应用程序

    安装 Docker Desktop,并启动内置的 Kubernetes 集群 注册一个hub.docker.com 账户,需要将本地构建好的镜像推送至公开仓库中 安装 operat…

    Go语言 2023年5月25日
    060
  • 推荐 10 本 Go 经典书籍,从入门到进阶(含下载方式)

    书单一共包含 10 本书,分为入门 5 本,进阶 5 本。我读过其中 7 本,另外 3 本虽然没读过,但也是网上推荐比较多的。 虽然分了入门和进阶,但是很多书中这两部分内容是都包含…

    Go语言 2023年5月25日
    062
  • Sentinel-Go 源码系列(一)|开篇

    大家好呀,打算写一个 Go 语言组件源码分析系列,一是为了能学习下 Go 语言,看下别人是怎么写 Go 的,二是也掌握一个组件。 本次选择了 Sentinel-Go,一是对 Jav…

    Go语言 2023年5月25日
    053
  • 十分钟学会Golang开发gRPC服务

    gRPC是Google发起的一个开源RPC框架,使用HTTP/2传输协议,使用Protocol Buffers编码协议,相比RESTful框架的程序性能提高不少,而且当前流行的编程…

    Go语言 2023年5月25日
    066
  • Go – 如何编写 ProtoBuf 插件(二)?

    上篇文章《Go – 如何编写 ProtoBuf 插件 (一) 》,分享了使用 proto3 的 &#x81EA;&#x5B9A;&#x4E49;…

    Go语言 2023年5月25日
    050
  • DDIA 学习笔记

    第一章 可靠性、可扩展性、可维护性 ​ 可靠性: 系统在 困境(adversity)(硬件故障、软件故障、人为错误)中仍可正常工作(正确完成功能,并能达到期望的性能水准。 ​ 可靠…

    Go语言 2023年5月25日
    066
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球