Go语言入门到实战——14.Go语言的协程机制以及并发机制

本文深入探讨Go语言的并发特性,包括协程的轻量级特性、与线程的对比,以及如何通过协程避免资源消耗。介绍了Go的CSP并发模型,通过通道实现线程安全的共享内存,并展示了如何通过多路选择和超时控制机制优化并发执行。示例代码详细解释了Go语言中并发编程的实践应用。
摘要由CSDN通过智能技术生成

Go语言入门到实战——00主目录
在上一讲中我们学习了Go语言的包的知识已经依赖管理。
协程(groutine)是一种更加轻量级的线程(thread)。

一.协程与线程得到比较

1.对于java而言,在JDK5以后,线程创建时线程栈的默认大小为1M,而协程栈初始化栈大小为2K
2.两者与系统线程(KSE,Kernel Space Entity,内核对象)的对应关系,java的线程与KSE的
对应关系是1:1,而java的协程与KSE的对应关系是M:N的


我们可以看到当采用KSE与线程是1:1的关系的时候,在调度的时候KSE由CPU直接调度,对应的去调度线程的话效率虽然很高,但是如果是需要进行切换的话,这个时候1:1就必须一个个的切换,切换次数多了对资源的消耗是非常大的,而如果选择多对多的话则不会有这么高的消耗。

上面是协程与系统线程的多对多关系图。我们可以看到系统线程调度协程时,会有一个协程队列,队列里面的协程都在处于等待状态,还有一个协程(即上面的G0)则是正在由系统线程调度运行。当当前运行的协程(G0)发生中断后(系统中断M0导致G0也中断),Processor就会选择另外一个系统线程,就是上面左图到右图的过程,继续执行队列里面的其他的协程,当协程被唤醒后就会重新加入到队列的尾部中(中断发生后寄存器的协程状态会被保存在协程对象,当再次执行时就会再次放入寄存器中)。那么这里为了防止出现饿死的现象,GO是这么设计的:

GO语言里面有一个守护线程在运行,他会记录Processor已经完成的协程的数量,当这个数量在
一段时间内都没有发生变化的时候,他就会设置一个标记,而目前正在运行的协程在读到这个标
记后就会把自己诊断下来,然后将自己插入到等待队列的队尾去,前面的协程进入执行。

那么go启动一个协程的方式是比较简单的,在执行的函数前面加上go关键字就可以启动一个协程了。

package test

import "testing"

func TestGroutine(t *testing.T) {
	for i := 0; i < 10; i++ {
		go func(i int) {
			t.Log(i)
		}(i)
	}
}

package test
//这种情况协程共享i变量而造成打印很多一样的结果
import "testing"

func TestGroutine(t *testing.T) {
	for i := 0; i < 10; i++ {
		go func() {
			t.Log(i)
		}()
	}
}

二.共享内存并发机制

package test
//很明显这段代码是线程不安全的,输出的counter的值是小于5000的
import (
	"testing"
	"time"
)

func TestGroutineThreadSafe(t *testing.T) {
	var counter int = 0
	for i := 0; i < 5000; i++ {
		go func() {
			counter++
		}()
	}
	time.Sleep(2 * time.Second)
	t.Log(counter)
}


上面出现这个情况的原因是由于每一个协程都是在共享Couter变量,但是没有加锁导致的。
下面我们使用锁机制来避免这一情况:

package test
//加锁之后counter的结果就正常了
import (
	"sync"
	"testing"
	"time"
)

func TestGroutineThreadSafe(t *testing.T) {
	var mut sync.Mutex
	var counter int = 0
	for i := 0; i < 5000; i++ {
		go func() {
			defer func() {
				mut.Unlock()
			}()
			mut.Lock()
			counter++
		}()
	}
	time.Sleep(1 * time.Second)
	t.Log(counter)
}


进行改进

package test

import (
	"sync"
	"testing"
)

