Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

runtime: let idle OS threads exit #14592

Open
brianmario opened this issue Mar 2, 2016 · 27 comments
Open

runtime: let idle OS threads exit #14592

brianmario opened this issue Mar 2, 2016 · 27 comments
Assignees
Labels
NeedsDecision Feedback is required from experts, contributors, and/or the community before a change can be made.
Milestone

Comments

@brianmario
Copy link

  1. What version of Go are you using (go version)?
    1.5.3, 1.6
  2. What operating system and processor architecture are you using (go env)?
    x86_64 - OSX and Linux
  3. What did you do?
    Any golang program will create a new OS thread when it needs to if things are blocked. But these threads aren't ever destroyed. For example, a program using 7 goroutines might have 40+ OS threads hanging around. The numbers will surely get much higher as traffic fluctuates against a golang server process throughout the day.
  4. What did you expect to see?
    Once an OS thread has been idle long enough, I would have expected it to be destroyed. Being recreated if needed. Expanding and contracting like the relationship between the heap and GC.
  5. What did you see instead?
    The many OS threads that were created hang around even with an idle program and very few goroutines.

After doing some reading into other (closed) issues on this repo dating back to 2012 - including the one where SetMaxThreads was introduced - I'm curious, why keep the OS threads around instead of cleaning them up?

@ianlancetaylor ianlancetaylor changed the title Cleanup OS threads during GC? runtime: cleanup OS threads during GC? Mar 2, 2016
@ianlancetaylor ianlancetaylor added this to the Unplanned milestone Mar 2, 2016
@Qubitium
Copy link

Qubitium commented May 9, 2016

Cleaning up the idles would benefit mobile (android) deployments since threads are very precious resource there. Perhaps a settings to toggle cleanup idle threads or not is appropriate for those that would trade absolute performance for reduced resource usage.

@bradfitz
Copy link
Contributor

bradfitz commented May 9, 2016

I don't think this has anything to do with GC. It sounds like a general runtime issue instead. Assigning to @aclements to triage for Go 1.8.

@brianmario
Copy link
Author

I don't think this has anything to do with GC.

Cleaning up new threads right after they've been used seemed potentially wasteful, so obviously some form of periodic cleanup felt more realistic. GC is a timeframe the runtime is already setting aside for periodic cleanup, so that's what I went with initially. But I agree that it doesn't necessarily need to be tied to the GC for any particular reason.

@quentinmit quentinmit added the NeedsFix The path to resolution is known, but the work has not been done. label Oct 11, 2016
@rsc rsc modified the milestones: Go1.9, Go1.8Maybe Oct 21, 2016
@rsc rsc changed the title runtime: cleanup OS threads during GC? runtime: let idle OS threads exit Oct 21, 2016
@rsc rsc added NeedsDecision Feedback is required from experts, contributors, and/or the community before a change can be made. and removed NeedsFix The path to resolution is known, but the work has not been done. labels Oct 21, 2016
@brianmario
Copy link
Author

I know there's plenty of other work on everyone's respective plates but has anyone had a chance to think about this some more?

@ayanamist
Copy link

As i know, GC will return allocated heap memory to system after 5 minutes idle, can it also exit idle threads?

@aclements
Copy link
Member

This is certainly technically possible, but the devil's in the details. For example, we often manipulate pointers to Ms (the structure representing an OS thread) in contexts that don't interact with stop-the-world and can't have write barriers, which means it's difficult to know when we're actually done with an M and can safely recycle it. Even if we don't release the M structure itself and just release the OS thread, we have to know when we can safely reuse the M for another OS thread to avoid races.

@ianlancetaylor

This comment was marked as resolved.

@aclements

This comment was marked as resolved.

@ayanamist

This comment was marked as resolved.

@ianlancetaylor

This comment was marked as resolved.

@bradfitz bradfitz modified the milestones: Go1.10, Go1.9 May 24, 2017
@bradfitz
Copy link
Contributor

Punting to Go 1.10.

@gopherbot
Copy link

CL https://golang.org/cl/46037 mentions this issue.

gopherbot pushed a commit that referenced this issue Oct 11, 2017
Currently, threads created by the runtime exist until the whole
program exits. For #14592 and #20395, we want to be able to exit and
clean up threads created by the runtime. This commit implements that
mechanism.

The main difficulty is how to clean up the g0 stack. In cgo mode and
on Solaris and Windows where the OS manages thread stacks, we simply
arrange to return from mstart and let the system clean up the thread.
If the runtime allocated the g0 stack, then we use a new exitThread
syscall wrapper that arranges to clear a flag in the M once the stack
can safely be reaped and call the thread termination syscall.

exitThread is based on the existing exit1 wrapper, which was always
meant to terminate the calling thread. However, exit1 has never been
used since it was introduced 9 years ago, so it was broken on several
platforms. exitThread also has the additional complication of having
to flag that the stack is unused, which requires some tricks on
platforms that use the stack for syscalls.

This still leaves the problem of how to reap the unused g0 stacks. For
this, we move the M from allm to a new freem list as part of the M
exiting. Later, allocm scans the freem list, finds Ms that are marked
as done with their stack, removes these from the list and frees their
g0 stacks. This also allows these Ms to be garbage collected.

This CL does not yet use any of this functionality. Follow-up CLs
will. Likewise, there are no new tests in this CL because we'll need
follow-up functionality to test it.

