Home > Article > Backend Development > Detailed explanation of the use of goroutine in go language
goroutine in go is a feature of go language that supports concurrency at the language level. When I first came into contact with Go, I was extremely happy with Go's goroutine. It's so easy to implement concurrency that it's almost ridiculous.
But during the project process, I increasingly discovered that goroutine is a thing that can easily be abused by everyone. Goroutine is a double-edged sword. Here are a few sins of using goroutine:
1. Pointer passing in goroutine is unsafe
fun main() { request := request.NewRequest() //这里的NewRequest()是传递回一个type Request的指针 go saveRequestToRedis1(request) go saveReuqestToRedis2(request) select{} }
Very logical code :
The main routine opens a routine to pass the request to saveRequestToRedis1, so that it can store the request in redis node 1
At the same time, open another routine to pass the request to saveReuqestToRedis2, so that it can store the request. Go to redis node 2
Then the main routine enters the loop (without ending the process)
Now comes the problem. The two functions saveRequestToRedis1 and saveReuqestToRedis2 were not actually written by me, but by another member of the team. Written by someone, I know nothing about its implementation, and I don’t want to take a closer look at the specific internal implementation. But according to the function name, I took it for granted and passed the request pointer in.
In fact, saveRequestToRedis1 and saveRequestToRedis2 are implemented like this:
func saveRequestToRedis1(request *Request){ … request.ToUsers = []int{1,2,3} //这里是一个赋值操作,修改了request指向的数据结构 … redis.Save(request) return }
What's the problem with this? The two goroutines saveRequestToRedis1 and saveReuqestToRedis2 modify the same shared data structure, but because the execution of the routine is unordered, we cannot guarantee that the request.ToUsers setting and redis.Save() are an atomic operation, so that the actual storage of redis will occur. Data error bug.
Well, you can say that there is a problem with the implementation of this saveRequestToRedis function, and you did not consider that it would be called using go routine. Please think about it again. There is no problem with the specific implementation of saveRequestToRedis. It should not consider how the upper layer uses it.
That's because there is a problem with the use of my goroutine. When the main routine opens a routine, it does not confirm whether any code in the routine has modified the data in the main routine. Yes, the main routine does need to consider this situation.
When the main goroutine enables the go routine, it needs to read each line of code in the sub-routine to determine whether the shared data has been modified? ? How much this slows down the development speed in the actual project development process!
Go language uses goroutine to reduce the pressure of concurrent development, but it never thought that it would increase development pressure on the other hand.
So much is said above, just to draw a conclusion:
gorotine pointer passing is unsafe! !
If the previous example is not subtle enough, here is another example:
fun (this *Request)SaveRedis() { redis1 := redis.NewRedisAddr("xxxxxx") redis2 := redis.NewRedisAddr("xxxxxx") go this.saveRequestToRedis(redis1) go this.saveRequestToRedis(redis2) select{} }
Few people will consider whether there is a problem with the object pointed to by this pointer. The this pointer here is passed to routine It should be said that it is very hidden.
2. Goroutine increases the risk factor of the function
This point is actually derived from the above point. As mentioned above, it is unsafe to pass a pointer to a Go function. So think about it from another angle, how can you guarantee that the function you want to call will not use go inside the function implementation? If you don't look at the specific implementation inside the function body, there is no way to determine it.
For example, if we slightly change the above typical example
func main() { request := request.NewRequest() saveRequestToRedis1(request) saveRequestToRedis2(request) select{} }
Now that we are not using concurrency, this problem will definitely not occur, right? I chased into the function and was dumbfounded:
func saveReqeustToRedis1(request *Request) { … go func() { … request.ToUsers = []{1,2,3} …. redis.Save(request) } }
A goroutine started inside and modified the object pointed to by the request pointer. An error occurred here. Well, if you don't look at the specific implementation inside the function when calling the function, this problem cannot be avoided.
So, from the worst perspective, every function call is theoretically unsafe! Just imagine, if this calling function was not written by someone from my own development team, but used third-party open source code on the Internet... I really can't imagine how much time it would take to find this bug.
3. Goroutine abuse trap
Look at this example:
func main() { go saveRequestToRedises(request) } func saveRequestToRedieses(request *Request) { for _, redis := range Redises { go redis.saveRequestToRedis(request) } } func saveRequestToRedis(request *Request) { …. go func() { request.ToUsers = []{1,2,3} … redis.Save(request) } }
It’s amazing, go is everywhere, It seemed to appear out of nowhere in the blink of an eye. This is the abuse of go. We see go everywhere, but it is not very clear. Where should we use go? Why use go? Will goroutine really improve efficiency?
Concurrency in C language is much more complex and cumbersome than concurrency in Go language, so we will think deeply before using it and consider the benefits and disadvantages of using concurrency.
Processing methods
Here are some of my methods for dealing with these problems:
1. When starting When using a goroutine, if a function must pass a pointer, but the function level is very deep and safety cannot be guaranteed, pass the pointer to a clone of the object instead of passing the pointer directly
fun main() { request := request.NewRequest() go saveRequestToRedis1(request.Clone()) go saveReuqestToRedis2(request.Clone()) select{} }
Clone function needs to be written separately. You can simply follow this method after the structure is defined. for example:
func (this *Request)Clone(){ newRequest := NewRequst() newRequest.ToUsers = make([]int, len(this.ToUsers)) copy(newRequest.ToUsers, this.ToUsers) }
其实从效率角度考虑这样确实会产生不必要的Clone的操作,耗费一定内存和CPU。但是在我看来,首先,为了安全性,这个尝试是值得的。
其次,如果项目对效率确实有很高的要求,那么你不妨在开发阶段遵照这个原则使用clone,然后在项目优化阶段,作为一种优化手段,将不必要的Clone操作去掉。这样就能在保证安全的前提下做到最好的优化。
2、什么时候使用go的问题
有两种思维逻辑会想到使用goroutine:
1 业务逻辑需要并发
比如一个服务器,接收请求,阻塞式的方法是一个请求处理完成后,才开始第二个请求的处理。其实在设计的时候我们一定不会这么做,我们会在一开始就已经想到使用并发来处理这个场景,每个请求启动一个goroutine为它服务,这样就达到了并行的效果。这种goroutine直接按照思维的逻辑来使用goroutine
2 性能优化需要并发
一个场景是这样:需要给一批用户发送消息,正常逻辑会使用
for _, user := range users { sendMessage(user) }
但是在考虑到性能问题的时候,我们就不会这样做,如果users的个数很大,比如有1000万个用户?我们就没必要将1000万个用户放在一个routine中运行处理,考虑将1000万用户分成1000份,每份开一个goroutine,一个goroutine分发1万个用户,这样在效率上会提升很多。这种是性能优化上对goroutine的需求
按照项目开发的流程角度来看。在项目开发阶段,第一种思路的代码实现会直接影响到后续的开发实现,因此在项目开发阶段应该马上实现。
但是第二种,项目中是由很多小角落是可以使用goroutine进行优化的,但是如果在开发阶段对每个优化策略都考虑到,那一定会直接打乱你的开发思路,会让你的开发周期延长,而且很容易埋下潜在的不安全代码。
因此第二种情况在开发阶段绝不应该直接使用goroutine,而该在项目优化阶段以优化的思路对项目进行重构。
推荐:golang开发栏目
The above is the detailed content of Detailed explanation of the use of goroutine in go language. For more information, please follow other related articles on the PHP Chinese website!