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

bpo-31356: Add context manager to temporarily disable GC #4224

Merged
merged 6 commits into from
Jan 29, 2018

Conversation

pablogsal
Copy link
Member

@pablogsal pablogsal commented Nov 2, 2017

As discussed in the issue by @serhiy-storchaka , @rhettinger , @ncoghlan and @gpshead this PR implements a context manager that temporarily disables GC and restores it to its previous state. The implementation is done in C, using the GIL as the locking mechanism to evict the thread problems discussed in the issue.

The documentation is updated as well as the test suite to take this into account.

Please, indicate any change that is needed and I will be more than happy to change it. 😄

https://bugs.python.org/issue31356

@pablogsal pablogsal force-pushed the bpo31356 branch 3 times, most recently from 5e263d4 to 411fc6e Compare November 2, 2017 00:42
new_thing(PyTypeObject *type, PyObject *args, PyObject *kwdict){
disabled_object *self;
self = (disabled_object *)type->tp_alloc(type, 0);
return (PyObject *) self;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiosity, is this any different from a single statement:

return (PyObject *) type->tp_alloc(type, 0);

Copy link
Member Author

@pablogsal pablogsal Nov 2, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not in this case (unless I am missing something) because I am not doing anything special with the object itself before returning but is the convention when calling tp_alloc. Check for example:

https://github.com/python/cpython/blob/master/Modules/itertoolsmodule.c#L37

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, thanks for the quick answer :)

# such as a pair trade, robotic braking, etc

without needing to explicitly enable and disable the garbage collector yourself.
This context manager is implemented in C to assure atomicity, thread safety and speed.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be good to clarify what atomicity and thread safety is assured (if any). I understand there is just one GC setting that applies to all threads.

What happens if this context manager is used in two threads at once? In particular, if thread B exiting the with statement can enable GC when thread A is still inside a with statement, that does not seem “safe”. What is the effect of calling gc.disable while another thread runs the context manager? Is GC re-enabled at __exit__, or is it disabled indefinitely?

Copy link
Member Author

@pablogsal pablogsal Nov 11, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will complement the documentation explaining this. Just to clarify the current behaviour: The context manager deactivates the garbage collector and restores the previous state of the garbage collector in the exit method. So if one thread uses the context manager while other has called it already, the garbage collector will be kept deactivated at the exit of the second invocation and it will be reactivated only at the end of the first (assuming that it was activated initially). So the guarantee is: the garbage collector will be deactivated at the entering of the context manager and the initial state will be restored at the end. This means that a thread can never reactivate the garbage collector at exit if it entered with the garbage collector deactivated. Calling gc.disable in a thread if another is inside the context manager has no effect (the gc will be kept disabled and it won't change the behaviour at exit in the thread that started the context manager).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as thread-safety goes: the CM is just as thread-unsafe as doing this dance manually. That means if thread A enters first, then thread B enters, then thread A exits, the gc will be turned back on even though thread B is still inside the context manager.

While we could choose to make this more thread-safe by replacing the CM-local "Was the GC enabled?" flag with a process-global counted semaphore:

  • that still isn't entirely thread-safe due to the possibility of direct calls to the underlying APIs
  • it adds pointless overhead to the single-threaded case

What could be useful though is a "gc.isenabled()" check in exit that emits RuntimeWarning if the GC is already enabled at that point. That way, while the CM wouldn't magically fix your thread safety problems, it would help you find out you had them in the first place.

Copy link
Member Author

@pablogsal pablogsal Nov 12, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 to the gc.isenabled() check at exit. Should I proceed to implement it?

Copy link
Member Author

@pablogsal pablogsal Nov 12, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notice that this still have edge cases for example:

  1. Thread A enters (gc disabled)
  2. Thread B enters (gc disabled)
  3. Thread A exits (gc enabled) <-- bad
  4. Thread C enters (gc disabled)
  5. Thread C exists (gc disabled)
  6. Thread B exists (gc disabled)

In this example, the hypothetical RuntimeWarning will never get triggered and thread B will never know that is has been running with the gc disabled for a while.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, the documentation should still say the context manager is not thread-safe, and can't be readily made so due to existing code that calls the low-level APIs directly.

https://www.python.org/dev/peps/pep-0556/ is likely to be a better way forward for handling GC pauses in a thread-safe way than ad hoc disabling of the GC around performance critical bits of code.

Copy link
Member Author

@pablogsal pablogsal Nov 13, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ncoghlan In 00222b4 you will find a way to emit a warning if the garbage collector is activated when some thread is inside the gc.ensure_disabled context + tests. Please, review it and if its ok I will update the documentation with the current behaviour and caveats. Of course if you prefer a different implementation I am more than happy to change it 😄

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notice that this implementation is really one step away from the counted semaphore and it covers the public API so maybe it makes sense to completely disallow enabling the gc if another thread is in inside the context. A microbenchmark reveals that is only 5.47% +- 3.2 (mean -+ stdv) slower than the previous one.

@pablogsal
Copy link
Member Author

Hi @ncoghlan ! Just a reminder in case this got outside of your radar!

@rhettinger rhettinger merged commit 72a0d21 into python:master Jan 29, 2018
@pablogsal pablogsal deleted the bpo31356 branch January 29, 2018 21:05
@ncoghlan
Copy link
Contributor

Thanks for taking care of that @rhettinger!

And thanks for the PR @pablogsal :)

1st1 added a commit to 1st1/cpython that referenced this pull request Feb 2, 2018
…honGH-4224)"

This reverts commit 72a0d21.

The reverted commit had a few issues hene it was unanimously decided
to undo it. See the bpo issue for details.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants