前言
go语言是并发语言,至于并发与并行的区别,简单来说就是,并发就是同时处理很多事情,它强调的是处理能力,并不是同时在做,并行就是同时在做多个事情,举个简单的例子,你在敲代码,口渴了去喝水(水杯就在旁边),你能同时处理这两件事,就叫并发,你也可以边敲代码边听音乐,这就是并行,并发强调事务的交叠性,并行则强调事务的同时运作。
进程,线程,协程
进程这有什么难理解的,你电脑上每时每刻都有多个进程,简单说就是正在执行的程序,是cpu资源分配与调度的独立单位,进程创建,撤销以及切换的开销是比较大的
线程 被称为轻量级进程,是一个基本的cpu执行单元,也是程序执行过程的最小单元,一个进程包含多个线程。线程的存在减少了程序并发执行时的开销,但是线程自己并没有自己的系统资源,只存在一些运行必须的资源,在统一进程中多个线程可以同时共享进程所拥有的的系统资源,不过对于某些独占资源的锁机制,处理不当可能会产生死锁
协程是一种用户态的轻量级线程 ,之所以这样说,是因为协程的调度完全由用户控制。协程最大的优势是轻量级,可以创建上百万甚至千万的协程,而进程与线程通常不能超过1万
对于go语言对于并非的实现靠岸的是协程,Goroutine
使用Goroutine
在函数或方法调用前面加上关键字go,就会同时运行一个新的Goroutine
func hello() {
fmt.Println("Hello world goroutine")
}
func main() {
go hello()
fmt.Println("main function")
}
main function
为什么呢,go的主goroutine运行在这里并没有等其他的goroutine,所以main运行完之后直接就结束了,相当于hello函数刚刚开始就被中断了,这与goroutine的运行机制有关,下面摘抄教程的规则:
当新的Goroutine开始时,Goroutine调用立即返回。与函数不同,go不等待Goroutine执行结束。当Goroutine调用,并且Goroutine的任何返回值被忽略之后,go立即执行到下一行代码。
main的Goroutine应该为其他的Goroutines执行。如果main的Goroutine终止了,程序将被终止,而其他Goroutine将不会运行。
修改后的代码
func hello() {
fmt.Println("Hello world goroutine")
}
func main() {
go hello()
time.Sleep(1 * time.Second)
fmt.Println("main function")
}
这里加了一个sleep函数,让主goroutine等待了一段时间
Hello world goroutine
main function
那么如果启动多个goroutine该怎么办呢
直接附上代码
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() {
go numbers()
go alphabets()
time.Sleep(3000 * time.Millisecond)
fmt.Println("main terminated")
}
在上面的每个函数里都定义了一定的等待时间,这样就可以保证多个goroutine就可以正常的进行工作了
1 a 2 3 b 4 c 5 d e main terminated
runtime包
本来在想要不要将这个,但是觉得有很有必要说一下,那我就简单的把它的用途说一下吧。
1.获取goroot和os
//获取goroot目录:
fmt.Println("GOROOT-->",runtime.GOROOT())
//获取操作系统
fmt.Println("os/platform-->",runtime.GOOS)
2.获取,设置CPU数量
func init(){
//1.获取逻辑cpu的数量
fmt.Println("逻辑CPU的核数:",runtime.NumCPU())
//2.设置go程序执行的最大的:[1,256]
n := runtime.GOMAXPROCS(runtime.NumCPU())
fmt.Println(n)
}
3.Goshed()
func main() {
go func() {
for i := 0; i < 5; i++ {
fmt.Println("goroutine。。。")
}
}()
for i := 0; i < 4; i++ {
//让出时间片,先让别的协议执行,它执行完,再回来执行此协程
runtime.Gosched()
fmt.Println("main。。")
}
}
goroutine。。。
goroutine。。。
goroutine。。。
goroutine。。。
goroutine。。。
main。。
main。。
main。。
main。。
4.Goexit的使用
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函数。。。")
}
临界资源安全问题
所谓临界资源,就是并发过程中中,几个进程/线程/协程共同占有可以使用的资源。所谓并发并不难,只是对于临界资源的争夺成了一大难题,很多编程语言都是通过上锁的方式来解决的,所以我们可以通过sync包来访问
下面摘抄教程上的一段代码,注释写的很详细
import (
"fmt"
"math/rand"
"time"
"sync"
)
//全局变量
var ticket = 10 // 100张票
var wg sync.WaitGroup
var matex sync.Mutex // 创建锁头
func main() {
/*
4个goroutine,模拟4个售票口,4个子程序操作同一个共享数据。
*/
wg.Add(4)
go saleTickets("售票口1") // g1,100
go saleTickets("售票口2") // g2,100
go saleTickets("售票口3") //g3,100
go saleTickets("售票口4") //g4,100
wg.Wait() // main要等待。。。
//time.Sleep(5*time.Second)
}
func saleTickets(name string) {
rand.Seed(time.Now().UnixNano())
defer wg.Done()
//for i:=1;i<=100;i++{
// fmt.Println(name,"售出:",i)
//}
for { //ticket=1
matex.Lock()
if ticket > 0 { //g1,g3,g2,g4
//睡眠
time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
// g1 ,g3, g2,g4
fmt.Println(name, "售出:", ticket) // 1 , 0, -1 , -2
ticket-- //0 , -1 ,-2 , -3
} else {
matex.Unlock() //解锁
fmt.Println(name, "售罄,没有票了。。")
break
}
matex.Unlock() //解锁
}
}
售票口1 售出: 10
售票口1 售出: 9
售票口3 售出: 8
售票口2 售出: 7
售票口4 售出: 6
售票口1 售出: 5
售票口3 售出: 4
售票口2 售出: 3
售票口4 售出: 2
售票口1 售出: 1
售票口3 售罄,没有票了。。
售票口4 售罄,没有票了。。
售票口2 售罄,没有票了。。
售票口1 售罄,没有票了。。
在go语言中一直提倡**不要以共享内存的方式去通信,而要以通信的方式去共享内存。**所以其实在go中并不提倡上锁,而是通过channel
channel通道
channel一般被认为是Goroutine通信的管道。一般来说goroutine会把自己数据封装成一个对象,把这个数据对象的指针存到channel,这样另一个goroutine就可以通过channel读出这个指针,并处理它指向的内存对象
声明通道
func main() {
var a chan int
if a == nil {
fmt.Println("channel 是 nil 的, 不能使用,需要先创建通道。。")
a = make(chan int)
fmt.Printf("数据类型是: %T", a)
}
}
channel 是 nil 的, 不能使用,需要先创建通道。。
数据类型是: chan int
当然了,也可以简单的神明a:=make(chan int)
通道的数据类型
通道是引用类型,作为参数时,传递的是内存地址
func main() {
ch1 := make(chan int)
fmt.Printf("%T,%p\n",ch1,ch1)
test1(ch1)
}
func test1(ch chan int){
fmt.Printf("%T,%p\n",ch,ch)
}
通道的使用方法
发送和接收
一般格式
data := <-a
a <-data
v,ok := <-a
一般来说发送和接收的默认值是阻塞的
直接看代码
func main() {
var ch1 chan bool //声明,没有创建
fmt.Println(ch1) //
fmt.Printf("%T\n", ch1) //chan bool
ch1 = make(chan bool) //0xc0000a4000,是引用类型的数据
fmt.Println(ch1)
go func() {
for i := 0; i < 10; i++ {
fmt.Println("子goroutine中,i:", i)
}
// 循环结束后,向通道中写数据,表示要结束了。。
ch1 <- true
fmt.Println("结束。。")
}()
data := <-ch1 // 从ch1通道中读取数据
fmt.Println("data-->", data)
fmt.Println("main。。over。。。。")
}
<nil>
chan bool
0xc0000160c0
子goroutine中,i: 0
子goroutine中,i: 1
子goroutine中,i: 2
子goroutine中,i: 3
子goroutine中,i: 4
子goroutine中,i: 5
子goroutine中,i: 6
子goroutine中,i: 7
子goroutine中,i: 8
子goroutine中,i: 9
结束。。
data--> true
main。。over。。。。
注意:如果一个goroutine在通道上发送数据,那么对应的其他通道应该接收数据,如果这种情况没有发生就会死锁,类似,如果goroutine正在等待从通道接收数据,那么另一些goroutine应该在通道上写入数据,否则还会死锁
关闭通道
发送者可以通过关闭通道close(ch),来通知接收方不会有更多的数据发送到channel
接收者可以在接收数据时增设额外的变量来检查通道是否关闭
v,ok:=<-ch
通道上的范围循环
直接上代码
import (
"time"
"fmt"
)
func main() {
ch1 :=make(chan int)
go sendData(ch1)
// for循环的for range形式可用于从通道接收值,直到它关闭为止。
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)//通知对方,通道关闭
}
读取数据: 0
读取数据: 1
读取数据: 2
读取数据: 3
读取数据: 4
读取数据: 5
读取数据: 6
读取数据: 7
读取数据: 8
读取数据: 9
main..over.....
缓冲通道与定向通道
缓冲通道
之前学的任何通道都是非缓冲的,发送和接收到一个未缓冲的通道是阻塞的
那么对于缓冲通道,就是指一个通道带有一个缓冲区,发送到一个缓冲通道只有缓冲区满的时候才被阻塞,同理,从缓冲通道接收的信息,只有在缓冲区为空的时候才会被阻塞
直接上代码
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)
}
0 0
0 5
1 5
--------------
子goroutine,写出第 0 个数据
子goroutine,写出第 1 个数据
子goroutine,写出第 2 个数据
子goroutine,写出第 3 个数据
读取的数据是: 数据0
子goroutine,写出第 4 个数据
读取的数据是: 数据1
子goroutine,写出第 5 个数据
读取的数据是: 数据2
子goroutine,写出第 6 个数据
读取的数据是: 数据3
子goroutine,写出第 7 个数据
读取的数据是: 数据4
子goroutine,写出第 8 个数据
读取的数据是: 数据5
子goroutine,写出第 9 个数据
读取的数据是: 数据6
读取的数据是: 数据7
读取的数据是: 数据8
读取的数据是: 数据9
读完了,, false
main...over...
双向通道
就是截止到如今我们所学的通道既可以发送数据,又可以读取数据,我们称之为双向通道
单向通道
直接上代码
func main() {
/*
单向:定向
chan <- T,
只支持写,
<- chan T,
只读
用于参数传递:
*/
ch1 := make(chan int)//双向,读,写
//ch2 := make(chan <- int) // 单向,只写,不能读
//ch3 := make(<- chan int) //单向,只读,不能写
//ch1 <- 100
//data :=<-ch1
//ch2 <- 1000
//data := <- ch2
//fmt.Println(data)
// <-ch2 //invalid operation: <-ch2 (receive from send-only type chan<- int)
//ch3 <- 100
// <-ch3
// ch3 <- 100 //invalid operation: ch3 <- 100 (send to receive-only type <-chan int)
//go fun1(ch2)
go fun1(ch1)
data:= <- ch1
fmt.Println("fun1中写出的数据是:",data)
//fun2(ch3)
go fun2(ch1)
ch1 <- 200
fmt.Println("main。。over。。")
}
//该函数接收,只写的通道
func fun1(ch chan <- int){
// 函数内部,对于ch只能写数据,不能读数据
ch <- 100
fmt.Println("fun1函数结束。。")
}
func fun2(ch <-chan int){
//函数内部,对于ch只能读数据,不能写数据
data := <- ch
fmt.Println("fun2函数,从ch中读取的数据是:",data)
}
fun1函数结束。。
fun1中写出的数据是: 100
main。。over。。
其他的time包之类的通道相关函数我就不说了,找度娘吧
到这里go的基础篇就更完了,下面我就要具体写一个项目(简单的小游戏之类),博客也会更新这个项目的相关内容,我也会把我的代码发到我的github上,还有建议在用github的伙伴尽快把自己的相关资源copy一份到gitee上,因为漂亮国估计又要耍无赖了,下面附上操作链接,希望大家未雨绸缪
https://mp.weixin.qq.com/s/daGQBccYVdD07otdQrt9AA