Dynamic diagram, how to make goroutine quit halfway?

Just look at the title, you may not understand what I'm talking about.

We usually create a coroutine and run a section of logic. The code is about this long.

package main
 
import (
    "fmt"
    "time"
)
func Foo() {
    fmt.Println("Print 1")
    defer fmt.Println("Print 2")
    fmt.Println("Print 3")
}
 
func main() {
    go  Foo()
    fmt.Println("Print 4")
    time.Sleep(1000*time.Second)
}
 
// This code, normal operation will have the following results
 Print 4
 Print 1
 Print 3
 Print 2

Note that "print 2" above is in defer, so it will be printed before the end of the function. Therefore, it is placed in "print 3" after.

So today's problem is how to make the Foo() function end halfway. For example, run to print 2 and exit the coroutine. Output the following results

Print 4
 Print 1
 Print 2

Don't sell off. I'll tell you the answer directly.

Insert a runtime.Goexit() after "print 2", and the collaboration will end directly. And print 2 in defer can be executed before the end.

package main
 
import (
    "fmt"
    "runtime"
    "time"
)
func Foo() {
    fmt.Println("Print 1")
    defer fmt.Println("Print 2")
    runtime.Goexit() // Join the business
    fmt.Println("Print 3")
}
 
func main() {
    go  Foo()
    fmt.Println("Print 4")
    time.Sleep(1000*time.Second)
}
 
 
// Output results
 Print 4
 Print 1
 Print 2

It can be seen that the line of print 3 did not appear, and the cooperation process ended ahead of schedule.

In fact, the interview questions are finished here. Is this wave of self-question and self-answer OK?

But this is not the focus today. We need to figure out the internal logic.

What is runtime.Goexit()?

Look at the internal implementation.

func Goexit() {
    // The following functions omit some logic
    gp := getg() 
    for {
    // Get defer and execute
        d := gp._defer
        reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
    }
    goexit1()
}
 
func goexit1() {
    mcall(goexit0)
}

From the code point of view, runtime.Goexit() will first execute the methods in defer. This explains why print 2 in defer can be output normally in the beginning code.

The code then executes goexit1. The essence is the simple encapsulation of goexit0.

We can follow the code and see what goexit0 does.

// goexit continuation on g0.
func goexit0(gp *g) {
  // Get the current goroutine
    _g_ := getg()
    // Set the current goroutine status to_ Gdead
    casgstatus(gp, _Grunning, _Gdead)
  // The number of global processes minus one
    if isSystemGoroutine(gp, false) {
        atomic.Xadd(&sched.ngsys, -1)
    }
  
  // Omit various emptying logic
 
  // Take g off m.
  dropg()
 
 
    // Put this g back into the local process queue of p, but not the global process queue.
    gfput(_g_.m.p.ptr(), gp)
 
  // Reschedule, take a runnable collaboration and run it
    schedule()
}
 

This code has a high information density.

Many nouns may make people look confused.

In a brief description, there is a GMP model in Go language. M is the kernel thread. G is the coroutine we usually use. P will act as a tool between G and m and be responsible for scheduling g to M.

GMP diagram

Since it is scheduling, that is, not every G can always be in the running state. When G cannot run, save it, and then schedule the next g that can run to run.

For G and P that cannot run temporarily, there will be a local queue to store these G, and if P's local queue cannot be saved, there will be a global queue, which does similar things.

After understanding this background, go back to the goexit0 method. What you do is set the current collaboration G to_ Gdead state, then remove it from M and try to put it back into the local queue of P. Then reschedule a wave, get another G that can run, take it out and run.

goexit

So to sum up, as long as you execute the goexit function, the current coroutine will exit and the next executable coroutine can be scheduled to run.

After seeing this, you should be able to understand why runtime.Goexit() can end the collaboration in only half of the execution in the first code.

Purpose of goexit

I understand it, but I can't help wondering. If you ask in an interview like this, it only means that you have met an interviewer who likes to embarrass young people, but who is serious will end up halfway? So what is the real purpose of goexit?

There is a small detail. I don't know if you pay attention to it when you debug.

To illustrate the problem, here is a piece of code.

package main
 
import (
    "fmt"
    "time"
)
func Foo() {
    fmt.Println("Print 1")
}
 
func main() {
    go  Foo()
    fmt.Println("Print 3")
    time.Sleep(1000*time.Second)
}

This is a very simple piece of code. It doesn't matter what you output. Through the go keyword, a goroutine is started to execute Foo(), which ends after printing. The main collaboration process sleep takes a long time, only waiting for death.

Here, in our newly started coroutine, we can arbitrarily set a breakpoint in the Foo() function. Then debug it.

You will find that the bottom of the stack of this coroutine starts from runtime.goexit().

If you usually pay attention, you will find that in fact, all the bottom of the stack starts with this function. Let's continue with the code.

What is goexit?

Click in the above debug stack and you will find that this is an assembly function. You can see that it calls the goexit1() function in the runtime package.

// The top-most function running on a goroutine
// returns to goexit+PCQuantum.
TEXT runtime·goexit(SB),NOSPLIT,$0-0
    BYTE    $0x90    // NOP
    CALL    runtime·goexit1(SB)    // does not return
    // traceback from goexit1 must hit code range of goexit
    BYTE    $0x90    // NOP

