2

golang中协程&管道&锁 - 木子七

 6 months ago
source link: https://www.cnblogs.com/Mickey-7/p/18029807
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

golang中协程&管道&锁

进程和线程#

进程(Process)就是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基 本单位,进程是一个动态概念,是程序在执行过程中分配和管理资源的基本单位,每一个进 程都有一个自己的地址空间。一个进程至少有 5 种基本状态,它们是:初始态,执行态, 等待状态,就绪状态,终止状态,通俗的讲进程就是一个正在执行的程序。

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

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

并发和并行#

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

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

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

image-20240222190919472

协程goroutine#

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

协程:可以理解为用户级线程,这是对内核透明的,也就是系统并不知道有协程的存在,是 完全由用户自己的程序进行调度的。Golang 的一大特色就是从语言层面原生支持协程,在 函数或者方法前面加 go 关键字就可创建一个协程。可以说 Golang 中的协程就是 goroutine

多协程和多线程:Golang 中每个 goroutine (协程) 默认占用内存远比 Java 、C 的线程少

OS 线程(操作系统线程)一般都有固定的栈内存(通常为 2MB 左右)

一个 goroutine (协程) 占用内存非常小,只有 2KB 左右,多协程 goroutine 切换调度开销方面远比线程要少

使用协程#
func test() {
	for i := 0; i <= 10; i++ {
		fmt.Println(i)
	}
}

func main() {
	
	// go关键字声明这是一个协程,test方法代码和主进程代码同时指向
	// 如果主进程执行完了,整个程序就结束了,协程没有执行完也不执行了,如果协程执行完主进程没有执行完,主进程会继续执行
	go test()

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

}
sync.WaitGroup 等待协程#
import (
	"fmt"
	"sync"
)

// 需要导入sync包
// 声明WaitGroup
var wg sync.WaitGroup

func test() {
	for i := 0; i <= 10; i++ {
		fmt.Println(i, "123")
	}
	wg.Done() // 协程计数器-1
}

func main() {

	wg.Add(1) // 协程计数器+1,
	go test()
	for i := 0; i <= 10; i++ {
		fmt.Println(i)
	}

	wg.Wait() // 等待协程执行完毕,启动一个协程登记+1,结束就-1,等于0 的时候就等待结束

}

并行运行时使用的CPU核数#

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

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

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

import (
	"fmt"
	"runtime"
)

func main() {
	// 获取当下计数器上面的CPU个数
	cpuNUm := runtime.NumCPU()
	fmt.Println(cpuNUm)

	// ky自己设置使用多少CPU
	runtime.GOMAXPROCS(cpuNUm - 1)

}

channel管道#

管道是 Golang 在语言级别上提供的 goroutine 间的通讯方式,我们可以使用 channel 在

多个 goroutine 之间传递消息。如果说 goroutine 是 Go 程序并发的执行体,channel 就是它们

之间的连接。channel 是可以让一个 goroutine 发送特定值到另一个 goroutine 的通信机制。

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

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

管道被设计为一种并发安全的数据结构,可以在多个协程之间安全地进行通信

channel类型#

channel是一种类型,一种引用类型

/*
声明管道类型的语法:
var 管道名 chan 管道传递的数据类型

*/

var int1 chan int  // 传递int类型的管道
var str chan string
var b chan bool
var s chan []int
channel操作#

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

发送和接收都使用<-符号

创建channel#

向管道中存储数据,管道就必须有容量,所以需要使用make初始化分配容量

	// 创建一个管道,传递int类型,容量为3
	var ch = make(chan int, 3)
向管道存储数据#
	// 管道名 <- 数据
	ch <- 10
从管道获取数据#
	// 接收变量 := <-管道名
	a := <-ch
	fmt.Println(a) // 10

管道先入先出#
	var ch = make(chan int, 3)
	// 向管道存储多条数据
	ch <- 10
	ch <- 11
	ch <- 12

	// 取出第一条数据
	a := <-ch
	fmt.Println(a) //10
	// 取出第二条数据,不赋值给任何
	<-ch
	// 第三天数据
	b := <-ch
	fmt.Println(b) //12

管道的值、长度和容量#
	var ch = make(chan int, 3)
	// 管道的值是一个地址,长度是0是因为现在还没有写入值
	// 值-0xc0000b2000-长度-0,容量-310
	fmt.Printf("值-%v-长度-%v,容量-%v", ch, len(ch), cap(ch))