func TestGroutineThreadSafe(t *testing.T) {
	var wg sync.WaitGroup
	var mut sync.Mutex
	var counter int = 0
	for i := 0; i < 5000; i++ {
		wg.Add(1) //每次开启一个协程,wg里面协程数量就+1
		go func() {
			defer func() {
				mut.Unlock()
			}()
			mut.Lock()
			counter++
			wg.Done() //每完成一个协程,wg里面的协程数量就-1
		}()
	}
	// time.Sleep(1 * time.Second)
	wg.Wait() //当wg里面所有的协程都执行完了,才会继续往下执行,这样我们
			  //就不需要去自己估摸等待的时间了
	t.Log(counter)
}

三.CSP并发机制

上一小节讲解的是传统的处理并发问题的方法,而CSP并发机制则是go的一种独特的并发机制。
首先先了解一下Actor模型和CSP模型的区别,接下来我们主要介绍一下CSP机制的channel.

这种channel是最典型的一直channel机制,这种机制的的特点是:

通信双方想要完成通信必须两边都在,如果一方在另外一方不在,那么在的一方就会一直处于阻塞的状态来等待
另外一方的出现。


而上面这种channel机制我们称之为buffer channel

并不要求双方都在,而是当发送方或者接收方有需要了就去channel去放消息或者去消息。

下面我们看一下一个普通的没有使用csp的例子:

package test

import (
	"fmt"
	"testing"
	"time"
)

func service() string {
	time.Sleep(time.Millisecond * 50) //这个服务需要持续50毫秒
	return "done"
}

func otherTask() {
	fmt.Println("Working on other Task")
	time.Sleep(time.Millisecond * 100) //这份任务需要执行100毫秒
	fmt.Println("Task is done")
}

func TestService(t *testing.T) {
	fmt.Println(service())
	otherTask()
}
//可以看到这里一共消耗了0.15s,说明上述任务是串行执行的,这样会导致浪费很多的时间


下面我们对Service进行包装来使用csp机制:

package test

import (
	"fmt"
	"testing"
	"time"
)

func service() string {
	time.Sleep(time.Millisecond * 50) //这个服务需要持续50毫秒
	return "done"
}

func otherTask() {
	fmt.Println("Working on other Task")
	time.Sleep(time.Millisecond * 100) //这份任务需要执行100毫秒
	fmt.Println("Task is done")
}

func AsyncService() chan string {
	retCh := make(chan string) //这个channel只支持string类型
	//retCh := make(chan string,1),修改为这一行就会得到buffer channel
	go func() {
		res := service()
		fmt.Println("returned service")
		retCh <- res //将res放入通道
		fmt.Println("service exited")
	}()
	return retCh
}
func TestAsyncService(t *testing.T) {
	retCh := AsyncService()
	otherTask()
	fmt.Println(<-retCh) //从channel取消息
	time.Sleep(1 * time.Second) //防止Test结束后退出导致service exited没有打印出来
}


不过细心就会发现一个问题,我们这里使用的是经典的普通channel,也就是当我们的channel的消息没有被取走的时候,那么就会阻塞在retCh <- res //将res放入通道这里,那么我们可以改为buffer channel(参加注释),

从上面我们可以看出,结果就不是必须done被打印出之后,service exited.

四.多路选择和超时控制机制

多路选择形式:

//下面只要有一个channel获取到了消息就会执行对应的语句,当有多个语句都获得了消息的,也并不会根据
//他们的顺序去执行,这个是没有保证的
select{
	case ret:= <-retCh1:
		//执行语句1
	case ret:= <-retCh2:
		//执行语句2
	...
	default:
		//执行语句
}

我们可以利用多路选择来实现超时控制:

package test

import (
	"fmt"
	"testing"
	"time"
)

func service() string {
	time.Sleep(time.Millisecond * 50) //这个服务需要持续50毫秒
	//time.Sleep(time.Millisecond * 200) //对应第二张结果
	return "done"
}

func AsyncService() chan string {
	retCh := make(chan string, 1) //这个channel只支持string类型
	go func() {
		res := service()
		fmt.Println("returned service")
		retCh <- res //将res放入通道
		fmt.Println("service exited")
	}()
	return retCh
}
func TestAsyncService(t *testing.T) {
	select {
	case ret := <-AsyncService():
		t.Logf("%s ", ret)
	case <-time.After(100 * time.Millisecond):
		t.Error("time out")
	}
}

service 50ms未超时

service 200ms超时

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值