So I followed the code in pruntime/proc.go.

// Omit some codes
func goexit1() {
    mcall(goexit0)
}

Isn't it familiar? Isn't this the goexit0 internally executed in runtime.Goexit() we talked about at the beginning.

Why is this method at the bottom of each stack?

The first thing we need to know is that the execution process of the function stack is first in and last out.

Suppose we have the following code

func main() {
    B()
}
 
func B() {
    A()
}
 
func A() {
 
}

The above code is that main runs the B function, and the B function then runs the A function. The code execution is like the following dynamic diagram.

Function stack execution order

This is a first in and last out process, that is, the function stack we often call. After executing the child function A(), it will return to the parent function B(). After executing B(), it will finally return to main(). Here, the bottom of the stack is main(). If goexit is inserted at the bottom of the stack, you can run to goexit when the program execution ends.

Combined with the contents mentioned above, we can know that the goexit at the bottom of the stack will be executed after the business code in the collaboration runs, so as to exit the collaboration and schedule the next executable G to run.

Then the question comes again. Who did and when did you insert goexit at the bottom of the stack?

To answer directly, there is a newproc1 method in runtime/proc.go, which will be used whenever creating a collaboration. There's a place that says that.

func newproc1(fn *funcval, argp unsafe.Pointer, narg int32, callergp *g, callerpc uintptr) {
    // Get current g
  _g_ := getg()
    // Gets the p where the current g is located
    _p_ := _g_.m.p.ptr()
  // Create a new goroutine
    newg := gfget(_p_)
 
    // Insert goexit at the bottom
    newg.sched.pc = funcPC(goexit) + sys.PCQuantum 
    newg.sched.g = guintptr(unsafe.Pointer(newg))
    // Put the newly created g into p
    runqput(_p_, newg, true)
 
    // ...
}
 

The main logic is to get the scheduler P where the current coroutine G is located, then create a new G, and insert a goexit at the bottom of the stack.

So every time we debug, we can see a goexit function at the bottom of the function stack.

The main function is also a coroutine, and the bottom of the stack is also goexit?

Whether there is a goexit at the bottom of the main function stack, let's take a look at the following code breakpoints. Direct results.

The main function is also goexit() at the bottom of the stack.

From asm_amd64.s can see the process started by the Go program. The runtime · mainPC mentioned here is actually runtime.main

    // create a new goroutine to start program
    MOVQ    $runtime·mainPC(SB), AX        // That is, runtime.main
    PUSHQ    AX
    PUSHQ    $0            // arg size
    CALL    runtime·newproc(SB)

Create the runtime.main coroutine through runtime · newproc, and then start the main.main function in runtime.main. This is the main function we usually write.

// runtime/proc.go
func main() {
    // Omit a lot of code
    fn := main_main // In fact, it is our main function entry
    fn() 
}
 
//go:linkname main_main main.main
func main_main()

The conclusion is that the main function is also created by newproc. As long as the goroutine created by newproc, there will be a goexit at the bottom of the stack.

What is the difference between os.Exit() and runtime.Goexit()

Finally, go back to the beginning and realize the echo from beginning to end.

Can I use os.Exit() instead of runtime.Goexit()?

Both have the meaning of "exit", and the objects they exit are different. os.Exit() refers to the exit of the whole process; runtime.Goexit() refers to the co process exit.

It is conceivable that if os.Exit() is used instead, the content in defer will not be executed.

package main
 
import (
    "fmt"
    "os"
    "time"
)
func Foo() {
    fmt.Println("Print 1")
    defer fmt.Println("Print 2")
    os.Exit(0)
    fmt.Println("Print 3")
}
 
func main() {
    go  Foo()
    fmt.Println("Print 4")
    time.Sleep(1000*time.Second)
}
 
// Output results
 Print 4
 Print 1
 

summary

• through runtime.Goexit(), you can end the collaboration in advance and execute the content of defer before the end
• runtime.Goexit() actually encapsulates goexit0. As long as goexit0 is executed, the current coroutine will exit and the next executable coroutine can be scheduled to run.
• a new goroutine can be created through newproc, which will insert a goexit at the bottom of the function stack.
• os.Exit() refers to the exit of the whole process; runtime.Goexit() refers to the co process exit. The two have different meanings.

last

Useless knowledge has increased again.

In general, who can execute this function in business development?

But if you don't care during development, it doesn't mean that the interviewer doesn't care!

The next time the interviewer asks you, * * what if you want to quit the process halfway through goroutine** You know what to say?

Well, brothers, have you found that the writing of this article is short and watery, really because I'm lazy?

no

Of course not!

I'm thinking about the health of my brothers. It's bad for my health to keep squatting for too long, okay?

If the article is helpful to you, welcome

forget it.

Let's choke water in the ocean of knowledge

I'm Xiaobai. I'll see you next time!

Attention to the official account: debug

Article recommendation:

Programmer's Guide to sudden death preventionTCP sticky packet: I just made the mistake that every packet makes | hard core diagramDynamic diagram! Since the IP layer is fragmented, why should the TCP layer be segmented?

reference material

Rao Da's "where's goexit?"- https://qcrao.com/2021/06/07/where-is-goexit-from/

Tags: Java Go network Back-end

Posted on Mon, 22 Nov 2021 03:38:52 -0500 by lpxxfaintxx