go--并发

1,426 阅读7分钟

进程和线程

进程

进程就是一个正在执行的程序

线程

是进程的一个执行实例,是程序执行的最小单元,它是比进程更小的能独立运行的基 本单位

关系

一个进程可以创建多个线程,同一个进程中的多个线程可以并发执行,一个程序要运行的话 至少有一个进程。

并行和并发

并发

多个线程同时竞争一个位置,竞争到的才可以执行,每一个时间段只有一个线程在执 行。

并行

多个线程可以同时执行,每一个时间段,可以有多个线程同时执行

通俗的讲

多线程程序在单核 CPU 上面运行就是并发,多线程程序在多核 CUP 上运行就是并 行,如果线程数大于 CPU 核数,则多线程程序在多个 CPU 上面运行既有并行又有并发

Golang 中的协程(goroutine)以及主线程

golang 中的主线程:

(可以理解为线程/也可以理解为进程),在一个 Golang 程序的主线程 上可以起多个协程。Golang 中多协程可以实现并行或者并发。

协程

可以理解为用户级线程,这是对内核透明的,也就是系统并不知道有协程的存在,是 完全由用户自己的程序进行调度的。

多协程

Golang 中的多协程有点类似其他语言中的多线程

多协程和多线程

Golang 中每个 goroutine (协程) 默认占用内存远比 Java 、C 的线程少。 OS 线程(操作系统线程)一般都有固定的栈内存(通常为 2MB 左右),一个 goroutine (协程) 占用内存非常小,只有 2KB 左右,多协程 goroutine 切换调度开销方面远比线程要少。 这也是为什么越来越多的大公司使用 Golang 的原因之一。

代码demo

打印2个方法

func test() {
	for i := 0; i < 5; i++ {
		fmt.Println("test() 你好golang-", i)
		time.Sleep(time.Millisecond * 100)
	}
}

func test1() {
	for i := 0; i < 5; i++ {
		fmt.Println("test1() 你好golang-", i)
		time.Sleep(time.Millisecond * 100)
	}
}

func main() {
	test()
	test1()
}

先运行完test,再去运行test1

test() 你好golang- 0
test() 你好golang- 1
test() 你好golang- 2
test() 你好golang- 3
test() 你好golang- 4
test1() 你好golang- 0
test1() 你好golang- 1
test1() 你好golang- 2
test1() 你好golang- 3
test1() 你好golang- 4

开启协程

func main() {
	go test1() //表示开启一个协程
	test() //主线程
}

打印顺序,是随机的

test1() 你好golang- 0
test() 你好golang- 0
test1() 你好golang- 1
test() 你好golang- 1
test() 你好golang- 2
test1() 你好golang- 2
test1() 你好golang- 3
test() 你好golang- 3
test() 你好golang- 4
test1() 你好golang- 4

主线程执行完毕,即使协程没有执行完毕,程序也会退出

协程可以在主线程没有执行完毕前提前退出,协程是否执行完毕不会影响主线程的执行

func test() {
	for i := 0; i < 5; i++ {
		fmt.Println("test() 你好golang-", i)
		time.Sleep(time.Millisecond * 10)
	}
}

func test1() {
	for i := 0; i < 5; i++ {
		fmt.Println("test1() 你好golang-", i)
		time.Sleep(time.Millisecond * 100)
	}
}

func main() {
	go test1() //表示开启一个协程
	test() //主线程
}

主线程执行完了,协程还没有跑完,会停止协程

test() 你好golang- 0
test1() 你好golang- 0
test() 你好golang- 1
test() 你好golang- 2
test() 你好golang- 3
test() 你好golang- 4

sync.WaitGroup

sync.WaitGroup 可以实现主线程等待协程执行完毕。

package main

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

var wg sync.WaitGroup

func test() {
	for i := 0; i < 5; i++ {
		fmt.Println("test() 你好golang-", i)
		time.Sleep(time.Millisecond * 10)
	}
	wg.Done()
}

func test1() {
	for i := 0; i < 5; i++ {
		fmt.Println("test1() 你好golang-", i)
		time.Sleep(time.Millisecond * 100)
	}
}

func test2() {
	for i := 0; i < 10; i++ {
		fmt.Println("test2() 你好golang-", i)
		time.Sleep(time.Millisecond * 100)
	}
	wg.Done() //协程计数器-1
}

func main() {
	wg.Add(1)  //协程计数器+1
	go test1() //表示开启一个协程
	wg.Add(1)  //协程计数器+1
	go test2() //表示开启一个协程
	test()     //主线程

	wg.Wait() //等待协程执行完毕...
	fmt.Println("主线程退出...")
}

统计 1-120000 的数字中那些是素数

基础版

大概5毫秒

package main

