December 19, 2017·4 min read·engineeringpythonconcurrencymaintenance

Python 3.6.4 prevents a stalled file operation from holding the GIL

Python 3.6.4 releases the GIL during FileIO file-size lookup and corrects several other cases where blocked or interrupted work affected unrelated operations.

A file operation that appears local can wait on a remote filesystem. If the interpreter holds its global lock during that wait, the effect can extend beyond the thread performing the read. Python 3.6.4 fixes this case in io.FileIO.readall() and io.FileIO.read(). Both now release the GIL while obtaining the file size, addressing a hang involving an inaccessible NFS server.

The December 19 release is a maintenance update to Python 3.6. Its candidate change record in the final tag records this correction as issue 32186, with a patch by Nir Soffer. The note identifies a hang of all threads, which makes the change relevant to a program even when only one of its operations touches the unavailable server.

I regard this as a useful example of why Python's execution model needs attention below the application abstraction. A file object presents a small interface, but implementing that interface includes decisions about which operations can block while the interpreter lock is held. Those decisions affect the behavior of work elsewhere in the process.

Looking up a size is part of the blocking path

The correction concerns obtaining the file size inside the read methods. It does not claim to make an unavailable NFS server respond or impose a timeout on the filesystem operation. Releasing the GIL changes what other Python threads can do during that wait. The file operation itself still depends on the underlying filesystem's response.

That is a practical distinction between containing a stall and resolving it. A stalled operation can remain stalled while unrelated threads regain the opportunity to execute Python code. The release note does not describe a new parallel-computation model or remove the GIL from the interpreter. It corrects a particular place where an operation that may block should not retain the lock throughout its wait.

A library method's name is not enough to identify all of its blocking behavior. Reading can involve preparatory metadata access as well as transferring bytes. A review focused only on the final data-read system call can miss the earlier step that caused the reported hang. The issue makes that preparatory step explicit.

An empty queue still has state to clean up

Issue 31620 addresses memory leakage when callers waiting on an empty asyncio.Queue time out. An empty queue has no queued application item, but it can still contain bookkeeping for operations waiting to receive one. Repeated unsuccessful waits therefore exercise library state even when the application receives no result.

The fix is useful for programs whose normal behavior includes periods without work. A timeout is an ordinary way for a caller to stop waiting, so the library must complete the associated cleanup. The change record identifies that particular leak without giving a general memory-use figure or claiming that every asynchronous queue operation has changed.

Another asyncio entry, issue 31632, corrects _SSLProtocolTransport.set_protocol(). The method had been modifying the wrong reference to the protocol. This is an ownership correction inside the transport implementation. Updating a protocol relationship has to affect the reference subsequently used by the transport, otherwise the apparent configuration change and the actual behavior can diverge.

These are separate defects, but each concerns state hidden behind a small public operation. The queue wait needs to remove abandoned waiting state, and the transport method needs to update the intended protocol reference. Application code delegates those details to the library when it chooses the higher-level interface.

Helper processes need a recovery path

The release also changes multiprocessing's semaphore tracker and forkserver. Issue 31310 says the semaphore tracker should launch again after a crash. Issue 31308 makes the forkserver immune to Ctrl-C and other user interruptions and restarts it when necessary after a crash. The helper process is part of the execution environment even when the application does not invoke it as a separate command.

A helper's lifecycle can otherwise become an implicit precondition for later operations. Starting the application successfully does not establish that its supporting process remains available. These corrections make recovery part of the implementation's behavior for the particular helpers named in the notes. They do not establish that arbitrary application worker failures are automatically recoverable.

I prefer these concrete recovery rules to assuming that a background helper will survive for the whole lifetime of the application. The important behavior is what the library does after the helper fails. Here the release identifies when it should arrange another instance instead of leaving later work dependent on an unavailable one.

One more maintenance change makes failure visible to automation. Under issue 31351, python -m ensurepip returns a nonzero exit status if pip bootstrapping fails. A failed setup step can then be distinguished through the command's status instead of requiring a caller to interpret its output. Like releasing the GIL around file-size lookup, this preserves information at the boundary between one operation and the code depending on it. The release gives callers a usable failure signal while correcting the specific runtime conditions described in its change record.