Go语言学习笔记(并发编程)

并发编程

并行和并发
并行(parallel):指在同一时刻,有多条指令在多个处理器同时执行。

  |
 A|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||----->
  |                                                                     Parallelism:1.Multiprocessores,Multicore
  |                                                                     2.Physically simultaneous processing
 B|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||----->
  |
  |
 C|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||----->
  |
  |————————————————————————————————————————————————————————————————>>

并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮流执行,
使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,
使多个进程快速交替的执行。

  |
 A||||||||||-----------------|||||||||----------------------------->
  |                                                                     Concurrency:1.Single Processor
  |                                                                     2.logically simultaneous processing
 B|---------||||||||------------------|||||||||-------------------->
  |                                        
  |                                        
 C|----------------|||||||||-------------------|||||||||----------->
  |
  |————————————————————————————————————————————————————————————————>>

|—| 并行是两个队列同时使用两台咖啡机
|—| 并发是两个队列交替使用一台咖啡机
在这里插入图片描述

并发是在有限的资源中有效的利用cpu的性能

Go语言并发优势
有人把Go比作21世纪的C语言,第一是因为Go语言设计简单,第二,21世纪最重要的就是并发程序设计,而Go从语言层面就支持了并发。
同时,并发程序的内存管理有时候是非常复杂的,而Go语言提供了自动垃圾回收机制。
Go语言为并发编程而内置的上层API基于CSP(communicating sequential processes,顺序通信进程)模型。这就意味着显示锁都是可以避免的,
因为Go语言通过安全的通道发送和接受数据以实现同步,这大大地简化了并发程序的编写。
一般情况下,一个普通的桌面计算机跑十几二十个线程就有点负载过大了,但是同样这台机器却可以轻松地让成百上千甚至过万个goroutine进行资源竞争。

goroutine
goroutine是什么
goroutine是Go并行设计的核心。goroutine说到底其实就是协程,但是它比线程更小,十几个groutine可能体现在底层就五六个线程,Go语言
内部帮你实现了这些goroutine之间的内存共享。执行goroutine只需极少的栈内存(大概是4~5KB),当然会根据相应的数据伸缩。也正是因为
如此,可同时运行成千上万个并发任务。goroutine比thread更易用,更高效,更轻便。

创建goroutine (go的协程:比线程更小的单位,一个协程对应一个任务)

只需要在函数调用语句前添加go关键字,就可以创建并发执行单元。开发人员无需了解任何执行细节,
调度器会自动将其安排到合适的系统线程上执行。

在并发编程里,我们通常想将一个过程切分成几块,然后让每个goroutine各自负责一块工作。当一个
程序启动时,其主函数即在一个单独的goroutine中运行,我们叫它main goroutine。
新的goroutine 会用go语句来创建

实例:

package main

import (
	"fmt"
	"time"
)

func newTask() {
	for {
		fmt.Println("this is a newTask")
		time.Sleep(time.Second) //延时1s
	}

}

func main() {
	go newTask() //新建一个协程,新建一个任务
	for {
		fmt.Println("this is a main goroutine")
		time.Sleep(time.Second) //延时1s
	}

}

如果main goroutine 退出来,那么其他子协程也要跟着退出
实例:

package main
import (
	"fmt"
	"time"
)
func main() {
	go func() {
		i := 0
		for {
			fmt.Println("子协程 i=", i)
			time.Sleep(time.Second) //延时1s
		}
	}() //不要忘记()调用

	i := 0
	for {
		i++
		fmt.Println("main i=", i)
		time.Sleep(time.Second) //延时1s
		if i == 2 {
			break
		}
	}

}

主协程先退出,导致子协程没有打印结果(注意一下)
实例:

package main
import (
	"fmt"
	"time"
)
func main() {
	go func() {
		i := 0
		for {
			fmt.Println("子协程 i=", i)
			time.Sleep(time.Second) //延时1s
		}
	}() //不要忘记()调用

}

runtime包
1.Gosched
runtime.GOsched()用于让出CPU时间片,让出当前goroutine的执行权限,调度器安排其他等待的任务运行,并在下次某个时候
从该位置恢复执行。
这个就像跑接力赛,A跑了一会碰到代码runtime.Gosched() 就把接力棒交给了B,A歇着了,B继续跑。

package main

import (
	"fmt"
	"runtime"
)

func main() {
	go func() {
		for i := 0; i < 5; i++ {
			fmt.Println("go")
		}
	}()
	for i := 0; i < 2; i++ {
		//让出时间片,先让别的协程执行,它执行完,再回来执行此协程
		runtime.Gosched()
		fmt.Println("hello")
	}
}