import (
	"fmt"
	"time"
)

//需求:要统计1-120000的数字中那些是素数?for循环实现
func main() {
	start := time.Now().Unix()
	for num := 2; num < 120000; num++ {
		var flag = true
		for i := 2; i < num; i++ {
			if num%i == 0 {
				flag = false
				break
			}
		}
		if flag {
			// fmt.Println(num, "是素数")
		}
	}
	end := time.Now().Unix()
	fmt.Println(end - start) //5毫秒 
}

协程版

goroutine 开启多个协程,大概2毫秒

package main

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

var wg sync.WaitGroup

func test(n int) {
	for num := (n-1)*30000 + 1; num < n*30000; num++ {
		if num > 1 {
			var flag = true
			for i := 2; i < num; i++ {
				if num%i == 0 {
					flag = false
					break
				}
			}
			if flag {
				// fmt.Println(num, "是素数")
			}
		}
	}
	wg.Done()
}

func main() {
	start := time.Now().Unix()
	for i := 1; i <= 4; i++ {
		wg.Add(1)
		go test(i)
	}
	wg.Wait()
	fmt.Println("执行完毕")
	end := time.Now().Unix()
	fmt.Println(end - start) //2毫秒

}

Channel 管道

基本概念

管道是 Golang 在语言级别上提供的 goroutine 间的通讯方式,我们可以使用 channel 在 多个 goroutine 之间传递消息。

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

Golang 的并发模型是 CSP(Communicating Sequential Processes),提倡通过通信共享内 存而不是通过共享内存而实现通信。

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

基本操作

创建channel

	ch := make(chan int, 3) //长度为3的int类型chan
	fmt.Println("ch", ch) //0xc00001c100

给管道里面存储数据

	ch <- 10
	ch <- 20
	ch <- 30

获取管道里面的内容

	a := <-ch
	fmt.Println("a", a) //a 10
    <-ch
	c := <-ch

	fmt.Println("c", c)  //30

打印管道的长度和容量

fmt.Printf("值:%v, 容量:%v,长度:%v\n", ch, cap(ch), len(ch)) // 值:0xc00001c100, 容量:3,长度:0

管道的类型

	ch1 := make(chan int, 4)
	ch1 <- 31
	ch1 <- 41
	ch1 <- 51

	ch2 := ch1
	ch2 <- 61
	<-ch1
	<-ch1
	<-ch1
	d := <-ch1
	fmt.Println((d))

管道阻塞

	//长度只有1,
	// ch6 := make(chan int, 1) 
	// ch6 <- 34
	// ch6 <- 44
	//fatal error: all goroutines are asleep - deadlock!
	管道为空,继续取值
	ch7 := make(chan string, 2)
	ch7 <- "a"
	ch7 <- "b"
	m1 := <-ch7
	m2 := <-ch7
	m3 := <-ch7
	fmt.Println(m1, m2, m3)

边存边取

	ch8 := make(chan int, 1)
	ch8 <- 34
	<-ch8
	ch8 <- 44
	<-ch8
	ch8 <- 54
	m4 := <-ch8
	fmt.Println(m4)

循环

for range

使用for range遍历通道,当通道被关闭的时候就会退出for range,如果没有关闭管道就会报个错误fatal error: all goroutines are asleep - deadlock!

func main() {
	var ch1 = make(chan int, 10)
	for i := 1; i <= 10; i++ {
		ch1 <- i
	}
	close(ch1) //关闭管道
	// //for range循环遍历管道的值  ,注意:管道没有key
	for v := range ch1 {
		fmt.Println(v)
	}
}

for

通过for循环遍历管道的时候管道可以不关闭

func main() {
	var ch2 = make(chan int, 10)
	for i := 1; i <= 10; i++ {
		ch2 <- i
	}

	for j := 0; j < 10; j++ {
		fmt.Println(<-ch2)
	}
}

边存边取

package main

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

var wg sync.WaitGroup

func fnIn(ch chan int) {
	for i := 1; i <= 10; i++ {
		ch <- i
		fmt.Printf("【写入】数据%v成功\n", i)
		time.Sleep(time.Millisecond * 500)
	}
	close(ch)
	wg.Done()
}

func fnOut(ch chan int) {
	for v := range ch {
		fmt.Printf("【读取】数据%v成功\n", v)
		time.Sleep(time.Millisecond * 10)
	}
	wg.Done()
}

func main() {

	var ch = make(chan int, 10)

	wg.Add(1)
	go fnIn(ch)
	wg.Add(1)
	go fnOut(ch)

	wg.Wait()
	fmt.Println("退出...")
}

打印输出

