Go语言自学笔记(六)

Go语言优势:并发编程。

并行和并发的区别:

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

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

Go语言并发优势:从语言层面支持并发并且支持自动垃圾回收机制。顺序通信进程避免显式死锁。创建goroutine协程,比线程更小,简单,实用。

协程的创建:

package main
import "time"    //可以调用语句time.Sleep(time.Second)延时一秒
func newTask(){}
func main(){
    go newTask()    //新建一个协程,子协程
    for{}    //主协程
}

主协程与子协程交替执行,任务调度器自动协调。但需要注意的是,如果主协程main先退出,子协程也会跟着退出。有事主协程退出可能导致子协程没有来得及调用。协程也可以通过go func(){}()创建给匿名函数。

协程相关runtime函数:需要导入runtime包。

runtime.Gosched()函数:用于让出CPU时间片,让出当前goroutine的执行权限,调度器安排其他等待的任务执行,并在下次某个时候从该位置恢复运行。

package main
import "time"
import "runtime"
func newTask(){}
func main(){
    go newTask()    //新建一个协程,子协程
    for{    //主协程
        runtime.Gosched()    //让出时间片,给其他协程执行,之后完成后调回此协程执行
    }
}

runtime.Foexit()函数:立即终止当前goroutine执行,调度器确保所有已经注册defer延迟调用被执行。

package main
import "time"
import "runtime"
import "fmt"
func newTask(){
    defer fmt.Println("延迟调用")
    //return 直接终止此函数但是不会终止协程
    runtime.Goexit()    //直接终止协程,在终止函数之后的所有语句都无法执行(不影响延迟语句的执行)
    fmt.Println("测试语句1")    //不会输出
}
func main(){
    go fun(){
        newTask()
        fmt.Println("测试语句2")    //不会输出
    }
    for{
        runtime.Gosched()    //让出时间片,给其他协程执行,之后完成后调回此协程执行
    }
}

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

func main(){
    n := runtime.GOMAXPROCS(1)    //指定单核运行,核数多可分配的时间片较多,执行较快。
}

多任务执行时存在着资源竞争问题:主协程运行时,多个子协程同时运行,代码执行较乱。

goroutine运行在相同的地址空间,因此访问共享内存必须做好同步。goroutine奉行通过通信来共享内存,而不是共享内存来通信,故引入协程同步及数据交换:channel类型。

通过channel实现同步:

package main
var ch = make(chan int)    //创建一个全局channel管道
func fun1(){    //先执行fun1,再向管道内发送数据留给fun2接收
    ch<-123    //向管道内发送数据
}
func fun2(){
    <-ch    //检测到管道内数据,接收数据并立即开始执行fun2。如果管道内没有数据则阻塞
}
func main(){
    go fun1()
    go fun2()
    for{}
}

通过channel实现同步以及数据交互:

func main(){
    ch := make(chan string)
    go func(){
        fmt.Println("子协程执行")
        ch<-"子协程工作完毕"
    }()
    str:=<-ch    //没有数据主协程阻塞
    fmt.Println(str)
}

无缓存的channel与有缓存的channel:

无缓存的channel:管道之中不能存放数据,存在两端阻塞:接收者接受数据之前发送者一直是阻塞的,反之同理。

定义方式:

make (chan Type)    //make (chan Type,0) 

如果没有指定缓冲区容量,那么该通道就是同步的,因此会阻塞到发送者准备好发送和接收者准备好接收。

內建函数:可以通过len()来判断缓冲区剩余的数据个数,通过cap()来查看缓冲区的大小。

有缓存的channel:管道中可以存放一定量的数据,解决两端阻塞问题,但如果数据超出存储能力也会存在阻塞。

定义方式:

make (chan Type, capacity)    //capacity为int类型的容量

如果给定了一个缓冲区容量,通道就是异步的。只要缓冲区有未使用的空间用于发送数据,或还包含可以接收的数据,那么其通信就会 无阻塞地进行。

package main
import "fmt"
var ch = make(chan int, 3)    //创建一个全局channel管道
func fun1(){    //执行fun1,多次向管道内发送数不存在阻塞
    for i:=0;i<3;i++{    //超过通道容量可能出现阻塞
        ch<-i
    }
}
func fun2(){
    for i:=0;i<3;i++{    //超过通道容量可能出现阻塞
        fmt.Println(<-ch)    //检测到管道内数据就可以开始接收数据并执行fun2
    }
}
func main(){
    go fun1()
    go fun2()
    for{}
}

程序有缓存的channel实现了一次性在管道中存入多个数据,不阻塞。但for传递的数据如果超出容量3仍然会阻塞。

channel的关闭:

for循环检测关闭:

func main(){
    ch := make(chan int)
    go func(){
        for i:=0;i<5;i++{
            ch<-i
        }
        close(ch)    //写入完成,关闭管道
    }()
    for{
        if num, ok := <-ch; ok == true{
            fmt.Println(num)
        } else {    //管道关闭
            break
        }
    }
}

迭代检测关闭:

