Go语言之Go语言并发

Go 语言并发

Golang从语言层面就对并发提供了支持,而goruntine是Go语言并发设计的核心。

Go语言的并发机制运用起来非常舒适,在启动并发的方式上直接添加了语言级的关键字就可以实现,和其他编程语言相比更加轻量。

进程&线程

A、进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。

B、线程是进程的一个执行实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。

C、一个进程可以创建和撤销多个线程;同一个进程中的多个线程之间可以并发执行。

并发&并行

A、多线程程序在一个核的cpu上运行,就是并发。

B、多线程程序在多个核的cpu上运行,就是并行。

并发不是并行:

并发主要由切换时间片来实现"同时"运行,并行则是直接利用多核实现多线程的运行,Go程序可以设置使用核数,以发挥多核计算机的能力。

协程&线程

协程:独立的栈空间,共享堆空间,调度由用户自己控制,本质上有点类似于用户级线程,这些用户级线程的调度也是自己实现的。

线程:一个线程上可以跑多个协程,协程是轻量级的线程。

Goroutine 介绍

goroutine 只是由官方实现的超级"线程池"。每个实力4~5KB的栈内存占用和由于实现机制而大幅减少的创建和销毁开销是Go语言高并发的根本原因。

goroutine 奉行通过通信来共享内存,而不是共享内存来通信。只需在函数调用语句前添加 go 关键字,就可创建并发执行单元。开发人员无需了解任何执行细节,调度器会自动将其安排到合适的系统线程上执行。goroutine 是一种非常轻量级的实现,可在单个进程里执行成千上万的并发任务。

事实上,入口函数 main 就以 goroutine 运行。另有与之配套的 channel 类型,用以实现 "以通讯来共享内存" 的 CSP 模式。

goroutine 是通过 Go 的 runtime管理的一个线程管理器

package main

import (
	"fmt"
	"time"
)

func main() {
	go func() {
		fmt.Println("hello word")
	}()
	time.Sleep(1 * time.Second)
}

进入 main 函数开启一个 goroutine 运行匿名函数函数体内容:fmt.Println("Hello, World!") 。主线程执行 time.Sleep(1 * time.Second) 等待 1 秒。goroutine 执行完毕回到主线程,主线程的sleep 完成结束程序。

注意:若去掉 time.Sleep(1 * time.Second) 这段代码,进入 main 函数开启一个 goroutine,没等 goroutine 运行匿名函数函数体内容,主线程已经完成结束程序。

Go语言Chan应用

Channel 是 CSP 模式的具体实现,用于多个 goroutine 通讯。其内部实现了同步,确保并发安全。

Channel 是先进先出,线程安全的,多个goroutine同时访问,不需要加锁。

chan 阻塞

我们定义的管道 intChan 容量是5,开启 goroutine 写入10条数据,在写满5条数据时会阻塞,而 read() 每秒会从 intChan 中读取一条,然后write() 再会写入一条数据。

package main

import (
	"fmt"
	"time"
)

func write(ch chan int) {
	for i := 0; i < 10; i++ {
		ch <- i
		fmt.Println("write data:", i)
	}
}
func read(ch chan int) {
	for {
		i := <-ch
		fmt.Println("read data:", i)
		time.Sleep(time.Second)
	}
}
func main() {
	intChan := make(chan int, 5)
	go write(intChan)
	go read(intChan)

	time.Sleep(10 * time.Second)
}

同步模式

默认为同步模式,需要发送和接收配对。否则会被阻塞,直到另一方准备好后被唤醒。

package main

import "fmt"

func main() {
	data := make(chan string) // 数据交换队列
	exit := make(chan bool)   // 退出通知

	go func() {
		for d := range data { // 从队列迭代接收数据,直到 close 。
			fmt.Println(d)
		}
		fmt.Println("received over")
		exit <- true // 发出退出通知。
	}()
	data <- "oldboy" // 发送数据。
	data <- "Linux"
	data <- "GOlang"
	data <- "python"
	close(data) // 关闭队列。
	fmt.Println("send over")
	<-exit // 等待退出通知。
}

异步模式

异步方式通过判断缓冲区来决定是否阻塞。如果缓冲区已满,发送被阻塞;缓冲区为空,接收被阻塞。