【写入】数据1成功
【读取】数据1成功
【写入】数据2成功
【读取】数据2成功
【读取】数据3成功
【写入】数据3成功
【写入】数据4成功
【读取】数据4成功
【写入】数据5成功
【读取】数据5成功
【写入】数据6成功
【读取】数据6成功
【写入】数据7成功
【读取】数据7成功
【写入】数据8成功
【读取】数据8成功
【写入】数据9成功
【读取】数据9成功
【写入】数据10成功
【读取】数据10成功
退出...

优化版,打印素数

package main

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

var wg sync.WaitGroup

func main() {
	start := time.Now().Unix()
	intChan := make(chan int, 1000)
	primeChan := make(chan int, 16000)
	exitChan := make(chan bool, 16)

	// 存放数字的协程
	wg.Add(1)
	go putNum(intChan)

	//统计素数的协程,一次1000分16次
	for i := 0; i < 16; i++ {
		wg.Add(1)
		go primeNum(intChan, primeChan, exitChan)
	}

	//打印素数的协程
	wg.Add(1)
	go printPrime(primeChan)

	// 判断exitChan是否存满值
	wg.Add(1)
	go func() {
		for i := 0; i < 16; i++ {
			<-exitChan
		}
		//关闭primeChan
		close(primeChan)
		wg.Done()
	}()

	wg.Wait()
	end := time.Now().Unix()
	fmt.Println("执行完毕....", end-start, "毫秒")
}

// 向 intChan放入 1-120000个数
func putNum(intChan chan int) {
	for i := 2; i < 120000; i++ {
		intChan <- i
	}
	close(intChan)
	wg.Done()
}

//从 intChan取出数据,并判断是否为素数,如果是,就把得到的素数放在primeChan
func primeNum(intChan chan int, primeChan chan int, exitChan chan bool) {
	for num := range intChan {
		var flag = true
		for i := 2; i < num; i++ {
			if num%i == 0 {
				flag = false
				break
			}
		}
		if flag {
			primeChan <- num //num是素数
		}
	}
	//要关闭 primeChan
	// close(primeChan) //如果一个channel关闭了就没法给这个channel发送数据了
	//给exitChan里面放入一条数据
	exitChan <- true
	wg.Done()
}

//printPrime打印素数的方法
func printPrime(primeChan chan int) {
	for v := range primeChan {
		fmt.Println(v)
	}
	wg.Done()
}

单向管道

限制管道在函数中只能发送或只能接收

package main

import (
	"fmt"
	"sync"
)

var wg sync.WaitGroup

func main() {
	// 在默认情况下下,管道是双向
	ch1 := make(chan int, 2)
	ch1 <- 10
	ch1 <- 12
	m1 := <-ch1
	m2 := <-ch1
	fmt.Println(m1, m2) //10 12

	// 管道声明为只写
	ch2 := make(chan<- int, 2)
	ch2 <- 10
	ch2 <- 12
	// <-ch2 // invalid operation: <-ch2 (receive from send-only type chan<- int)

	// 管道声明为只读
	ch3 := make(<-chan int, 2)
	// ch3 <- 11 //invalid operation: <-ch2 (receive from send-only type chan<- int)
}

select 多路复用

传统的方法在遍历管道时,如果不关闭会阻塞而导致 deadlock,在实际开发中,可能我们 不好确定什么关闭该管道

Go 内置了 select 关键字,可以同时响应多个管道的操作

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

func main() {
	//1.定义一个管道 10个数据int
	intChan := make(chan int, 10)
	for i := 0; i < 10; i++ {
		intChan <- i
	}
	//2.定义一个管道 5个数据string
	stringChan := make(chan string, 5)
	for i := 0; i < 5; i++ {
		stringChan <- "hello" + fmt.Sprintf("%d", i)
	}

	for {
		select {
		case v := <-intChan:
			fmt.Printf("从 intChan 读取的数据%d\n", v)
			time.Sleep(time.Millisecond * 50)
		case v := <-stringChan:
			fmt.Printf("从 stringChan 读取的数据%v\n", v)
			time.Sleep(time.Millisecond * 50)
		default:
			fmt.Printf("数据获取完毕")
			return
		}
	}
}

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

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

Golang 并发安全和锁

互斥锁

互斥锁是一种常用的控制共享资源访问的方法,它能够保证同时只有一个 goroutine 可以访 问共享资源。

Go 语言中使用 sync 包的 Mutex 类型来实现互斥锁

var count = 0
var wg sync.WaitGroup

// var mutex sync.Mutex

func test() {
	// mutex.Lock()
	count++
	fmt.Println("the count is :", count)
	time.Sleep(time.Microsecond)
	// mutex.Unlock()
	wg.Done()
}

func main() {
	for r := 0; r < 10; r++ {
		wg.Add(1)
		go test()
	}
	wg.Wait()
}