[go语言基础]经典案例:使用并发计算

文章介绍了Go语言中的协程goroutine概念,它是轻量级的并发单位,比线程更高效。文章详细阐述了如何启动协程,以及在并发执行中可能出现的资源竞争问题。为解决这个问题,文章提到了两种策略:使用锁和临界区,以及利用管道进行同步。此外,还提供了计算素数的并发示例,展示了如何运用管道和协程来提高计算效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

(A)一些基础知识的使用

go语言中协程goroutine是一个全新的概念,是一个比线程更小的并发单位,线程可以分为多个协程,可以理解为一个线程中的多个函数进行来回切换.

相比于其他语言切换线程导致的cpu和线程上下文占用保存带来的消耗,go语言可以借助自身的协程机制实现轻松的并发

go语言开启协程的机制分简单,一个单独的go标签即可

	go runtime()



    func runtime() {
	for i := 1; i <= 10; i++ {
		fmt.Println("协程A", i)
		time.Sleep(100)
	}
	wait.Done()
}

注意一个问题,如果协程没结束但是主线程结束了,那么协程都会打出gg
所以一般要使用waitgroup库中的函数或者使用主线程休眠,让主线程可以维持到所有的协程都完成工作.

使用go语言执行百万级别的并发也很简单,直接循环加上go就可以了,这个的来源也是关于协程

func millionSync() {
	for i := 1; i <= 1000000; i++ {
		go fmt.Println("输出点什么")
	}
}

但是正如其他语言多线程产生的影响一样,go语言中也会产生资源竞争问题

// 但是线程并发会带来一些安全问题,最显著的就是资源竞争
// 举个例子
func TestSourceCompete() {
	wait = sync.WaitGroup{}
	wait.Add(2)
	a := 0
	go func(a *int) {
		a1 := *a
		time.Sleep(500) //模拟获取数据的时间
		a1++
		time.Sleep(500) //模拟计算的时间
		*a = a1
		time.Sleep(500) //模拟赋值的时间

		fmt.Println("子线程2", *a)
		wait.Done()
	}(&a)
	go func(a *int) {
		a1 := *a
		time.Sleep(500) //模拟获取数据的时间
		a1++
		time.Sleep(500) //模拟计算的时间
		*a = a1
		time.Sleep(500) //模拟赋值的时间
		fmt.Println("子线程1", *a)
		wait.Done()
	}(&a)
	fmt.Println("主线程", a)
	wait.Wait()
}

//上面这里例子里,我们原本的思路应该是两个协程分别把数据假+1然后变成2
//但是这里因为可以放缓了读取数据的情况,所以发生了数据不安全的结果
//导致两个进程的运算结果都一样

对于这种资源竞争,我们有两种处理思路

(1)锁/临界区

使用锁和临界区的机制,

处理方法就是加上锁,也就是我们解决资源冲突的第一种方法,互斥锁的作用是保护临界区(Critical Section),即一段代码或一段逻辑,在同一时间只能由一个协程执行。

也就是说,可以理解为一旦有一个协程进入了锁区域,则其他协程就会自然阻塞

(2)使用管道

管道的使用稍微有点复杂,不过问题也不大

1.管道的生成方式有两种

make(chan int)  无缓冲
make(chan int,1)  缓冲管道

无缓冲轨道的特点是:在试图写入的时候,必须在同时有某个位置进行接收,不然就会发生死锁,所以建议管道的两端使用,而不是在一个协程中使用,这是一个经典的死锁情况

ch是一个无缓冲

ch <- 1
fmt.println(<-ch)

有缓冲轨道的特点则是,内存存在一个长度有限的缓冲区,输入的数据是可以他暂存的,并且这个东西本质上是一个队列的数据结构,符合先进先出

2.管道的两个注意事项

