Go 中channel/goroutine实现并发和并行

1.为什么要使用goroutine

可以让程序并发和并行的运行,增加程序运行速度

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

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

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

2.Golang中的携程(goroutine)以及主线程

golang 中的主线程:在一个Golang程序的主线程上可以启用多个协程。golang中多协程可以实现并行和并发。

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

主线程如果执行完毕后,想要等待携程完成,在退出。

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

var wg sync.WaitGroup

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

func main(){
    wg.Add(1) // 协程计数器 +1
    go test()  //表示开启一个协程
    for( i:= 0; i< 10; i++ {
        fmt.Println("main ",i)
    }

    wg.Wait() //等待协程计数器为0 退出
}

3.golang 并行运行的时候占用的CPU数量

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

4.单个协程运行----素数判断

package main

import (
	"fmt"
	"time"
)
func main(){
	start := time.Now().Unix()
	for num:=2; num < 12000; 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,"ms")
	
}

5. 多个协程进行统计素数

package main

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

var wg sync.WaitGroup

func test(n int) {
	for num := (n - 1)*30000 + 1; num < n * 30000; num ++ {
		var flag = true
		for i := 2; i < num; i++ {
			if num%i == 0{
				flag = false
				break
			}
		}
		if flag{

		}
	} 
	wg.Done()
}

func main() {
	start := time.Now().Unix()
	for i := 1; i < 4; i++ {
		wg.Add(1)
		go test(i)
	}
	wg.Wait()
	end := time.Now().Unix()
	fmt.Println("全部执行完毕,共用时", end - start,"ms")
}

6.Channel 管道

         管道是Golang在语言级别上提供的goroutine间的通讯方式,我们可以使用channel在多个goroutine之间传递消息。如果说goroutine是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个goroutine发送特定值到另一个goroutine的通讯机制。

        Golang的并发模型是CSP,提倡通过通讯共享内存而不是通过共享内存而实现通信。

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

1、channel 类型

是一种引用类型

       var 变量 chan 元素类型

例如:

        var ch1 chan int   //声明一个传递整型的管道

        var ch2 chan bool //声明一个传递布尔类型的管道

        var ch3 chan []int  //声明一个传递int 切片的管道

2、创建channel

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

make( chan 元素类型, 容量)

3、channel操作

管道由发送send, 接收 receive, 关闭close 三种操作。

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

发送(将数据放在管道内)

将一个值发送到管道中

ch <- 10 

接收(从管道内取值)

从一个管道中接收值

b := <-ch  

 管道属于引用数据类型

ch1 := make(chan int, 4)
ch1 <- 34
ch1 <- 54
ch1 <- 64

ch2 := ch1
ch2 <- 25


<-ch1
<-ch1
<-ch1
d := <- ch1
fmt.Println(d) // d表示25  证明管道类型是引用类型

管道阻塞

ch6 := make(chan int, 1)


第一种玩法
ch6 <- 34
ch6 <- 64
//报错死锁,这就属于管道阻塞,传进来超容量数据

第二种玩法

ch6 <- 34
ch6 <- 64

m1:= <-ch6
m2:= <-ch6
m3:= <-ch6
//管道中如果没有值了还在取,就会出发阻塞了

如果要循环读取管道,就需要close(ch)关闭管道,否则会造成死锁

通过for 循环管道,管道可以不关闭,但是通过for .......range 获取读取管道,需要关闭

7、单向管道

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

//单向管道   只写
    ch2 := make(chan<- int ,2)
    ch2 <- 10
    ch2 <- 12
//单向管道   只读
    ch3 := make(<-chan int, 2)
    
//双向管道
    ch4 := make(chan int, 2)

用途: 可以在形参中定义,保证一个函数只能写,另一个函数只能读

8、select 多路复用

        在某些场景下,我们需要同时对多个通道接收数据。这个时候我们就可以用到golang中给我们提供的select多路服用。

        通常情况通道在接收数据时,如果没有数据可以接收将会发生阻塞。

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

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

func main(){

    intChan := make(chan int,10)
    fot i:=0; i < 10; i++ {
        intChan <- i
    }

    stringChan := make(chan string,5)
    for i:=0; i < 10; i++ {
        stringChan <- "hello" +fmt.Sprintf("%d",i)
    }

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

}

9、goroutine 遇到异常如何处理

        在golang语言中,如果有多个协程的话,其中一个出问题,并不影响其他的协程进行工作。

        如果遇到报错的话,可以通过匿名函数和defer进行处理

func test() {
    //处理协程异常情况
    defer func(){
        //捕获函数出现的panic
        if err := recover(); err != nil {
            fmt.Println("函数出现的错误",err)
        }
    }
    //协程操作
    。。。。。
    。。。。。
}

10、Golang并发安全和锁

        互斥锁:是传统并发编程中对共享资源进行访问控制的主要手段,它由标准库sync中的Mutex结构体类型表示。sync.Mutex类型只有两个公开的指针方法:lock和Unlock.lock锁定当前的共享资源,Unlock进行解锁

var mutex sync.Mutex
var count = 0

func test() {
    mutex.lock()
    count++
    fmt.Println("the count is:",count)
    mutex.Unlock()
}

你可能感兴趣的:(微服务和云原生,开发语言,golang)