func main(){
    ch := make(chan int)
    go func(){
        for i:=0;i<5;i++{
            ch<-i
        }
        close(ch)    //写入完成,关闭管道
    }()
    for num := range ch{    //可以自动跳出循环,更加简便
        fmt.Println(num)
    }
}

总结:

1.channel不像文件一样需要经常去关闭,只有当你确实没有任何发送的数据了,或者你想显式的结束range虚幻之类的,餐区关闭通道。

2.关闭channel后,无法向channel再发送数据(引发panic错误后导致接收立即返回零值)。

3.关闭channel后,可以继续向channel接收数据。

4.对于nil channel,无论收发搜会被阻塞。

单向管道与双向管道:

双向管道:能够正常进行收发

var ch1 chan int

单向管道:只能进行接收或者只能进行发送

var ch2 chan<- float64    //只能写入float64数据
var ch3 <-chan int    //只能读取int数据

chan<-表示数据进入管道,要把数据写进管道,对于调用者就是输出。

<-chan表示数据从管道出来,对于调用者就是得到管道的数据,当然就是输入。

单向管道只能编译不能运行,否则会出现死锁。但双向管道可以转化为单向管道,单向管道不能转化为双向管道:

ch := make(chan int)
var writeCh chan<- int = ch    //只能写不能读
var readCh <-chan int = ch    //只能读不能写

单向channel作为函数参数的应用:引用传递

func producer(out chan<- int) {
    for i:=1;i<10;i++ {
        out <- i*i
    }
    close(out)
}
func consumer(in <-chan int) {
    for num := range in{
        fmt.Println(num)
    }
}
func main(){
    ch := make(chan int)
    go producer(ch)    //引用传递
    consumer(ch)    //引用传递
}

定时器:

Timer定时:Timer是一个定时器,代表未来的一个单一事件(只会产生一次事件),你可以告诉timer你要等待多长时间,它提供一个channel,在将来的那个时间的channel提供了一个时间值。需要导入包time

package main
import "time"
import "fmt"
func main(){
    //创建一个定时器延时两秒,两面后向time通道写内容(当前时间)
    timer := time.NowTimer(2 * time.Second)    //延时2秒
    fmt.Println("当前时间:",time.Now())
    //2s后,向time.C写数据,检测到数据后可以进行读取
    t := <-timer.C    //没有数据则会阻塞
    fmt.Println(t)
}

三种方式通过timer进行延时:

func mian(){
    <-timer.After(2 * time.Second)    //定时2s阻塞2s,之后产生事件
    fmt.Println("时间到")
}
func main(){
    time.Sleep(2 * time.Second)
    fmt.Println("时间到")
}
func main(){
    timer := time.NewTimer(2 * time.Second)
    <-time.C
    fmt.Println("时间到")
}

定时器的停止:

func main(){
    timer := time.NewTimer(3 * time.Second)
    go func(){
        <-time.C
        fmt.Println("时间到")    //不会输出
    }
    timer.Stop()    //计时器被停止,通道不会被写入内容
    for{}
}

计时器的重置:

func main(){
    timer := time.NewTimer(3 * time.Second)
    timer.Reset(1 * time.Second)    //被更改为延时一秒,语句返回bool类型设置状态
    <-time.C
    fmt.Println("时间到")    //程序运行一秒后输出
}

Ticker定时:Ticker是一个定时出发的计时器,它会以一个间隔(interval)向一个通道内发送事件(当前时间)(循环产生事件),而channel的接收者可以以固定的事件间隔从channel中读取时间。需要导入time包

package main
import "time"
import "fmt"
func main(){
    ticker := time.NowTicker(1 * time.Second)
    i := 0
    for {
        <-ticker.C
        i++
        fmt.Println(i)
        if i==5 {
            ticker.Stop()
            break
        }
    }
}

select监听:

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

select {
case <-chan1:
    //如果chan1成功读到数据,则进行该case处理语句
case chan2 <- 1:
    //如果成功向chan2写入数据,则进行该case处理语句
default:
    //如果上面都没有成功,则进入default处理流程,但可能使程序由阻塞变为不阻塞,故一般default不写在select语块内
}

在一select语句中,Go语言会按顺序从头至尾评估每一个发送和接收的语句。

如果其中的任意一个语句可以继续执行(即没有被阻塞),那么就从那些可以执行的语句中任意选择一条来使用。

如果没有任意一条语句可以执行(即所有的通道都被阻塞),那么有两种可能情况:

1.如果给出了default语句,那么就会执行default语句,同时程序的执行会从select语句后的语句中恢复。

2.如果没有default语句,那么select语句将被阻塞,直到至少有一个通信可以进行下去。

用select实现斐波那契额数列:

package main
import "fmt"
//ch只写,quit只读
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)
		}
		//可以停止
		quit <- true
	}()
	//生产者,产生数字写入channel
	fibonacci(ch, quit)
}

select超时:有时候会出现goroutine阻塞的情况,那么我们可以利用select来设置超时,避免整个程序进入阻塞的状态。--长期阻塞所做的相应操作。

package main
import "fmt"
import "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("程序结束")
}

 

你可能感兴趣的:(Go语言)