通常情况下,异步 channel 可减少排队阻塞,具备更高的效率。但应该考虑使用指针规避大对象拷贝,将多个元素打包,减小缓冲区大小。

package main

import "fmt"

func main() {
	data := make(chan string, 3) // 缓冲区可以存储 3 个元素
	exit := make(chan bool)

	data <- "old boy" // 在缓冲区未满前,不会阻塞。
	data <- "python"
	data <- "linux"

	go func() {
		for d := range data { // 在缓冲区未空前,不会阻塞。
			fmt.Println(d)
		}
		//  表示读取出data通道中数据
		exit <- true
	}()
	data <- "java" // 如果缓冲区已满,阻塞。
	data <- "C"
	close(data)
	<-exit
}

chan 选择

如果需要同时处理多个 channel,可使用 select 语句。它随机选择一个可用 channel 做收发操作,或执行 default case。

用 select 实现超时控制:

package main

import (
	"fmt"
	"time"
)

func main() {
	exit := make(chan bool)
	intChan := make(chan int, 2)
	strChan := make(chan string, 2)

	go func() {
		select {
		case vi := <-intChan:
			fmt.Println(vi)
		case vs := <-strChan:
			fmt.Println(vs)
		case <-time.After(time.Second * 3):
			fmt.Println("timeout.")
		}

		exit <- true
	}()

	// intChan <- 100 // 注释掉,引发 timeout。
	// strChan <- "oldboy"

	<-exit
}

在循环中使用 select default case 需要小心,避免形成洪水。

简单工厂模式

用简单工厂模式打包并发任务和 channel。

package main

import (
	"fmt"
	"math/rand"
	"time"
)

func NewTest() chan int {
	c := make(chan int)
	rand.Seed(time.Now().UnixNano())
	go func() {
		time.Sleep(time.Second)
		c <- rand.Int()
	}()
	return c
}
func main() {
	t := NewTest()
	fmt.Println(<-t) // 等待 goroutine 结束返回。
}

Go 语言WaitGroup

WaitGroup能够一直等到所有的goroutine执行完成,并且阻塞主线程的执行,直到所有的goroutine执行完成。

WaitGroup总共有三个方法:Add(delta int),Done(),Wait()。简单的说一下这三个方法的作用。

Add:添加或者减少等待goroutine的数量;

Done:相当于Add(-1);

Wait:执行阻塞,直到所有的WaitGroup数量变成 0;

WaitGroup用于线程同步,WaitGroup等待一组线程集合完成,才会继续向下执行。 主线程(goroutine)调用Add来设置等待的线程(goroutine)数量。 然后每个线程(goroutine)运行,并在完成后调用Done。 同时,Wait用来阻塞,直到所有线程(goroutine)完成才会向下执行。

WaitGroup实例如下:

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(n int) {
            // defer wg.Done()
            defer wg.Add(-1)
            EchoNum(n)
        }(i)
    }
    wg.Wait()
}

func EchoNum(i int) {
    time.Sleep(time.Second)
    fmt.Println(i)
}

程序中将每次循环的数量 sleep 1 秒钟后输出。如果程序不使用WaitGroup,将不会输出结果。因为goroutine还没执行完,主线程已经执行完毕。

注掉的 defer wg.Done() 和 defer wg.Add(-1) 作用一样。

WaitGroup应用

一、用 channel 实现信号量 (semaphore)。

package main

import (
	"sync"
)

func main() {
	wg := sync.WaitGroup{}
	wg.Add(3) //增加三个线程
	sem := make(chan int, 1)
	for i := 0; i < 3; i++ {
		go func(id int) {
			defer wg.Done() //减少一个线程
			sem <- 1        // 向 sem 发送数据,阻塞或者成功。
			for x := 0; x < 3; x++ {
				println(id, x)
			}
			<-sem // 接收数据,使得其他阻塞 goroutine 可以发送数据
		}(i)
	}
	wg.Wait()

}

D:\goprogram\go\src\day10
λ go run test.go
0 0
0 1
0 2
1 0
1 1
1 2
2 0
2 1
2 2

二、用 closed channel 发出退出通知。

package main

import (
	"sync"
	"time"
)