2.Goexit
调用runtime.Goexit()将立即终止当前goroutine的执行,调度器确保所有已注册defer延迟调用被执行。

package main

import (
	"fmt"
	"runtime"
)

func test() {
	defer fmt.Println("ccccccccccccccc")
	//return //终止此函数
	runtime.Goexit()
	fmt.Println("dddddddddddddddddd")
}

func main() {
	go func() {
		fmt.Println("aaaaaaaaaaaaaaaaa")
		test()
		fmt.Println("bbbbbbbbbbbbbbb")
	}()

	//特地写一个死循环,不让主协程结束
	for {

	}
}

3.GOMAXPROCS
调用runtime.GOMAXPROCS() 用来设置可以并行计算的CPU核数的最大值,并返回之前的值

package main

import (
	"fmt"
	"runtime"
)

func main() {
	n := runtime.GOMAXPROCS(1) //指定以单核运算/2
	fmt.Println("n= ", n)
	for {

		go fmt.Print(1)
		fmt.Print(0)

	}

}

在第一次执行(runtime.GOMAXPROCS(1))时候,最多同时只能有一个goroutine被执行。所以会打印很多1。过来一段时间后,GO调度器
会将其置为休眠,并唤醒另一个goroutine,这是就开始打印很多0了,在打印的时候,goroutine是被调度到操作系统线程上的。
在第二次执行(runtime.GOMAXPROCS(2))时,我们使用了两个CPU,所以连个goroutine可以一起被执行,以同样的频率交替打印0和1。

多任务资源竞争问题

package main

import (
	"fmt"
	"time"
)

//定义一个打印机,参数为字符串,按每个字符打印
//打印机属于公共资源
func Printer(str string) {
	for _, data := range str {
		fmt.Printf("%c", data)
		time.Sleep(time.Second)
	}
	fmt.Println()
}
func person1() {
	Printer("hello")
}
func person2() {
	Printer("world")
}

func main() {
	//新建2个协程,代表2个人,2人同时使用打印机

	go person1()
	go person2()

	//特地的不让主协程结束,死循环
	for {
	}
}

导致输出时交替进行,不符合规则

channel
goroutine运行在相同的地址空间,因此访问共享内存必须做好同步。goroutine奉行通过通信
来共享内存,而不是共享内存来通信。
引用类型channel是CSP模式的具体实现,用于多个goroutine的通讯。其内部实现了同步,确保并发安全。

channel类型
和map类似,channel也是一个对应make创建的底层数据结构的引用。
当我们复制一个channel或用于函数参数传递时,我们只是拷贝了一个channel引用,因此调用者
和被调用者将引用同一个channel对象。和其它的引用类型一样,channel的零值也是nil

定义一个channel时,也需要定义发送到channel的值的类型。channel可以使用内置的make()
函数来创建:
make(chan Type) //等价于make(chan Type , 0)
make(chan Type ,capacity)
当capacity=0时,channel时无缓冲阻塞读写的,当capacity>0时,channel有缓冲,是非阻塞的,
直到写满capacity个元素才阻塞写入。

channel通过操作符“<-”来接收和发送数据,发送和接收数据语法:
  channel  <- value        //发送value到channel
  <-channel               //接收并将其丢弃
  x:=<-channel            //从channel中接收数据,并赋值给x
  x,ok:=<channel         //功能同上,同时检查通道(管道)是否已关闭或者是否为空
默认情况下,channel接收和发送数据都是阻塞的,除非另一端已经准备好,这样就
使得goroutine同步变动更加简单,而不需要显示的lock。

通过channel实现同步,即有先有后

package main

import (
	"fmt"
	"time"
)

//全局变量,创建一个channel
var ch = make(chan int)

//定义一个打印机,参数为字符串,按每个字符打印
//打印机属于公共资源
func Printer(str string) {
	for _, data := range str {
		fmt.Printf("%c", data)
		time.Sleep(time.Second)
	}
	fmt.Println()
}

//目的是person1执行完person2再执行
func person1() {

	Printer("hello")
	ch <- 666 //给管道写数据,发送
}
func person2() {
	<-ch //从管道取数据,接收,如果没有数据它就会阻塞
	Printer("world")
}

func main() {
	//新建2个协程,代表2个人,2人同时使用打印机

	go person1()
	go person2()

	//特地的不让主协程结束,死循环
	for {
	}

}

无缓存channel同步再探

package main

import "fmt"
import "time"

