引言

假设,我们现在要求统计40000000的数字中,那些是整数?

按照我们前面的思路就是直接使用for循环,然后去判断这些是不是整数,如下

main.go

package main

func Sum()  {
	var xx []int
	for i := 0; i < 40000000;i++{
		if i % 2 == 0 {
			xx = append(xx,i)
		}
	}
}

func main(){
   Sum()
}

main_test.go

package main

import "testing"

func TestSum(t *testing.T) {  
	Sum()   
}

测试

D:\go_setup\go1.17\src\go_code\go_pro\main>go test -v main_test.go main.go
=== RUN   TestSum
--- PASS: TestSum (0.34s)
PASS
ok      command-line-arguments  1.453s

在运行程序的时候发现编辑器会卡住,一直等待程序完成后才结束

我们下面通过协程来解决运行效率低、编辑器卡住的问题,先了解一些知识

一、进程和线程说明

1、 "进程"就是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位

2、 "线程"是进程的一个执行实例,是程序执行的最小单位,它是比进程更小的能独立运行的基本单位
    //比如迅雷软件,我们打开迅雷就会启动一个进程 
    //我们通过迅雷同时下载多个文件,每个下载的任务都是一个线程

3、一个进程可以创建和销毁多个线程,同一个进程中的多个线程可以"并发"执行

4、 一个程序至少有一个"进程",一个进程至少有一个"线程"

说白了,进程就是头头,线程就是小弟,线程越多干活的人就越多

进程和线程关系图

 比如,我们在windows上启动一个迅雷,在资源管理器中我们能看到迅雷的进程

在我们下载任务的时候,实际上是由迅雷进程创建出几个线程去完成下载操作的

线程就相当于子任务,不会影响到进程的运行,并且同时运行多个效率会提升

二、并发和并行

1、什么是并发

我们同时启动多个线程,这些线程运行在同一个cpu上就是并发

从微观的角度看,在一个时间点上,其实只有一个任务在执行

2、什么是并行

多个线程在多颗cpu上运行,并行的效率比并发要高

 小结

并发,因为是在一个cpu上跑,当有多个线程时,看起来是都在跑实际上只有一个在跑

并行,根据线程数量平均的分配到不同的cpu上,所有线程都在跑

 三、协程(goroutine)

在go语言中,进程被称为主线程,基于主线程创建的一种特殊的线程被称为协程(goroutine协程比起上面说的线程更加的轻量级,可以轻松启动上万个

协程的特点

1、有独立的栈空间    //一旦开启一个协程,它有独立的空间
2、共享程序堆空间
3、调度由用户可控制   //程序开启和停止是程序员控制的
4、协程是轻量级的线程

go语言中的主线程和协程关系图

四、goroutine 快速入门

我们先模拟一个并发的场景,代码如下

package main

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

//编写一个函数,该函数每隔1秒输出"hello world"
func test(){
	for i := 1; i <= 5; i++ {
		fmt.Println("test()  hello world" + strconv.Itoa(i)) //strconv.itoa  数字转字符串
		time.Sleep(time.Second)
	}
}
func main(){
	test()
	for i := 1; i <= 5 ;i++ {
		fmt.Println("main()  hello golang" + strconv.Itoa(i))
		time.Sleep(time.Second)
	}
}

返回

test()  hello world1
test()  hello world2
test()  hello world3
test()  hello world4
test()  hello world5
main()  hello golang1
main()  hello golang2
main()  hello golang3
main()  hello golang4
main()  hello golang5

上面并发的场景下,他会先执行test()函数下的代码进行输出,而主线程等待

当test()执行完毕后,才会继续跑主线程,这样就是并发,将所有任务放在一颗cpu上

案例2 并行执行

我们创建一个协程的方法很简单,在要用的函数前添加go 字段即可

package main

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


func test(){
	for i := 1; i <= 5; i++ {
		fmt.Println("test()  hello world" + strconv.Itoa(i)) 
		time.Sleep(time.Second) 
	}
}
func main(){
	go test()   //我们在调用test函数是在前面添加关键字 go
                //表明我们开启一个协程来运行这个函数
                //协程不会影响我们主线程,所以会同时运行main中的程序
	for i := 1; i <= 5 ;i++ {
		fmt.Println("main()  hello golang" + strconv.Itoa(i))
		time.Sleep(time.Second)
	}
}

返回

main()  hello golang1
test()  hello world1
main()  hello golang2
test()  hello world2
test()  hello world3
main()  hello golang3
main()  hello golang4
test()  hello world4
test()  hello world5
main()  hello golang5

因为是并行执行,完成的顺序是不确定的,所以结果是无序的

不过这样会调用不同的cpu进行工作,大大的增加程序运行的效率

协程运行流程图

 

 小结

1、主线程是一个物理线程,直接作用在cpu上的。 是重量级的,非常消耗cpu资源
2、协程是从主线程开启的,是轻量级的线程,是逻辑态。堆资源消耗相对较小
3、golang的协程机制是重要的特点,可以轻松的开启上万个协程。

五、设置Golang协程运行的cpu数量

在golang中有个runtime包是用于查看系统环境资源的,我们利用这个包来最大化cpu性能

可以找到NumCPU、GOMAXPROCS的函数,用于查询当前系统有几颗CPU

并且设置我们可以使用多少颗cpu

案例

package main

import (
	"fmt"
	"runtime"

)

func main(){
	num := runtime.NumCPU()  //这里得到当前所有的cpu数量
	fmt.Printf("couNum=%v\n",num)

	runtime.GOMAXPROCS(num-1) //这里我们预留一个cpu给其他程序用
	fmt.Println("OK")
}

返回

couNum=12
OK

1.8版本之后,默认让程序运行在多个核上,可以不设置
1.8版本之前,要设置一下runtime.GOMAXPROCS(num-1)

六、使用协程测试开头的程序

这里主程序不变更,我们使用测试文件去开启协程进行测试

 main_test.go

package main

import "testing"

func TestSum(t *testing.T) {  //Test + 要测试的函数名
	go  Sum()   //直接调用该函数即可
}

测试

D:\go_setup\go1.17\src\go_code\go_pro\main>go test -v main_test.go main.go
=== RUN   TestSum
--- PASS: TestSum (0.00s)
PASS
ok      command-line-arguments  0.529s

对比开头的1.453s 可以看到运行效率得到了大幅度提升