📜 ⬆️ ⬇️

Death goroutine under control

Introduction


Recently I came across a small useful package and decided to share the find. To do this, publish a translation of an article discussing the problem of correctly terminating goroutine from the outside and offering a solution as that very small tomb package.

Article translation


Definitely one of the reasons why people are attracted to the Go language is a first-class approach to concurrency. Opportunities such as communication through channels, lightweight processes (goroutines) and their proper planning are not only native to the language, but also integrated into it with taste.

If you listen to conversations in the community for several days, then there is a great chance that you will hear someone proudly note the principle:
')
Do not communicate using shared memory, share memory using communication.

There is a blog entry on this topic, as well as an interactive exercise (code walk).

This model is very practical, and when developing algorithms you can get a significant gain if you approach the problem from this side, but this is not new.

In my note, I want to refer to the currently open Go issue related to this approach: the completion of background activity.

As an example, let's create a specially simplified goroutine that sends strings through the pipe:

 type LineReader struct { Ch chan string r *bufio.Reader } func NewLineReader(r io.Reader) *LineReader { lr := &LineReader{ Ch: make(chan string), r: bufio.NewReader(r), } go lr.loop() return lr } 

The LineReader structure has a Ch channel through which the client can receive lines, as well as an internal buffer r (not accessible from the outside) used to efficiently read these lines. The NewLineReader function creates an initialized LineReader, starts a reading cycle, and returns the created structure.

Now let's look at the cycle itself:

 func (lr * LineReader) loop () {
         for {
                 line, err: = lr.ReadSlice ('\ n')
                 if err! = nil {
                         close (lr.Ch)
                         return
                 }
                 lr.Ch <- string (line)
         }
 }

In the loop, we get a string from the buffer, in case of an error, close the channel and stop, otherwise we pass the string to the other side, possibly blocking while it is doing its own business. This is all clear and familiar to the Go-developer.

But there are two details related to the completion of this logic: firstly, information about the error is lost, and secondly, there is no clean way to interrupt the procedure from the outside. Of course, an error can be easily logged, but what if we want to store it in a database, or send it by wire, or even process it, taking into account its nature? The possibility of a clean stop is also valuable in many cases, for example, for launching from under a test-runner.

I am not saying that this is something that is difficult to do in any way. I want to say that today there is no generally accepted approach to handle these moments in a simple and consistent way. Or maybe not. The tomb package for Go is my experiment in trying to solve a problem.

The job model is simple: Tomb tracks whether the goroutine is alive, dying or dead, as well as the cause of death.

To understand this model, let's see how this concept is applied to the LineReader example. As a first step, you need to change the creation process to add support for Tomb:

type LineReader struct {
Ch chan string
r *bufio.Reader
t tomb.Tomb
}
 
func NewLineReader(r io.Reader) *LineReader {
lr := &LineReader{
Ch: make(chan string),
r: bufio.NewReader(r),
}
go lr.loop()
return lr
}

Looks very similar. Only a new field in the structure, even the creation function has not changed.

Next, change the loop function to support error tracking and interruption:

func (lr *LineReader) loop() {
defer lr.t.Done()
for {
line, err := lr.r.ReadSlice('n')
if err != nil {
close(lr.Ch)
lr.t.Kill(err)
return
}
select {
case lr.Ch <- string(line):
case <-lr.t.Dying():
close(lr.Ch)
return
}
}
}

Note some interesting points: first, just before the loop function completes, Done is called to track the goroutine completion. Then, a previously unused error is now passed to the Kill method, which marks goroutine as dying. Finally, the link to the channel has been modified so that it is not blocked if goroutine dies for any reason.

Tomb has Dying and Dead channels returned by methods of the same name that close when Tomb changes its state accordingly. These channels allow you to organize an explicit lock until the state changes, and also to selectively unblock the select statement in such cases as shown above.

Having such a modified cycle as described above, it is easy to implement the Stop method to request a clean synchronous termination of goroutine from the outside:

func (lr *LineReader) Stop() error {
lr.t.Kill(nil)
return lr.t.Wait()
}

In this case, the Kill method will put the tomb into a dying state from the outside executing goroutine, and the Wait method will block until the goroutine has completed and reported it through the Done method, as shown above. This procedure behaves correctly even if goroutine was already dead or in a dying state due to internal errors, because only the first call to the Kill method with a real error is remembered as the cause of death for goroutine. The nil value passed to t.Kill is used as the cause of a clean termination without an actual error and causes Wait to return nil when goroutine completes, indicating a clean stop based on Go idioms.

That's all that can be said on the topic. When I started developing on Go 1, I wondered if more language support was needed to come up with a good deal for these kinds of problems, such as some kind of tracking the state of the goroutine itself, similar to what Erlang does with its lightweight processes, but it turned out that this is more a matter of organizing the workflow using existing building blocks.

The tomb package and its Tomb type are the actual presentation of a good arrangement for completing a goroutine, with familiar method names, inspired by existing idioms. If you want to use this, you can get the package with the command:

  $ go get launchpad.net/tomb

Detailed API documentation is available at:

gopkgdoc.appspot.com/pkg/launchpad.net/tomb

Good luck!

Source: https://habr.com/ru/post/141536/


All Articles