eventlet / eventlet

Concurrent networking library for Python
https://eventlet.net
Other
1.24k stars 322 forks source link

monkey-patched Thread survives os.fork() #802

Open yssrku opened 1 year ago

yssrku commented 1 year ago

Since fork() only duplicates the calling thread, seeing background threads being duplicated might be surprising. I know this is a side effect of green thread, but some code might not expect this.

import eventlet
eventlet.monkey_patch() # behavior differs when monkey_patch() is called

import os
import time
import threading

def in_thread():
    seq = 0

    while True:
        print(seq)
        seq += 1
        time.sleep(1)

threading.Thread(target=in_thread).start()

os.fork()
time.sleep(10)
4383 commented 5 months ago

Hello,

Thanks for reporting your observations, and sorry for the late reply.

Indeed, child threads which are forked is a side effect of monkey patching the stdlib.

I don't know if by design of eventlet/greenlet it is voluntary or not, or if we should consider that behavior as a bug. In all case, I think that the code in your snippet will lead at some point to a race condition related to seq increment which is not an atomic operation.

Besides, when I ran this snippet with a monkey patched stdlib, I can observe that at some point the code exit from the infinite loop created in the in_thread function. When I ran it with a vanilla stdlib, the loop continue indefinitely.

Without much available design details, lets consider it as a bug for now.

4383 commented 5 months ago

Here is the strace for the parent process:

strace: Process 669 attached
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, {tv_sec=1129850, tv_nsec=214844952}, NULL) = 0
getpid()                                = 669
write(1, "669: 6\n", 7)                 = 7
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, {tv_sec=1129851, tv_nsec=217304793}, NULL) = 0
getpid()                                = 669
write(1, "669: 7\n", 7)                 = 7
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, {tv_sec=1129852, tv_nsec=219498230}, NULL) = 0
getpid()                                = 669
write(1, "669: 8\n", 7)                 = 7
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, {tv_sec=1129853, tv_nsec=222583495}, NULL) = 0
getpid()                                = 669
write(1, "669: 9\n", 7)                 = 7
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, {tv_sec=1129854, tv_nsec=212697741}, NULL) = 0
rt_sigaction(SIGINT, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=SA_RESTORER|SA_ONSTACK, sa_restorer=0x7fefdf165050}, {sa_handler=0x7fefdf4b55f9, sa_mask=[], sa_flags=SA_RESTORER|SA_ONSTACK, sa_restorer=0x7fefdf165050}, 8) = 0
munmap(0x7fefddd67000, 417792)          = 0
munmap(0x7fefdee79000, 16384)           = 0
exit_group(0)                           = ?
+++ exited with 0 +++

And here is the strace for the forked process:

strace: Process 669 attached
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, {tv_sec=1129850, tv_nsec=214844952}, NULL) = 0
getpid()                                = 669
write(1, "669: 6\n", 7)                 = 7
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, {tv_sec=1129851, tv_nsec=217304793}, NULL) = 0
getpid()                                = 669
write(1, "669: 7\n", 7)                 = 7
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, {tv_sec=1129852, tv_nsec=219498230}, NULL) = 0
getpid()                                = 669
write(1, "669: 8\n", 7)                 = 7
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, {tv_sec=1129853, tv_nsec=222583495}, NULL) = 0
getpid()                                = 669
write(1, "669: 9\n", 7)                 = 7
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, {tv_sec=1129854, tv_nsec=212697741}, NULL) = 0
rt_sigaction(SIGINT, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=SA_RESTORER|SA_ONSTACK, sa_restorer=0x7fefdf165050}, {sa_handler=0x7fefdf4b55f9, sa_mask=[], sa_flags=SA_RESTORER|SA_ONSTACK, sa_restorer=0x7fefdf165050}, 8) = 0
munmap(0x7fefddd67000, 417792)          = 0
munmap(0x7fefdee79000, 16384)           = 0
exit_group(0)                           = ?
+++ exited with 0 +++

We can observe that both process exited alone without manually killing them.

4383 commented 5 months ago

Here is a code snippet where the pid is logged:

import eventlet
eventlet.monkey_patch() # behavior differs when monkey_patch() is called

import os
import time
import threading

def in_thread():
    seq = 0

    while True:
        print(f"{os.getpid()}: {seq}")
        seq += 1
        time.sleep(1)

threading.Thread(target=in_thread).start()

os.fork()
time.sleep(10)
4383 commented 5 months ago

The sigaction() system call is used to change the action taken by a process on receipt of a specific signal. Here the signal is the SIGINT (signal interrupt). See the rt_sigaction call in the previous straces. https://linux.die.net/man/2/rt_sigaction.