Golang并发

golang并发

goroutine

并发 多线程单cpu上运行,同一时间内只有一个进程执行

并行 多线程多cpu运行 同一时刻多个进程一起执行

如果线程数大于cpu数 既有并行又有并发

package main

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

var wg sync.WaitGroup

func test() {
	for i := 0; i < 10; i++ {
		fmt.Println("t", i)
		time.Sleep(time.Second * 2)
	}
	wg.Done() //协程计数器-1
}
func main() {

	cpuNum := runtime.NumCPU() //获取CPU个数
	runtime.GOMAXPROCS(4)      //设置可以使用的cpu个数
	wg.Add(1)                  //协程计数器+1
	go test()
	for i := 0; i < 10; i++ {
		fmt.Println(i)
		time.Sleep(time.Second)
	}
	wg.Wait() //等待协程执行完毕
}

管道

var c1 chan int     //声明一个传递int类型的管道
var c2 chan string  //声明一个传递string类型的管道

创建channel

make (chan 元素类型,容量)

ch := make(chan,int,3)
ch <- 10  //把10发送到ch
x :=  <-ch  //从管道取值
<-ch  //取值但不赋值
close(ch)  //关闭管道

管道循环

var ch1 = make(chan int,10)
for i :=1;i<=10,i++ {
	ch1 <- i
	
}
close(ch1)
//管道没有key
for v := range ch1 {
	fmt.Println(v)
}

//for循环遍历管道无需关闭 ,for range需要关闭

管道结合goroutine

//打印1-12000之间的素数

package main

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

var wg sync.WaitGroup

// 向 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发送数据了
	//什么时候关闭primeChan?

	//给exitChan里面放入一条数据
	exitChan <- true
	wg.Done()
}

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

func main() {

	start := time.Now().Unix()

	intChan := make(chan int, 1000)
	primeChan := make(chan int, 50000)
	exitChan := make(chan bool, 8) //标识primeChan close

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

	//统计素数的协程
	for i := 0; i < 8; 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 < 8; i++ {
			<-exitChan
		}
		//关闭primeChan
		close(primeChan)
		wg.Done()
	}()

	wg.Wait()

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

}


单向管道

读写管道
c :=make(chan int,2)
单向管道只读
c := make(<-chan int,2)
单向管道只写
c := make(chan<- int,2)

select多路复用

select语句是一种用于处理并发操作的重要机制。它允许在多个通信操作中选择一个可用的操作进行执行,从而实现对并发任务的控制

select语句由case子句组成,每个case子句描述一个通信操作。其基本语法如下

package main

import (
	"fmt"
	"time"
)

func main() {
	// 在某些场景下我们需要同时从多个通道接收数据,这个时候就可以用到golang中给我们提供的select多路复用

	//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)
	}
	//使用select来获取channel里面的数据的时候不需要关闭channel
	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 //注意退出...
		}
	}

}

panic

在Go语言中,panic是一种用于处理程序错误和异常情况的机制。当程序遇到无法处理的错误或异常时,可以触发panic,并停止当前函数的执行,然后逐层向上返回,直到被recover捕获或程序终止

可以使用panic关键字来触发panic panic(value) value:可以是任意类型的值,表示触发panic时传递的信息。

当程序遇到无法恢复的错误时,可以使用panic中断程序的正常执行流程。这些错误可能包括非法参数、不可预料的状态或不可修复的运行时错误。通过触发panic,可以停止当前函数的执行并向上传播错误信息。

func divide(a, b int) int { 
    if b == 0 { 
        panic("除数不能为零") 
    } 
    return a / b 
}

在一些特殊情况下,当发生错误时,我们希望程序能够立即停止,并输出错误信息,而不是继续执行下去。这种情况下,可以使用panic来触发错误,然后在程序的顶层函数中使用recover来捕获并处理这些错误。

func main() { 
    defer func() { 
        if err := recover(); err != nil { 
            fmt.Println("发生错误:", err) 
           } 
     }() 
    // 一些可能触发panic的操作 
}

互斥锁和读写锁

互斥锁

Mutex是互斥锁的意思,也叫排他锁,同一时刻一段代码只能被一个线程运行,使用只需要关注方法Lock(加锁)和Unlock(解锁)即可。
在Lock()和Unlock()之间的代码段称为资源的临界区(critical section),是线程安全的,任何一个时间点都只能有一个goroutine执行这段区间的代码。

package main

//go build -race main.go  编译后运行查看

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

var wg sync.WaitGroup

var mutex sync.Mutex

var m = make(map[int]int, 0)

func test(num int) {
	mutex.Lock()
	var sum = 1
	for i := 1; i <= num; i++ {
		sum *= i
	}
	m[num] = sum
	// fmt.Println(m[num])
	fmt.Printf("key=%v value=%v\n", num, sum)
	time.Sleep(time.Millisecond)
	mutex.Unlock()
	wg.Done()
}

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

}

读写锁

Mutex在大量并发的情况下,会造成锁等待,对性能的影响比较大。
如果某个读操作的协程加了锁,其他的协程没必要处于等待状态,可以并发地访问共享变量,这样能让读操作并行,提高读性能。
RWLock就是用来干这个的,这种锁在某一时刻能由什么问题数量的reader持有,或者被一个writer持有

主要遵循以下规则 :

  1. 读写锁的读锁可以重入,在已经有读锁的情况下,可以任意加读锁。
  2. 在读锁没有全部解锁的情况下,写操作会阻塞直到所有读锁解锁。
  3. 写锁定的情况下,其他协程的读写都会被阻塞,直到写锁解锁。

Go语言的读写锁方法主要有下面这种

  1. Lock/Unlock:针对写操作。
    不管锁是被reader还是writer持有,这个Lock方法会一直阻塞,Unlock用来释放锁的方法
  2. RLock/RUnlock:针对读操作
    当锁被reader所有的时候,RLock会直接返回,当锁已经被writer所有,RLock会一直阻塞,直到能获取锁,否则就直接返回,RUnlock用来释放锁的方法
package main

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

var wg sync.WaitGroup
var mutex sync.RWMutex

//写的方法
func write() {
	mutex.Lock()
	fmt.Println("执行写操作")
	time.Sleep(time.Second * 2)
	mutex.Unlock()
	wg.Done()
}

//读的方法
func read() {
	mutex.RLock()
	fmt.Println("---执行读操作")
	time.Sleep(time.Second * 2)
	mutex.RUnlock()
	wg.Done()
}

func main() {

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

}

Golang并发
http://www.jcwit.com/article/500/
作者
Carlos
发布于
2024年3月19日
许可协议