func main() {
	wg := sync.WaitGroup{}
	exit := make(chan bool)
	for i := 0; i < 2; i++ {
		wg.Add(1)
		go func(n int) {
			defer wg.Done()
			task := func() {
				println(n, time.Now().String())
				time.Sleep(1 * time.Second)
			}
			for {
				select {
				case <-exit: // closed channel 不会阻塞,因此可用作退出通知。
					return
				default: // 执行正常任务。
					task()
				}
			}
		}(i)
	}
	time.Sleep(time.Second * 3) // 让测试 goroutine 运行一会。
	close(exit)                 // 发出退出通知。
	wg.Wait()
}

WaitGroup陷阱

一、add 数量小于done数量导致 WaitGroup数量为负数

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    var wg sync.WaitGroup
    wg.Add(1)

    oldboy := func() {
        time.Sleep(time.Second)
        fmt.Println("The old boy welcomes you.")
        wg.Done()
    }

    go oldboy()
    go oldboy()
    go oldboy()

    time.Sleep(time.Second * 3)
    wg.Wait()
}

运行错误:

panic: sync: negative WaitGroup counter

二、add 数量大于 done 数量造成 deadlock

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    var wg sync.WaitGroup
    wg.Add(4)

    oldboy := func() {
        time.Sleep(time.Second)
        fmt.Println("The old boy welcomes you.")
        wg.Done()
    }

    go oldboy()
    go oldboy()
    go oldboy()

    time.Sleep(time.Second * 3)
    wg.Wait()
}

运行错误:

fatal error: all goroutines are asleep - deadlock!

三、跳过 add 和 Done 操作,直接执行 Wait

package main

import (
    "fmt"
    "sync"
)

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

    for i := 0; i < 5; i++ {
        go func(wg sync.WaitGroup, i int) {
            wg.Add(1)
            fmt.Printf("i=>%d\n", i)
            wg.Done()
        }(wg, i)
    }
    wg.Wait()
    fmt.Println("exit")
}

WaitGroup 同步的是 goroutine, 而上面的代码却在 goroutine 中进行 Add(1) 操作。因此,可能在这些 goroutine 还没来得及 Add(1) 就已经执行 Wait 操作了。

四、WaitGroup 拷贝传值问题

package main

import (
    "fmt"

    "sync"
)

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

    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(wg sync.WaitGroup, i int) {
            fmt.Printf("i=>%d\n", i)
            wg.Done()
        }(wg, i)
    }
    wg.Wait()
}

运行错误:

fatal error: all goroutines are asleep - deadlock!

wg 给拷贝传递到了 goroutine 中,导致只有 Add 操作,其实 Done操作是在 wg 的副本执行的,因此 Wait 就死锁了。

正确代码实例如下:

package main

import (
    "fmt"

    "sync"
)

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

    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(wg *sync.WaitGroup, i int) {
            fmt.Printf("i=>%d\n", i)
            wg.Done()
        }(wg, i)
    }
    wg.Wait()
}

Go 语言runtime

runtime包提供Go语言运行时的系统交互的操作,例如控制goruntine的功能。

调度器不能保证多个 goroutine 执行次序,且进程退出时不会等待它们结束。

默认情况下,进程启动后仅允许一个系统线程服务于 goroutine。可使用环境变量或标准库函数 runtime.GOMAXPROCS 修改,让调度器用多个线程实现多核并行,而不仅仅是并发。

runtime包常用方法

const GOOS string = theGoos

GOOS是可执行程序的目标操作系统(将要在该操作系统的机器上执行):darwin、freebsd、linux等。

func Gosched()

Gosched使当前go程放弃处理器,以让其它go程运行。它不会挂起当前go程,因此当前go程未来会恢复执行。

func NumCPU() int

NumCPU返回本地机器的逻辑CPU个数。

func GOROOT() string

GOROOT返回Go的根目录。如果存在GOROOT环境变量,返回该变量的值;否则,返回创建Go时的根目录。

func GOMAXPROCS(n int) int

GOMAXPROCS设置可同时执行的最大CPU数,并返回先前的设置。 若 n < 1,它就不会更改当前设置。本地机器的逻辑CPU数可通过 NumCPU 查询。本函数在调度程序优化后会去掉。

func Goexit()

Goexit终止调用它的go程。其它go程不会受影响。Goexit会在终止该go程前执行所有defer的函数。

在程序的main go程调用本函数,会终结该go程,而不会让main返回。因为main函数没有返回,程序会继续执行其它的go程。如果所有其它go程都退出了,程序就会崩溃。

