当前位置:网站首页>Goroutine Leaks - The Forgotten Sender
Goroutine Leaks - The Forgotten Sender
2022-08-01 20:34:00 【Cloud full of notes】
这里填写标题
1. Goroutine Leaks - The Forgotten Sender
1.1. Introduction
Concurrent programming allows developers to solve problems using more than one path of execution and is often used in an attempt to improve performance. Concurrency doesn’t mean these multiple paths are executing in parallel; it means these paths are executing out-of-order instead of sequentially. Historically, this type of programming is facilitated using libraries that are either provided by a standard library or from 3rd party developers.
In Go, concurrency features like Goroutines and channels are built into the language and runtime to reduce or eliminate the need for libraries. This has created the illusion that writing concurrent programs in Go is easy. You must be cautious when deciding to use concurrency as it comes with some unique side effects or traps if not used correctly. These traps can create complexity and nasty bugs if you’re not careful.
The traps I will discuss in this post are related to Goroutine leaks.
1.2. Leaking Goroutines
When it comes to memory management, Go deals with many of the details for you. The Go compiler decides where values are located in memory using escape analysis. The runtime tracks and manages heap allocations through the use of the garbage collector. Though it’s not impossible to create memory leaks in your applications, the chances are greatly reduced.
A common type of memory leak is leaking Goroutines. If you start a Goroutine that you expect to eventually terminate but it never does then it has leaked. It lives for the lifetime of the application and any memory allocated for the Goroutine can’t be released. This is part of the reasoning behind the advice “Never start a goroutine without knowing how it will stop”.
To illustrate a basic Goroutine leak, look at the following code:
Listing 1
https://play.golang.org/p/dsu3PARM24K
31 // leak is a buggy function. It launches a goroutine that
32 // blocks receiving from a channel. Nothing will ever be
33 // sent on that channel and the channel is never closed so
34 // that goroutine will be blocked forever.
35 func leak() {
36 ch := make(chan int)
37
38 go func() {
39 val := <-ch
40 fmt.Println("We received a value:", val)
41 }()
42 }
Listing 1 defines a function named leak. The function creates a channel on line 36 that allows Goroutines to pass integer data. Then on line 38 Goroutine is created which blocks on line 39 waiting to receive a value from the channel. While that Goroutine is waiting, the leak function returns. At this point, no other part of the program can send a signal over the channel. This leaves the Goroutine blocked on line 39 waiting indefinitely. The fmt.Println call on line 40 will never happen.
In this example, the Goroutine leak could be quickly identified during a code review. Unfortunately, Goroutine leaks in production code are usually more difficult to find. I can’t show every possible way a Goroutine leak can happen, but this post will detail one kind of Goroutine leak that you may encounter:
1.3. Leak: The Forgotten Sender
For this leak example you will see a Goroutine that is blocked indefinitely, waiting to send a value on a channel.
The program we will look at finds a record based on some search term which is then printed. The program is built around a function called search:
Listing 2
https://play.golang.org/p/o6_eMjxMVFv
29 // search simulates a function that finds a record based
30 // on a search term. It takes 200ms to perform this work.
31 func search(term string) (string, error) {
32 time.Sleep(200 * time.Millisecond)
33 return "some value", nil
34 }
The search function on line 31 in Listing 2 is a mock implementation to simulate a long running operation like a database query or a web call. In this example, it is hard-coded to take 200ms.
The program calls the search function as shown in Listing 3.
Listing 3
https://play.golang.org/p/o6_eMjxMVFv
17 // process is the work for the program. It finds a record
18 // then prints it.
19 func process(term string) error {
20 record, err := search(term)
21 if err != nil {
22 return err
23 }
24
25 fmt.Println("Received:", record)
26 return nil
27 }
In Listing 3 on line 19, a function called process is defined which takes a single string argument representing a search term. On line 20, the term variable is then passed to the search function which returns a record and an error. If an error occurs, the error is returned to the caller on line 22. If there was no error, the record is printed on line 25.
For some applications the latency incurred when calling search sequentially may be unacceptable. Assuming the search function can’t be made to run any faster, the process function can be changed to not consume the total latency cost incurred by search.
To do this, a Goroutine can be used as shown in Listing 4 below. Unfortunately this first attempt is buggy as it creates a potential Goroutine leak.
Listing 4
https://play.golang.org/p/m0DHuchgX0A
38 // result wraps the return values from search. It allows us
39 // to pass both values across a single channel.
40 type result struct {
41 record string
42 err error
43 }
44
45 // process is the work for the program. It finds a record
46 // then prints it. It fails if it takes more than 100ms.
47 func process(term string) error {
48
49 // Create a context that will be canceled in 100ms.
50 ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
51 defer cancel()
52
53 // Make a channel for the goroutine to report its result.
54 ch := make(chan result)
55
56 // Launch a goroutine to find the record. Create a result
57 // from the returned values to send through the channel.
58 go func() {
59 record, err := search(term)
60 ch <- result{
record, err}
61 }()
62
63 // Block waiting to either receive from the goroutine's
64 // channel or for the context to be canceled.
65 select {
66 case <-ctx.Done():
67 return errors.New("search canceled")
68 case result := <-ch:
69 if result.err != nil {
70 return result.err
71 }
72 fmt.Println("Received:", result.record)
73 return nil
74 }
75 }
In listing 4 on line 50, the process function is rewritten to create a Context that will be canceled in 100ms. For more information on how to use Context read the golang.org blog post.
Then on line 54, the program creates an unbuffered channel that allows Goroutines to pass data of the result type. On lines 58 to 61 an anonymous function is defined and then called as a Goroutine. This Goroutine calls search and attempts to send its return values through the channel on line 60.
While the Goroutine is doing its work, the process function executes the select block on line 65. This block has two cases which are both channel receive operations.
On line 66 there is a case which receives from the ctx.Done() channel. This case will be executed if the Context gets canceled (the 100ms duration passes). If this case is executed, then process will return an error indicating that it gave up waiting for search on line 67.
Alternatively, the case on line 68 receives from the ch channel and assigns the value to a variable named result. As before in the sequential implementation, the program checks and handles the error on lines 69 and 70. If there was no error, the program prints the record on line 72 and returns nil to indicate success.
This refactoring sets a maximum duration the process function will wait on search to complete. However this implementation also creates a potential Goroutine leak. Think about what the Goroutine in this code is doing; on line 60 it sends on the channel. Sending on this channel blocks execution until another Goroutine is ready to receive the value. In the timeout case, the receiver stops waiting to receive from the Goroutine and moves on. This will cause the Goroutine to block forever waiting for a receiver to appear which can never happen. This is when the Goroutine leaks.
1.4. Fix: Make Some Space
The simplest way to resolve this leak is to change the channel from an unbuffered channel to a buffered channel with a capacity of 1.
Listing 5
https://play.golang.org/p/u3xtQ48G3qK
53 // Make a channel for the goroutine to report its result.
54 // Give it capacity so sending doesn't block.
55 ch := make(chan result, 1)
Now in the timeout case, after the receiver has moved on, the search Goroutine will complete its send by placing the result value in the channel then it will return. The memory for that Goroutine will eventually be reclaimed as well as the memory for the channel. Everything will naturally work itself out.
In The Behavior of Channels William Kennedy gives several great examples of channel behavior and provides philosophy regarding their use. The final example of that article “Listing 10” shows a program similar to this timeout example. Read that article for more advice on when to use buffered channels and what level of capacity is appropriate.
1.5. Conclusion
Go makes it simple to start Goroutines but it is our responsibility to use them wisely. In this post, I have shown one example of how Goroutines can be used incorrectly. There are many more ways to create Goroutine leaks as well as other traps you may encounter when using concurrency. In future posts, I will provide more examples of Goroutine leaks and other concurrency traps. For now I will leave you with this advice; any time you start a Goroutine you must ask yourself:
- When will it terminate?
- What could prevent it from terminating?
Concurrency is a useful tool, but it must be used with caution.
边栏推荐
猜你喜欢