Change-Id: Ic851ee74227b6d39c6fc1219fc71b45d3004bc63
Reviewed-on: https://go-review.googlesource.com/46037
Run-TryBot: Austin Clements <austin@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Keith Randall <khr@golang.org>
@aclements
Copy link
Member

Issue #22439 has an example of a problem caused by not exiting idle Ms (and freeing their stack memory).

@rsc rsc modified the milestones: Go1.10, Go1.11 Nov 22, 2017
@alexbrainman
Copy link
Member

I just discovered that exiting threads might affect IO. From WSASendTo documentation https://msdn.microsoft.com/en-us/library/windows/desktop/ms741693(v=vs.85).aspx - "... Note All I/O initiated by a given thread is canceled when that thread exits. For overlapped sockets, pending asynchronous operations can fail if the thread is closed before the operations complete. See ExitThread for more information. ..."

Alex

@aclements
Copy link
Member

I just discovered that exiting threads might affect IO.

Oh, geez. Good call.

Could this be a problem even for the current thread exiting we do? Something like:

  1. G1 on M1 starts an overlapped I/O operation.
  2. G1 migrates to M2.
  3. G2 runs on M1, calls runtime.LockOSThread, and then exits, causing M1 to exit.

Is there any way to query whether there are pending I/O operations on a thread? I'm wondering if we could just block a thread from exiting until all of its pending I/Os are done.

@alexbrainman
Copy link
Member

Could this be a problem even for the current thread exiting we do? Something like:

G1 on M1 starts an overlapped I/O operation.
G1 migrates to M2.
G2 runs on M1, calls runtime.LockOSThread, and then exits, causing M1 to exit.

I did not know that OS threads are exiting at this moment. Is there a way to verify your scenario above? What would be the program to do that? I can do 1. by reading from a TCP connection, but how would do I force 2. and 3?

Is there any way to query whether there are pending I/O operations on a thread?

I do not know of any. You could call GetCurrentThreadId Windows API to get current thread id, but you, probably, can already work that information out of whatever you have recorded for Gs and Ms. Surely we could mark Ms when IO starts, and remove that mark once IO completes.

Alex

@gopherbot
Copy link

Change https://golang.org/cl/85662 mentions this issue: runtime: remove special handling of g0 stack

@kolayne

This comment was marked as resolved.

@ianlancetaylor

This comment was marked as resolved.

@superajun-wsj
Copy link
Contributor

maybe we could reduce M number by using runtime.LockOSThread(), and here is a demo to illustrate how it works.

image

when a LockOSThread goroutine exit without calling UnLockOSThread, the corresponding M will exit.

@ianlancetaylor
Copy link
Contributor

@superajun-wsj Please add text as text, not as an image. Actual text is much easier to read than text in an image. Thanks.

@fuweid
Copy link
Contributor

fuweid commented Sep 16, 2020

when a LockOSThread goroutine exit without calling UnLockOSThread, the corresponding M will exit.

Hi @superajun-wsj Not sure that it is good solution. When the child process is created by one thread called A with PdeathSignal: SIGKILL and the thread A becomes idle, if the thread A exits, the child process will receive KILL signal. So I think UnLockOSThread might introduce other issues. just my two cents.

@greenstatic
Copy link

Any news regarding this?

In my particular case I am developing for IoT hardware running Linux that only has 1 vCPU and 128 MB of memory. From my testing I am estimating that a single thread in my case has a 80 KiB memory overhead (which is strange in itself since a Ubuntu 20.04 LTS amd64 server I also tested on approx. has an overhead of only 8 KiB). My program after running for a week uses on average 40 goroutines at any given time, however the thread count after a week is 220+. This would mean the remaining threads (if each goroutine is scheduled on it's own thread) memory overhead is over 14 MiB (220 - 40 = 180 threads * 80 KiB ~= 14 MiB)! I know that doesn't seem like a lot however on these memory constrained devices that run Linux it's still an unnecessary resource hog.

I would be perfectly happy if there was a function that we could call in the runtime package to trigger a GC-like thread cleanup.

@LeGamerDc
Copy link

Any news regarding this?

In my particular case I am developing for IoT hardware running Linux that only has 1 vCPU and 128 MB of memory. From my testing I am estimating that a single thread in my case has a 80 KiB memory overhead (which is strange in itself since a Ubuntu 20.04 LTS amd64 server I also tested on approx. has an overhead of only 8 KiB). My program after running for a week uses on average 40 goroutines at any given time, however the thread count after a week is 220+. This would mean the remaining threads (if each goroutine is scheduled on it's own thread) memory overhead is over 14 MiB (220 - 40 = 180 threads * 80 KiB ~= 14 MiB)! I know that doesn't seem like a lot however on these memory constrained devices that run Linux it's still an unnecessary resource hog.

I would be perfectly happy if there was a function that we could call in the runtime package to trigger a GC-like thread cleanup.

if you go process did not create thread itself(like by set block to socket), you could use debug.SetMaxThreads to limit max number of threads go create

@omar391
Copy link

omar391 commented Sep 2, 2023

Any update on this?

@ianlancetaylor
Copy link
Contributor

There is no update.

1 similar comment
@hxzhouh
Copy link

hxzhouh commented Apr 2, 2024

There is no update.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
NeedsDecision Feedback is required from experts, contributors, and/or the community before a change can be made.
Projects
None yet
Development

No branches or pull requests