func main() {
	ch := make(chan string) //创建channel
	defer fmt.Println("主协程也结束")

	go func() {
		defer fmt.Println("子协程调用完毕")
		for i := 0; i < 2; i++ {
			fmt.Println("子协程 i=", i)
			time.Sleep(time.Second)
		}
		ch <- "我是子协程,我完事了" //没有它会deadlock
	}()

	str := <-ch //没有数据前会阻塞
	fmt.Println("str=", str)

}

无缓冲的channel
无缓冲的通道(unbuffered channel)是指在接收前没有能力保存任何值的通道。
这种类型的通道要去发送goroutine和接收goroutine同时准备好,才能完成发送和接收操作。
如果两个goroutine没有同时准备好,通道会导致先执行发送或街火速操作的goroutine阻塞等待。
这种对通道进行发送和接收的交互行为本身就是同步的。其中任意一个操作都无法离开另一个操作独立存在。
下图展示两个goroutine如何利用无缓冲的通道来共享一个值:
在这里插入图片描述

在第一步,两个goroutine都到达通道,但哪个都没有开始执行发送或者接收。
在第二步,左侧的goroutine将它的手伸进通道,者模拟了向通道发送数据的行为。
这时候,这个goroutine会在通道中被锁住,直到交换完成。
在第三步,右侧的goroutine将手放入通道,这模拟了从同通道里接收数据。这个
goroutine一样也会在通道中被锁住,直到交换完成。
第四步和第五步,进行交换,并最终,在第6步,两个goroutine都将它们的手从通
道里拿出来,这模拟了
被锁住的goroutine得以释放。两个goroutine现在都可以去做别的事情了。

无缓冲的channel创建格式:
make(chan Type)//等价于make(chan Type , 0)
如果没有指定缓冲区容量,那么该通道就是同步的,因此会阻塞到发送者准备好发送和
接收者准备好接收。

package main

import "fmt"
import "time"

func main() {
	// 创建一个无缓存的channel
	ch := make(chan int, 0)
	//len(ch)缓冲区神域数据个数,cap(ch)缓冲区大小
	fmt.Printf("len(ch)=%d , cap(ch)=%d\n", len(ch), cap(ch))
	//新建协程
	go func() {
		for i := 0; i < 3; i++ {
			fmt.Println("子协程i= ", i)
			ch <- i //往chan里写内容
			//fmt.Printf("len(ch)=%d , cap(ch)=%d\n", len(ch), cap(ch))
		}
	}()

	//延时
	time.Sleep(2 * time.Second)
	for i := 0; i < 3; i++ {
		num := <-ch //读管道中的内容,没有内容前,阻塞
		fmt.Println("num= ", num)

	}

}



/*
len(ch)=0 , cap(ch)=0
子协程i=  0
num=  0
子协程i=  1
子协程i=  2
num=  1
num=  2
*/

有缓冲的channel
有缓冲的通道(buffered channel)是一种在被接收前能存储一个或者多个值的通道。
这种类型的通道并不强制要求goroutine之间必须同时完成发送和接收,通道会阻塞
发送和接收动作的条件也会不同。只有在通道中没有要接收的值时,接收动作才会阻塞。
只有在通道没有可用缓冲区容纳被发送的值时,发送动作才会阻塞。
这导致有缓冲的通道和无缓冲的通道之间的一个很大的不同:无缓冲的通道保证进行发送和
接收的goroutine会在同一时间进行数据交换;有缓冲的通道没有这种保证。
示例图如下:
在这里插入图片描述

第一步,右侧的goroutine正从通道接收一个值。
第二步,右侧的这个goroutine独立完成了接收值的动作,而左侧的goroutine正在
发送一个新值到通道里。
第三步,左侧的goroutine还在向通道发送新值,而右侧的goroutine正从通道接收另外一个值。
这个步骤里的两个操作既不是同步的,也不会互相阻塞。
最后,在第四步,所有的发送和接收全部完成,而通道里还有几个值,也有一些空间可以
存更多的值。
有缓冲的channel创建格式:
make (chan Type,capacity)
如果给定了一个缓冲区容量,通道就是异步的。只要缓冲区有未使用空间用于发送数据,或还包含可以接收
的数据,那么其通信就会无阻塞地进行。

有缓冲的channel实例:

package main

import "fmt"
import "time"

func main() {
	// 创建一个有缓存的channel
	ch := make(chan int, 3)
	//len(ch)缓冲区神域数据个数,cap(ch)缓冲区大小
	fmt.Printf("len(ch)=%d , cap(ch)=%d\n", len(ch), cap(ch))
	//新建协程
	go func() {
		for i := 0; i < 10; i++ {
			ch <- i //往chan里写内容
			fmt.Printf("子协程[%d]=: len(ch)=%d , cap(ch)=%d\n", i, len(ch), cap(ch))
		}
	}()

	//延时
	time.Sleep(2 * time.Second)
	for i := 0; i < 10; i++ {
		num := <-ch //读管道中的内容,没有内容前,阻塞
		fmt.Println("num= ", num)

	}

}