func NumGoroutine() int

NumGoroutine返回当前存在的Go程数。

runtime包应用

一、查看机器的逻辑CPU个数、Go的根目录、操作系统

package main

import "runtime"

func main() {
	println("cpu:", runtime.NumCPU())
	println("go:", runtime.GOROOT())
	println("操作系统:", runtime.GOOS)

}

D:\goprogram\go\src\day10
λ go run test.go
cpu: 4
go: D:\go\go
操作系统: windows

二、GOMAXPROCS 设置golang运行的cpu核数

Golang 默认所有任务都运行在一个 cpu 核里,如果要在 goroutine 中使用多核,可以使用 runtime.GOMAXPROCS 函数修改,当参数小于 1 时使用默认值。

package main

import (
    "fmt"
    "runtime"
)

var (
    signal = false
)

func oldboy() {
    signal = true
}

func init() {
    runtime.GOMAXPROCS(1)
}

func main() {
    go oldboy()
    for {
        if signal {
            break
        }
    }
    fmt.Println("end")
}

上述代码单核执行如果for前面或者中间不延迟,主线程不会让出CPU,导致异步的线程无法执行,从而无法设置signal的值,从而出现死循环。

运行的cpu核数设置成2核

package main

import (
    "fmt"
    "runtime"
)

var (
    signal = false
)

func oldboy() {
    signal = true
}

func init() {
    runtime.GOMAXPROCS(2)
}

func main() {
    go oldboy()
    for {
        if signal {
            break
        }
    }
    fmt.Println("end")
}

运行结果:

end

三、Gosched 让当前的 goroutine 让出 CPU

这个函数的作用是让当前 goroutine 让出 CPU,当一个 goroutine 发生阻塞,Go 会自动地把与该 goroutine 处于同一系统线程的其他 goroutine 转移到另一个系统线程上去,以使这些 goroutine 不阻塞。当前的 goroutine 不会挂起,当前的 goroutine 程未来会恢复执行。

runtime.Gosched()用于让出CPU时间片。这就像跑接力赛,A跑了一会碰到代码runtime.Gosched()就把接力棒交给B了,A歇着了,B继续跑。

package main

import (
	"runtime"
	"sync"
)

func main() {
	wg := new(sync.WaitGroup)
	wg.Add(1)
	go func() {
		for i := 0; i < 6; i++ {
			println(i)
			runtime.Gosched()
		}
		defer wg.Done()
	}()
	for i := 0; i < 6; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			println("Hello.Golang!")
		}()
	}
	wg.Wait()
}

D:\goprogram\go\src\day10
λ go run test.go
0
1
2
3
4
5
Hello.Golang!
Hello.Golang!
Hello.Golang!
Hello.Golang!
Hello.Golang!
Hello.Golang!

四、Goexit 终止当前 goroutine 执行

调用 runtime.Goexit 将立即终止当前 goroutine 执行,调度器确保所有已注册 defer 延迟调用被执行。

package main

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

func main() {
    wg := new(sync.WaitGroup)
    wg.Add(1)

    go func() {
        defer wg.Done()
        defer fmt.Println("A.defer")
        func() {
            defer fmt.Println("B.defer")
            runtime.Goexit() // 终止当前 goroutine
            fmt.Println("B") // 不会执行
        }()

        fmt.Println("A") // 不会执行
    }()

    wg.Wait()
}
B.defer
A.defer

原文地址:https://www.cnblogs.com/heych/p/12579607.html

时间: 2024-11-09 03:39:01

Go语言之Go语言并发的相关文章

Go语言之GO 语言引用类型

GO 语言引用类型 Go 语言切片 Go 语言切片(Slice) Go 语言切片是对数组的抽象. Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go中提供了一种灵活,功能强悍的内置类型切片("动态数组"),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大. 定义切片 申明一个未指定大小的数组来定义切片: var identifier []type 切片不需要说明长度. 或使用make()函数来创建切片: var slice1 []type = m

Swift语言指南(一)--语言基础之常量和变量

Swift 是开发 iOS 及 OS X 应用的一门新编程语言,然而,它的开发体验与 C 或 Objective-C 有很多相似之处. Swift 提供了 C 与 Objective-C 中的所有基础类型,包括表示整数的 Int,表示浮点数的 Double 与 Float,表示布尔值的 Bool,以及表示纯文本数据的 String. Swift 还为两个基本集合类型 Array 与 Dictionary 提供了强大的支持,详情可参考 (集合类型)Collection Types. 与 C 语言类