】 【 nn. The Parameter () to generate and why do you want to initialize
![[Multi-task optimization] DWA, DTP, Gradnorm (CVPR 2019, ECCV 2018, ICML 2018)](/img/a1/ec038eeb6c98c871eb31d92569533d.png)
[Multi-task optimization] DWA, DTP, Gradnorm (CVPR 2019, ECCV 2018, ICML 2018)

Wildcard SSL/TLS certificate

算法---解码方法(Kotlin)

MySQL语法基础

SIPp installation and use

进行交互或动画时如何选择Visibility, Display, and Opacity

Which websites are commonly used for patent searches?

数字孪生北京故宫,元宇宙推进旅游业进程

LTE time domain and frequency domain resources
随机推荐
WeChat applet cloud development | personal blog applet
宝塔搭建PESCMS-Ticket开源客服工单系统源码实测
WhatsApp group sending actual combat sharing - WhatsApp Business API account
98.嵌入式控制器EC实战 EC开发板开发完成
useful website
Where should I prepare for the PMP exam in September?
[Multi-task learning] Modeling Task Relationships in Multi-task Learning with Multi-gate Mixture-of-Experts KDD18
【Dart】dart之mixin探究
Batch get protein .pdb files based on Uniprot ID/PDB ID
密码学的基础:X.690和对应的BER CER DER编码
Different operating with different locks, rounding
卷积神经网络(CNN)mnist数字识别-Tensorflow
【无标题】
【个人作品】记之-串口日志记录工具
Common pits in the Go language
不同的操作加不同的锁详解
实用新型专利和发明专利的区别?秒懂!
Determine a binary tree given inorder traversal and another traversal method
ARTS_202207W2
通配符 SSL/TLS 证书