//异步就像发短信一样,你干你的事情,我干我的事情
//同步就像打电话一样

range和close()
如果发送者知道,没有更多的值需要发送到channel的话,那么让接收者也能及时知道有没有多余的
值可接受是有用的,因为接收者可以停止不必要的接收等待。这可以通过内置的close()函数来关闭channel实现。

关闭channel

package main

import "fmt"

func main() {
	// 创建一个无缓存的channel
	ch := make(chan int, 3)
	//新建协程
	go func() {
		for i := 0; i < 5; i++ {
			ch <- i //往管道里写数据
		}
		//不需要再写数据时,关闭channel
		close(ch) //关闭管道命令
	}()

	for {
		//如果ok为true,说明管道没有关闭,为false说明管道已经关闭
		if num, ok := <-ch; ok == true {
			fmt.Println("num=", num)
		} else { //管道关闭
			break
		}

	}

}


//close(ch) //关闭管道命令可以被实时监测到

注意点:
channel不像文件一样需要经常去关闭,只有当你确定没有任何发送数据了,或者
你想显示的结束range循环之类的,才去关闭channel
关闭channel后,无法向channel再发送数据(引发panic错误后导致接收立即返回零值)
关闭channel后,可以继续向channel接收数据;
对于nil channel,无论收发都会被阻塞

可以使用range来迭代不断操作channel

package main

import "fmt"