管道阻塞#
  • 无缓冲管道:如果创建管道的时候没有指定容量,那么我们可以叫这个管道为无缓冲的管道,无缓冲的管道又称为阻塞的管道

  • 有缓冲管道:我们在使用 make 函数初始化 管道的时候为其指定管道的容量,有容量的管道就是有缓冲管道

  • 只要管道的容量大于零,那么该管道就是有缓冲的管道

    • 容量为1有一条数据的管道,再存储,阻塞管道
    • 容量为1有一条数据的管道,取一条数据后,再次取值,阻塞管道

image-20240223110224787
管道遍历和关闭管道#

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

当管道被关闭时,再往该管道发送值会引发 panic,从该管道取值的操作会先取完管道中的值,再然后取到的值一直都是对应类型的零值

func main() {

	var ch = make(chan int, 10)

	for i := 0; i < 10; i++ {
		ch <- i

	}

	// 在 Go 语言中,只有在接收端需要明确知道通道已经关闭的情况下才需要关闭通道。关闭通道是为了通知接收端不再有更多的数据发送过来,从而避免接收端陷入阻塞状态
	// 当管道被关闭时,再往该管道发送值会引发 panic
	close(ch)

	// 管道没有key,遍历管道的时候只用一个变量
	// 通过for range 遍历管道,如果没有关闭管道就会报错

	for v := range ch {
		fmt.Println(v) // 先进先出取值

	}

	/*
				 如果通过单纯的for循环变量管道,管道可以不关闭

			     在for range遍历通道时,当通道被关闭时,for range 循环会自动判断通道是否关闭,并在所有元素被接收后退出循环。
			     这是 for range 的一种特性,用于在遍历通道时方便地处理通道的关闭情况。
			     当通道未关闭时,for range 循环会一直等待接收通道中的元素。如果没有其他 goroutine 在向通道发送数据,或者没有明确的					 关闭通道的操作,那么 for range 循环就会一直阻塞在接收操作上,从而导致死锁

		 		   普通的 for 循环中,手动控制循环的条件,包括接收通道的操作。在这种情况下,可以根据自己的逻辑判断何时退出循环


         // 可以不关闭管道
				 for i:0;i<10;i++{ 
				    fmt.Println(<-ch)
				}
	*/

}

协程Goroutine 结合 Channel 管道 同步操作#
package main

import (
	"fmt"
	"sync"
)

var wg sync.WaitGroup

// 定义一个向管道写入数据的方法
func WriteData(ch chan int) {
	for i := 0; i < 10; i++ {
		fmt.Println("WriteData")
		ch <- i
	}
	close(ch)
	wg.Done()

}

// 定义一个从管道读取数据的方法
func GetData(ch chan int) {
	for i := range ch {
		fmt.Println(i)
	}

	wg.Done()

}
func main() {
	/*
		两个协程方法同时执行,一个写入数据,一个读取数据
	
	当一个协程向管道写入数据而另一个协程从管道读取数据时,管道提供了内置的同步机制,即发送操作和接收操作会自动进行同步等待。
	这意味着,写入操作会在管道有足够的空间时立即完成,否则会阻塞直到有空间可用。
	类似地,读取操作会在管道有数据可读时立即完成,否则会阻塞直到有数据可读
	
	range 循环会在管道被关闭且其中的所有元素都被接收后自动退出。
	如果 WriteData 协程完成并关闭了管道,GetData 中的 range 循环会自动退出,不会导致死锁



	*/

	var ch = make(chan int, 10)

	wg.Add(1)
	go WriteData(ch)
	wg.Add(1)
	go GetData(ch)
	wg.Wait()

}

/*
多个协程打印素数
1.协程存放数字
2.多个协程计算是否素数
3.协程打印素数
*/

var wg = sync.WaitGroup{}

// 向intChan放入数字
func PutNum(intChan chan int) {

	for i := 2; i < 100; 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++ {
			// 如果=0 说明不是素数
			if num%i == 0 {
				flag = false
				break

			}
		}
		// num是素数
		if flag {
			primeChan <- num
		}

	}
	wg.Done()
	// 每执行完一次 向exitChan存储一个true
	exitChan <- true

}

// 打印素数
func PrintPrime(primeChan chan int) {

	for v := range primeChan {
		fmt.Println(v)
	}
	wg.Done()

}