0基础学C语言:C语言视频教程免费分享!

C语言是一种通用的.过程式的编程语言,广泛用于系统与应用软件的开发.作为计算机编程的基础语言,长期以来它一直是编程爱好者追捧而又比较难学的语言.C语言是一种计算机程序设计语言,它既具有高级语言的特点,又具有汇编语言的特点. 很多初学者在学习C语言的时候,如果有适合自己的视频教程,学习起来就会事半功倍.今天在这里给大家分享一个0基础学习C语言的视频教程,需要的朋友可以看看,作为参考! 课程部分截图: 百度云盘下载:http://pan.baidu.com/s/1jIbtWEi 密码:npd9

编译性语言、解释性语言和脚本语言

1.计算机不能直接理解高级语言,只能理解机器语言,所以必须要把高级语言翻译成机器语言,计算机才能执行高级语言编写的程序.(计算机只能执行机器语言:我们要执行高级语言编的代码,就只能用编译器把它变成机器语言) 2.翻译有两种方式:a.编译b.解释.两种方式主要是翻译的时间不同 3.编译语言:编译型语言写的程序执行之前,需要一个专门的编译过程,把程序编译成机器语言文件:比如,exe文件,以后运行的话就不用重新编译了,直接使用编译的结果就行了:因为翻译只做了一次,运行时不需要翻译,所以编译型语言的程序

为什么和其他语言相比C语言是快速的语言

初入门的我们经常听见别人说"真正的程序员用C语言编程,C是最快的语言因为它是最靠近及其底层的语言."那么和其他语言相比C语言到底有什么特别的呢? C语言没有什么特别,这就是它快速的秘诀. 新的语言支持更多的特性,比如,垃圾回收(garbage collection),动态类型(dynamic typing)等等.这些新加入的特性让出学者们更容易上手. 问题的关键就在于,这些新的功能增加了处理开销(processing overhead),也就降低了程序性能.而C语言中没有这些功能,它不

Swift语言指南(八)--语言基础之元组

元组 元组(Tuples)将多个值组合为一个复合值.元组内的值可以是任何类型,各个元素不需要为相同类型(各个元素之间类型独立,互不干扰--Joe.Huang). 下例中,(404, "Not Found") 是一个描述HTTP状态码的元组.HTTP状态码是当你向WEB服务器请求页面时服务器返回的一个特殊值,如果你(向WEB服务器)请求了一个不存在的网页,返回的状态码就是 404 Not Found : 1 let http404Error = (404, "Not Found

动态语言和静态语言、编译型语言和解释型语言、强类型语言和弱类型语言的分析

一.动态语言和静态语言1. 我们常说的动.静态语言,通常是指: 动态类型语言 Dynamically Typed Language 静态类型语言 Statically Typed Language 可能还有:动.静态编程语言 Dynamic\Statically Programming Language 2.    动态类型语言:在运行期间检查数据的类型的语言例如:Ruby\Python这类语言编程,不会给变量指定类型,而是在附值时得到数据类型.Python是动态语言,变量只是对象的引用,变量a

初识GO语言——安装Go语言

本文包括:1)安装Go语言.2)运行第一个Go语言.3)增加vim中对Go语言的高亮支持. 1.安装Go语言 本文采用源码安装Go语言,Go语言的源代码在百度网盘 http://pan.baidu.com/s/1mguZqhM 1.1.修改环境变量 编辑文件~/.bashrc vim ~/.bashre 在文件最后添加如下代码 # about go language export GOROOT=$HOME/go export GOARCH=386 export GOOS=linux export

Swift语言指南(二)--语言基础之注释和分号

注释 通过注释向自己的代码中注入不可执行的文本,作为你自己的笔记或提示.Swift编译器运行时会忽略注释. Swift的注释与C语言极其相似,单行注释以两个反斜线开头: //这是一行注释 多行注释以/*开始,以*/结束: ? 1 2 3 <span style="color: rgb(0, 128, 0);">/* 这也是一条注释, 但跨越多行 */ </span> 与 C 语言的多行注释有所不同的是,Swift 的多行注释可以嵌套在其他多行注释内部.写法是在一