func main() {
	// 创建一个无缓存的channel
	ch := make(chan int, 3)
	//新建协程
	go func() {
		for i := 0; i < 5; i++ {
			ch <- i //往管道里写数据
		}
		//不需要再写数据时,关闭channel
		close(ch) //关闭管道命令
	}()

	 for num:=range ch{
                        fmt.Println("num=  ",num")
                  }
}

单向的channel
默认情况下,通道是双向的,也就是,既可以往里面发送数据也可以从里面接收数据。
但是,我们经常见一个通道作为参数进行过传递而只希望对方是单向使用的,要么只让它
发送数据,要么只让它接收数据,这时候我们可以指定通道的方向。
单向channel变量的声明非常简单,如下:

var ch1 chan int //ch1是一个正常的channel,不是单向的
var ch2 chan<- float64 //ch2是单向channel,只用于写float64数据
var ch3 <-chan int       //ch3是单向channel,只用于读取int数据

|-----| chan<- 表示数据进入管道,要把数据写进管道,对于调用者就是输出。
|-----| <-chan 表示数据从管道出来,对于调用者就是得到管道的数据,淡然就是输入。
可以将channel隐式转换为单向队列,只收或只发, 不能将单向channel转换为普通channel

package main

//import "fmt"
func main() {
	//创建一个channel,双向的
	ch := make(chan int)
	//双向channel能隐式转换为单向channel
	var writeCh chan<- int = ch //只能写,不能读
	writeCh <- 666

	var readCh <-chan int = ch //只能读,不能写
	<-readCh
	
	//单向无法转换为双向

}

单向channel的应用:

package main

import "fmt"

//此通道只能写,不能读
func producer(out chan<- int) {
	for i := 0; i < 10; i++ {
		out <- i * i
	}
	close(out)
}

//此通道只能读,不能写
func consumer(in <-chan int) {
	for num := range in {
		fmt.Println("num=", num)
	}
}

func main() {
	//创建一个channel,双向的
	ch := make(chan int)
	//生产者,生产数字,写入channel
	//新开一个协程
	go producer(ch) //channel传参,引用传递
	//消费者,从channel读取内容,打印
	consumer(ch)

}

//生产一个消费一个

定时器
Timer
Timer 是一个定时器,代表未来的一个单一事件,你可以告诉timer你要等待多长时间,它提供
一个channel,在将来的那个时间那个channel提供了一个时间值。

package main

import "time"
import "fmt"
func main() {
	//创建一个定时器,设置时间为2s,2s后,往time通道写内容(当前时间)
	timer := time.NewTimer(2 * time.Second)
	fmt.Println("当前时间:", time.Now())
	//2s后,往timer.C写数据,有数据后,就可以读取
	t := <-timer.C //channel没有数据后阻塞
	fmt.Println("t=", t)

}

验证time.NewTimer(),时间到了,只会响应一次

package main

import "time"
import "fmt"

func main() {

	timer := time.NewTimer(1 * time.Second)
	for {
		<-timer.C
		fmt.Println("时间到")
	}

}
//会死锁

通过Timer实现延时功能 (共三种)

package main

import "time"
import "fmt"

func main() {
	//延时2s后打印一句话
	timer := time.NewTimer(2 * time.Second)
	<-timer.C
	fmt.Println("时间到")

	time.Sleep(2 * time.Second)
	fmt.Println("时间到")
	<-time.After(2 * time.Second)
	//定时2s,阻塞2s,2s后产生一个事件,往channel写内容
	fmt.Println("时间到")

}

停止和重置定时器

定时器的停止

package main

import "time"
import "fmt"

func main() {
	//延时2s后打印一句话
	timer := time.NewTimer(3 * time.Second)
	go func() {
		<-timer.C
		fmt.Println("子协程可以打印了,因为定时器时间到")
	}()
	timer.Stop() //停止定时器
	for {

	}

}

定时器的重置

package main

import "time"
import "fmt"

func main() {
	//延时2s后打印一句话
	timer := time.NewTimer(3 * time.Second)
	ok := timer.Reset(1 * time.Second)
	fmt.Println("ok= ", ok)
	<-timer.C
	fmt.Println("时间到")

}

Ticker
Ticker是一个定时触发的定时器,它会以一个间隔(interval)往channel发送一个事件
(当前时间),而channel的接收者可以以固定的时间间隔从channel中读取事件。
Ticker像闹钟一样

package main

import "time"
import "fmt"

func main() {
	//延时2s后打印一句话
	ticker := time.NewTicker(1 * time.Second)

	i := 0
	for {
		i++
		<-ticker.C
		fmt.Println("时间到i=", i)
		if i == 5 {
			ticker.Stop()
			break
		}
	}

}

select
select作用
Go里面提供了一个关键字select,通过select可以监听channel上的数据流动。
select的用法与switch语法非常类似,由select开始一个新的选择块,每个选择条件由case
语句来描述。
与switch语句可以选择任何可使用相等比较的条件相比,select有比较多的限制,其中最大
的一条限制就是 每个case语句里必须是一个IO操作 ,大致的结构如下:

select{
  case <-chan1:
     //如果chan1成功读到数据,则进行该case处理语句
  case chan2<-1:
   //如果成功向chan2写入数据,则进行该case处理语句
 default:
  //如果上面都没有成功,则进入default处理流程
}

在一个select语句中,Go语言会按顺序从头到尾评估每个发送和接收的语句。
如果其中的任意一语句可以继续执行(即没有被阻塞),那么就从那些
可以执行的语句中任意选择一条来使用。

如果没有任意一条语句可以执行(即所有的通道都被阻塞),那么有两种可能的情况:
1.如果给出了default语句,那么就会执行default语句,同时程序的执行从select语句后的
语句中恢复
2.如果没有default语句,那么select语句将被阻塞,直到至少有一个通信可以进行下去。
在现实使用中很少使用default语句。

斐波那契数列

package main

import "fmt"

func fibonacci(ch chan<- int, quit <-chan bool) {
	x, y := 1, 1
	for {
		//监听channel数据流动
		select {
		case ch <- x:
			x, y = y, x+y
		case flag := <-quit:
			fmt.Println("flag=", flag)
			return
		}

	}
}

func main() {
	ch := make(chan int)    //数字通信
	quit := make(chan bool) //程序是否结束

	//消费者,从channel读取内容
	//新建协程
	go func() {
		for i := 0; i < 8; i++ {
			num := <-ch
			fmt.Println("num=", num)
		}
		//可以停止了
		quit <- true
	}()//别忘了()

	//生产者,产生数字,写入channel
	fibonacci(ch, quit)

}

超时
有时候会出现goroutine阻塞的情况,那么我们如何避免整个程序进入阻塞的情况呢?我们可以
利用select来设置超时,通过如下的方式实现:

package main

import (
	"fmt"
	"time"
)

func main() {
	ch := make(chan int)
	quit := make(chan bool)
	//新开一个协程
	go func() {
		for {
			select {
			case num := <-ch:
				fmt.Println("num=", num)
			case <-time.After(3 * time.Second):
				fmt.Println("超时")
				quit <- true
			}

		}
	}()

	for i := 0; i < 5; i++ {
		ch <- i
		time.Sleep(time.Second)
	}

	<-quit
	fmt.Println("程序结束")

}

//银行网站,登录后多少秒后就自动退出登录

(本笔记整理自网络资源,侵删)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值