func main() {

	var intChan = make(chan int, 1000)
	var primeChan = make(chan int, 1000)
	var exitChan = make(chan bool, 16)

	// 存入数字
	wg.Add(1)
	go PutNum(intChan)

	// 统计素数 启动16个协程
	for i := 0; i < 16; i++ {
		wg.Add(1)
		// 启动16个协程,操作同一个channel,所以不能在方法里直接关闭管道,需要16个协程都执行完再关闭管道
		go PrimeNum(intChan, primeChan, exitChan)
	}

	// 打印素数
	wg.Add(1)
	go PrintPrime(primeChan)

	// 匿名自执行函数-关闭primeChan
	// 管道循环
	wg.Add(1)
	go func() {
		for i := 0; i < 16; i++ {
			<-exitChan
		}
		// 关闭primeChan
		close(primeChan)
		wg.Done()
	}()

	wg.Wait()

}

单向管道#

有的时候我们会将管道作为参数在多个任务函数间传递,很多时候我们在不同的任务函数中 使用管道都会对其进行限制,比如限制管道在函数中只能发送或只能接收,默认情况下,管道是双向管道,可读可写

	// 双向管道 可读可写
	var ch = make(chan int, 10)
	ch <- 1
	<-ch

	// 只写管道,只可以写入不能读取
	// 声明的时候在chan 和类型之间加 <-
	var ch1 = make(chan<- int, 10)
	ch1 <- 1

	// 只读管道,只能读取不能写入
	// 在chan前面 + <-
	var ch2 = make(<-chan int, 10)
// 写入数据方法,类型为只写管道
func WriteData(ch chan<- int) {
	ch <- 1
}

// 读取数据方法,类型为只读管道
func GetData(ch <-chan int) {
	fmt.Println(<-ch)
}

func main() {

	// 定义一个双向管道
	var ch = make(chan int, 10)

	WriteData(ch)
	GetData(ch)

}

select多路复用#

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

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

  • 可处理一个或多个 channel 的发送/接收操作。

  • 如果多个 case 同时满足,select 会随机选择一个。

  • 对于没有 case 的 select{}会一直等待,可用于阻塞 main 函数

func main() {

	// 管道一 10个数字
	intChan := make(chan int, 10)
	for i := 0; i < 10; i++ {
		intChan <- i
	}

	// 管道二 10个字符串
	stringChan := make(chan string, 10)
	for i := 0; i < 10; i++ {
		stringChan <- strconv.Itoa(i)
	}

	/*

			死循环过程中,每次循环会随机从一个管道中读取数据,可能这次intChan,下次stringChan
		    直到所有数据读取完毕,会执行default,每次循环后是一个并发的操作,循环一次选择一个channel执行

	*/

	// 某些场景下,我们需要同时从多个管道接受数据,可以使用死循环和select
	// 使用select获取数据的时候,不需要关闭channel,如果关闭chanel,会一直执行读取,读取对应类型的零值
	for {
		select {
		case v := <-intChan:
			fmt.Println(v)
		case v := <-stringChan:
			fmt.Println(v)

		default:
			fmt.Println("数据全部读取完毕")
			return // 退出循环

		}
	}

}


并发安全和锁#

互斥锁#

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

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

  • Lock:锁定共享资源
  • Unlock:解锁共享资源
import (
	"fmt"
	"sync"
)

var wg sync.WaitGroup

// 声明一个互斥锁
var mutex sync.Mutex
var count int = 0

func test() {
	// 多个协程同时访问时,先获取锁,然后执行代码,最后解锁,同时只有一个协程能获取到锁,获取不到锁的协程就等待
	
	// 加锁
	mutex.Lock()
	count++
	fmt.Println(count)
	// 解锁
	mutex.Unlock()

}

func main() {

	for i := 0; i < 20; i++ {
		wg.Add(1)
		go test()
	}

	wg.Wait()
}

读写互斥锁#

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

读写锁在 Go 语言中使用 sync 包中的 RWMutex 类型。

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

var wg sync.WaitGroup

// 声明读写锁
var mutex sync.RWMutex

// 写的方法 互斥的
func WriteData() { 
	mutex.Lock() // +写的互斥锁
	fmt.Println("执行写操作")
	mutex.Unlock() // 解写的互斥锁
	wg.Done()

}

// 读的方法 并行的
func ReadData() {
	mutex.RLock() // +读的互斥锁
	fmt.Println("执行读操作")
	mutex.RUnlock() // 解读的互斥锁
	wg.Done()

}
func main() {

	// 开启10个协程执行写操作
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go WriteData()

	}

	// 开启10个协程执行写操作
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go ReadData()
	}
	wg.Wait()
}

image

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK