Go-02语言并发Goroutine&Channel

一、协程概念

进程(Process),线程(Thread),协程(Coroutine)

进程是一个程序在一个数据集中的一次动态执行过程,可以简单理解为“正在执行的程序”,它是CPU资源分配和调度的独立单位。

线程是在进程之后发展出来的概念。它是一个基本的CPU执行单元,也是程序执行过程中的最小单元,由线程ID、程序计数器、寄存器集合和堆栈共同组成。一个进程可以包含多个线程。

协程是一种用户态的轻量级线程,又称微线程,英文名Coroutine,协程的调度完全由用户控制。

二、Goroutine

go中使用Goroutine来实现并发concurrently。

与线程相比,创建Goroutine的成本很小,它就是一段代码,一个函数入口。以及在堆上为其分配的一个堆栈(初始大小为4K,会随着程序的执行自动增长删除)。因此它非常廉价,Go应用程序可以并发运行数千个Goroutines。

1.主goroutine

封装main函数的goroutine称为主goroutine。

主goroutine首先要做的是:设定每一个goroutine所能申请的栈空间的最大尺寸。

此后,主goroutine会进行一系列的初始化工作,涉及的工作内容大致如下:

  1. 创建一个特殊的defer语句,用于在主goroutine退出时做必要的善后处理。因为主goroutine也可能非正常的结束

  2. 启动专用于在后台清扫内存垃圾的goroutine,并设置GC可用的标识

  3. 执行mian包中的init函数

  4. 执行main函数

    执行完main函数后,它还会检查主goroutine是否引发了运行时恐慌,并进行必要的处理。最后主goroutine会结束自己以及当前进程的运行。

2.使用goroutine

在函数或方法调用前面加上关键字go,将会同时运行一个新的Goroutine。

package main

import (  
    "fmt"
    "time"
)

func numbers() {  
    for i := 1; i <= 5; i++ {
        time.Sleep(250 * time.Millisecond)
        fmt.Printf("%d ", i)
    }
}
func alphabets() {  
    for i := 'a'; i <= 'e'; i++ {
        time.Sleep(400 * time.Millisecond)
        fmt.Printf("%c ", i)
    }
}
func main() {  
  	//启动多个Goroutines
    go numbers()
    go alphabets()
  	//main的goroutine被用来睡眠3秒
    time.Sleep(3000 * time.Millisecond)
    fmt.Println("main terminated")
}

Goroutine的规则

  1. 当新的Goroutine开始时,Goroutine调用立即返回。与函数不同,go不等待Goroutine执行结束。当Goroutine调用,并且Goroutine的任何返回值被忽略之后,go立即执行到下一行代码。
  2. main的Goroutine应该为其他的Goroutines执行。如果main的Goroutine终止了,程序将被终止,而其他Goroutine将不会运行。

3.runtime包

关于 runtime 包几个方法:

  • NumCPU:返回当前系统的 CPU 核数量

  • GOMAXPROCS:设置最大的可同时使用的 CPU 核数

    通过runtime.GOMAXPROCS函数,应用程序何以在运行期间设置运行时系统中得P最大数量。但这会引起“Stop the World”。所以,应在应用程序最早的调用。并且最好是在运行Go程序之前设置好操作程序的环境变量GOMAXPROCS,而不是在程序中调用runtime.GOMAXPROCS函数。

    无论我们传递给函数的整数值是什么值,运行时系统的P最大值总会在1~256之间。

go1.8后,默认让程序运行在多个核上,可以不用设置了
go1.8前,还是要设置一下,可以更高效的利益cpu

func init(){
	//1.获取逻辑cpu的数量
	fmt.Println("逻辑CPU的核数:",runtime.NumCPU())
	//2.设置go程序执行的最大的:[1,256]
	n := runtime.GOMAXPROCS(runtime.NumCPU())
	fmt.Println(n)
}
  • Gosched:让当前线程让出 cpu 以让其它线程运行,它不会挂起当前线程,因此当前线程未来会继续执行

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

    func main() {
    	go func() {
    		for i := 0; i < 5; i++ {
    			fmt.Println("goroutine。。。")
    		}
    
    	}()
    
    	for i := 0; i < 4; i++ {
    		//让出时间片,先让别的协议执行,它执行完,再回来执行此协程
    		runtime.Gosched()
    		fmt.Println("main。。")
    	}
    }
    
  • Goexit:退出当前 goroutine(但是defer语句会照常执行)

    
    func main() {
    	//创建新建的协程
    	go func() {
    		fmt.Println("goroutine开始。。。")
    
    		//调用了别的函数
    		fun()
    
    		fmt.Println("goroutine结束。。")
    	}() //别忘了()
    
    	//睡一会儿,不让主协程结束
    	time.Sleep(3*time.Second)
    }
    
    
    
    func fun() {
    	defer fmt.Println("defer。。。")
    
    	//return           //终止此函数
    	runtime.Goexit() //终止所在的协程
    
    	fmt.Println("fun函数。。。")
    }
    
  • NumGoroutine:返回正在执行和排队的任务总数

    runtime.NumGoroutine函数在被调用后,会返回系统中的处于特定状态的Goroutine的数量。这里的特指是指Grunnable\Gruning\Gsyscall\Gwaition。处于这些状态的Groutine即被看做是活跃的或者说正在被调度。

    注意:垃圾回收所在Groutine的状态也处于这个范围内的话,也会被纳入该计数器。

  • runtime.GC:会让运行时系统进行一次强制性的垃圾收集

    1. 强制的垃圾回收:不管怎样,都要进行的垃圾回收。
    2. 非强制的垃圾回收:只会在一定条件下进行的垃圾回收(即运行时,系统自上次垃圾回收之后新申请的堆内存的单元(也成为单元增量)达到指定的数值)。
  • GOROOT :获取goroot目录

  • GOOS : 查看目标操作系统

       //获取goroot目录:
       	fmt.Println("GOROOT-->",runtime.GOROOT())
       
       	//获取操作系统
       	fmt.Println("os/platform-->",runtime.GOOS) // GOOS--> darwin,mac系统
    

三、channel

Go语言中,要传递某个数据给另一个goroutine(协程),可以把这个数据封装成一个对象,然后把这个对象的指针传入某个channel中,另外一个goroutine从这个channel中读出这个指针,并处理其指向的内存对象。Go从语言层面保证同一个时间只有一个goroutine能够访问channel里面的数据,为开发者提供了一种优雅简单的工具,所以Go的做法就是使用channel来通信,通过通信来传递内存数据,使得内存数据在不同的goroutine中传递,而不是使用共享内存来通信。

1.概念

通道就是goroutine之间的通道。它可以让goroutine之间相互通信。

每个通道都有与其相关的类型。该类型是通道允许传输的数据类型。

2.数据类型

channel是引用类型

3.声明

//声明通道
var 通道名 chan 数据类型
//创建通道:如果通道为nil(就是不存在),就需要先创建通道
通道名 = make(chan 数据类型)
//简化方式
通道名 := make(chan 数据类型)

4.发送/接收

在通道上箭头的方向指定数据是发送还是接收。

data := <- a // 从通道读取数据
a <- data // 向通道写数据

一个通道发送和接收数据,默认是阻塞的

如果Goroutine在一个通道上发送数据,那么其他的Goroutine应该接收数据,否则程序将在运行时出现死锁。

同样,如果Goroutine正在等待从通道接收数据,那么其他Goroutine将会在该通道上写入数据,否则程序将会死锁。

5.关闭

发送者可以通过关闭信道,来通知接收方不会有更多的数据被发送到channel上。

close(ch)

接收者可以在接收来自通道的数据时使用额外的变量来检查通道是否已经关闭。

v, ok := <- ch  

如果ok的值是true,表示成功的从通道中读取了一个数据value。如果ok是false,这意味着我们正在从一个封闭的通道读取数据。

6.循环

for循环的for range形式可用于循环从通道上获取数据,直到通道关闭。

package main

import (
	"time"
	"fmt"
)

func main()  {
	ch1 :=make(chan int)//简化声明channel
	go sendData(ch1)
	// 通道的范围循环
	for v := range ch1{
		fmt.Println("读取数据:",v)
	}
	fmt.Println("main..over.....")
}
func sendData(ch1 chan int)  {
	for i:=0;i<10 ; i++ {
		time.Sleep(1*time.Second)
		ch1 <- i
	}
	close(ch1)//通知对方,通道关闭
}

7.缓冲通道

无缓冲通道(unbuffered channel)是指在接收前没有能力保存任何值的通道。

缓冲通道(buffered channel)是一种在被接收前能存储一个或者多个值的通道。

这种类型的通道并不强制要求 goroutine 之间必须同时完成发送和接收。只有在通道中没有要接收的值时,接收动作才会阻塞。只有在通道没有可用缓冲区容纳被发送的值时,发送动作才会阻塞。

package main

import (
	"fmt"
	"strconv"
	"time"
)

func main() {
	/*
	非缓存通道:make(chan T)
	缓存通道:make(chan T ,size)
		缓存通道,理解为是队列:

	非缓存,发送还是接受,都是阻塞的
	缓存通道,缓存区的数据满了,才会阻塞状态。。

	 */
	ch1 := make(chan int)           //非缓存的通道
	fmt.Println(len(ch1), cap(ch1)) //0 0
	//ch1 <- 100//阻塞的,需要其他的goroutine解除阻塞,否则deadlock

	ch2 := make(chan int, 5)        //缓存的通道,缓存区大小是5
	fmt.Println(len(ch2), cap(ch2)) //0 5
	ch2 <- 100                      //
	fmt.Println(len(ch2), cap(ch2)) //1 5

	//ch2 <- 200
	//ch2 <- 300
	//ch2 <- 400
	//ch2 <- 500
	//ch2 <- 600
	fmt.Println("--------------")
	ch3 := make(chan string, 4)
	go sendData3(ch3)
	for {
		time.Sleep(1*time.Second)
		v, ok := <-ch3
		if !ok {
			fmt.Println("读完了,,", ok)
			break
		}
		fmt.Println("\t读取的数据是:", v)
	}

	fmt.Println("main...over...")
}

func sendData3(ch3 chan string) {
	for i := 0; i < 10; i++ {
		ch3 <- "数据" + strconv.Itoa(i)
		fmt.Println("子goroutine,写出第", i, "个数据")
	}
	close(ch3)
}



8.定向通道

双向通道,既可以发送数据,也可以读取数据的通道。

data := <- a // 从通道读取数据
a <- data 	 // 向通道写数据

单向通道,只能发送或者接收数据的通道。

chan <- T //只写
<- chan T //只读

四、Timer

定时器,标准库中的Timer让用户可以定义自己的超时逻辑,是一次性的时间触发事件。

1.time.NewTimer()

package main

import (
	"time"
	"fmt"
)

func main() {

	/*
		1.func NewTimer(d Duration) *Timer
			创建一个计时器:d时间以后触发,go触发计时器的方法比较特别,就是在计时器的channel中发送值
	 */
	//新建一个计时器:timer
	timer := time.NewTimer(3 * time.Second)
	fmt.Printf("%T\n", timer) //*time.Timer
	fmt.Println(time.Now())   //2019-08-15 10:41:21.800768 +0800 CST m=+0.000461190

	//此处在等待channel中的信号,执行此段代码时会阻塞3秒
	ch2 := timer.C     //<-chan time.Time
	fmt.Println(<-ch2) //2019-08-15 10:41:24.803471 +0800 CST m=+3.003225965

}


2.time.After()

在等待持续时间之后,然后在返回的通道上发送当前时间。它相当于NewTimer(d).C。

package main

import (
	"time"
	"fmt"
)

func main() {

	/*
		func After(d Duration) <-chan Time
			返回一个通道:chan,存储的是d时间间隔后的当前时间。
	 */
	ch1 := time.After(3 * time.Second) //3s后
	fmt.Printf("%T\n", ch1) // <-chan time.Time
	fmt.Println(time.Now()) //2019-08-15 09:56:41.529883 +0800 CST m=+0.000465158
	time2 := <-ch1
	fmt.Println(time2) //2019-08-15 09:56:44.532047 +0800 CST m=+3.002662179


}


五、select

select 是 Go 中的一个控制结构。select会随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。

select {
    case communication clause  :
       statement(s);      
    case communication clause  :
       statement(s); 
    /* 可以定义任意数量的 case */
    default : /* 可选 */
       statement(s);
}
  • 每个case都必须是一个通信

  • 所有channel表达式都会被求值

  • 所有被发送的表达式都会被求值

  • 如果有多个case都可以运行,select会随机公平地选出一个执行。其他不会执行。

  • 否则:

    如果有default子句,则执行该语句。

    如果没有default字句,select将阻塞,直到某个通信可以运行;Go不会重新对channel或值进行求值。

package main

import (
	"fmt"
	"time"
)

func main() {
	/*
	分支语句:if,switch,select
	select 语句类似于 switch 语句,
		但是select会随机执行一个可运行的case。
		如果没有case可运行,它将阻塞,直到有case可运行。
	 */

	ch1 := make(chan int)
	ch2 := make(chan int)

	go func() {
		time.Sleep(2 * time.Second)
		ch2 <- 200
	}()
	go func() {
		time.Sleep(2 * time.Second)
		ch1 <- 100
	}()

	select {
	case num1 := <-ch1:
		fmt.Println("ch1中取数据。。", num1)
	case num2, ok := <-ch2:
		if ok {
			fmt.Println("ch2中取数据。。", num2)
		}else{
			fmt.Println("ch2通道已经关闭。。")
		}


	}
}

select语句结合time包的和chan相关函数:

package main

import (
	"fmt"
	"time"
)

func main() {
	ch1 := make(chan int)
	ch2 := make(chan int)

	//go func() {
	//	ch1 <- 100
	//}()

	select {
	case <-ch1:
		fmt.Println("case1可以执行。。")
	case <-ch2:
		fmt.Println("case2可以执行。。")
	case <-time.After(3 * time.Second):
		fmt.Println("case3执行。。timeout。。")

	//default:
	//	fmt.Println("执行了default。。")
	}
}

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值