并且对于缓冲轨道来说,如果太多数据被阻塞"拒之门外"就会发生数据丢失的情况
当缓冲区管道已满时,继续尝试发送数据会导致发送操作被阻塞,直到有空间可用。
在这段时间内,如果没有其他协程进行接收操作,那么后续的发送操作将无法执行,数据将会丢失。

管道中的数据也可以一次性读取两个数字 val和ok,ok代表这个是否成功读取

关于成功读取和阻塞之间的区别:
//管道如果在关闭状态,是不会发生阻塞的,立即执行,并且ok会成为false
//如果管道是在开启状态,则会在有些是否发生阻塞

3.使用select对于管道进行监听

select的特点有两个 ,首先是如果有多个case分支被选中,就会通过伪随机获取一个并且执行

第二个是case坚挺的是"语句能否立刻执行成功"而不是"能否正确读取数字"

比如说管道已经关闭的情况下,<-done仍然是可以被选中的,因为这个语句虽然无法读取数据,但是可以正常执行.但是当done放开但是管道为空的时候,是不能立刻读取的,这个时候就会被case忽略

(B)关于具体的计算方式

使用并发计算1-100内的素数,首先是最简单的思路,每个协程负责一部分

func CountSu1() {
	//利用多个协程计算素数,第一种思路,每个协程负责固定的一部分
	wait := sync.WaitGroup{}
	wait.Add(10)
	for i := 1; i <= 10; i++ {
		go func(i int) {
			for j := (i - 1) * 10; j <= i*10-1; j++ {
				if isPrime(j) {
					fmt.Print(j, " ")
				}
			}
			wait.Done()
		}(i)
	}
	wait.Wait()
}

第二种:使用管道的思路进行处理

大致思路就是:我们创建两个管道 A和B

A管道是由主管道往里传递数据的,从1到100,由于管道本身是线程安全的,多个协程可以放心的去竞争管道的数据资源

B管道是负责信息通知的,这个管道不传递任何数据,如果使用select监听,也无法选中.当主线程往管道中输入原始数据结束以后,B管道就会关闭,则各个协程的select语句就能读取到这个变化

//第二种思路,每个协程自由进行计算

func CountSu2() {
	//利用多个协程计算素数,每个携程直接取出数据
	wait := sync.WaitGroup{}
	wait.Add(10)
	channelOfPrime := make(chan int, 10)
	done := make(chan int, 10)
	defer close(channelOfPrime)
	//一共分成十个协程,具体的思路是应用到管道
	//另外注意这里为了让管道读取停下来
	for i := 1; i <= 10; i++ {
		go func() {
			go Worker(channelOfPrime, done)
			wait.Done()
		}()
	}
	//主线程把数字全都发送到通道之中
	for i := 2; i < 100; i++ {
		channelOfPrime <- i
		time.Sleep(100)
	}
	close(done)
	wait.Wait()
}

func Worker(ch <-chan int, done <-chan int) {
	for {
		select {
		case val, ok := <-ch:
			if ok {
				if isPrime(val) {
					fmt.Print(val, " ")
				}
			}
		case <-done:
			return
		}
		//这个的读取逻辑是这样的,管道在开启状态的时候,能否读取到数据不一定,但是读取操作一定可以进行
		//只有管道关闭以后,这个东西才能立刻被选中,否则开启状态会阻塞

		//首先select语句的判断是只要能立刻被执行不阻塞,这个就算是可以被选中!

		//其次是关于管道的内容
		//管道如果在关闭状态,是不会发生阻塞的,立即执行,并且ok会成为false
		//如果管道是在开启状态,则会在有些是否发生阻塞
		//并且对于缓冲轨道来说,如果太多数据被阻塞"拒之门外"就会发生数据丢失的情况
		//当缓冲区管道已满时,继续尝试发送数据会导致发送操作被阻塞,直到有空间可用。
		//在这段时间内,如果没有其他协程进行接收操作,那么后续的发送操作将无法执行,数据将会丢失。

	}
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值