Skip to content

Commit d981e08

Browse files
authored
docs: Merge pull request #1324 from svinota/p11-docs-threading
docs: add threading vs. fork documentation Bug-Url: #1324
2 parents a45f3b8 + badf570 commit d981e08

File tree

3 files changed

+73
-21
lines changed

3 files changed

+73
-21
lines changed

docs/asyncio.rst

-21
Original file line numberDiff line numberDiff line change
@@ -184,24 +184,3 @@ such as `GenericNetlinkSocket`, or using custom wrappers, like in
184184
`IPRoute`. The plan is to refactor all components to provide an asynchronous
185185
API, keeping the synchronous API for compatibility with existing projects
186186
that use pyroute2.
187-
188-
Thread safety
189-
-------------
190-
191-
Is the current core thread-safe? Yes and no at the same time. While there
192-
are no locks in the core, components like sockets and message queues are
193-
now thread-local.
194-
195-
This means that the same pyroute2 socket object manages as many
196-
underlying netlink sockets as there are threads accessing it.
197-
198-
Pros:
199-
200-
* Simplicity and absence of mutexes, which eliminates the risk of deadlocks.
201-
202-
Cons:
203-
204-
* Race conditions are still possible if shared data is not thread-local.
205-
* Debugging existing netlink flows at runtime is limited because any
206-
debugger session will create its own underlying netlink socket. This
207-
makes logging and post-mortem analysis more important.

docs/index.rst

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Usage
2121

2222
usage
2323
asyncio
24+
threading
2425
iproute
2526
ndb
2627
fixtures

docs/threading.rst

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
.. _threading:
2+
3+
Using the library in threaded environments
4+
==========================================
5+
6+
Network namespaces
7+
------------------
8+
9+
To run a separate socket in one network namespace while keeping the
10+
Python process in another namespace, the library follows these steps:
11+
12+
1. Spawn a child process.
13+
2. Execute `netns.setns()` in the child.
14+
3. Create a socket.
15+
4. Send the file descriptor back to the parent using `socket.send_fds()`.
16+
5. Terminate the child process.
17+
6. Create a socket in the parent using `socket(fileno=...)`.
18+
19+
As a result, the parent process obtains a socket belonging to
20+
another network namespace. However, it can be used natively like
21+
any other socket, both synchronously and asynchronously.
22+
23+
Starting a child process: os.fork()
24+
-----------------------------------
25+
26+
By default, pyroute2 uses `os.fork()` to create the child process.
27+
In multithreaded environments, `os.fork()` does not recreate threads;
28+
the child process continues only from the thread where `os.fork()`
29+
was called. This can leave the garbage collector in a corrupted state.
30+
31+
While this is generally not an issue -- since the socket creation routine
32+
stops the garbage collector, and does not rely on shared data -- there
33+
is still some risk.
34+
35+
To address this, pyroute2 provides a configuration option:
36+
`config.child_process_mode`. The default value is `"fork"`, but you
37+
can change it to `"mp"` to use the `multiprocessing` module for creating
38+
and managing the child process.
39+
40+
Starting a child process: multiprocessing
41+
-----------------------------------------
42+
43+
The `multiprocessing` module may or may not rely on `os.fork()`, depending
44+
on the method set via `multiprocessing.set_start_method()`. In Python
45+
versions earlier than 3.14, the default method is `"fork"`, but starting
46+
from Python 3.14, the default is `"spawn"`.
47+
48+
Using `"spawn"` is safer but significantly slower. Additionally, `"spawn"`
49+
introduces limitations due to pickling:
50+
51+
* The target function and its arguments must be pickleable.
52+
* Passing lambda functions as the target is not possible.
53+
* The libc instance cannot be passed to the child process.
54+
55+
Since pyroute2 does not manage the `multiprocessing` start method, the
56+
start method cannot be configured via `config.child_process_mode`. If you
57+
set `config.child_process_mode` to `"mp"`, and need to explicitly specify
58+
the start method, you must call `multiprocessing.set_start_method()`
59+
manually elsewhere in your program.
60+
61+
Threading and asyncio
62+
---------------------
63+
64+
An asyncio event loop can only run in the thread where it was started.
65+
In a multithreaded environment, the library creates a local event loop
66+
and a local netlink socket for each thread that accesses the object.
67+
While this approach is safe, it complicates object termination.
68+
Although an event loop can be stopped from another thread, it cannot
69+
be closed.
70+
71+
The best solution is to call `close()` in every thread where you
72+
call `bind()`.

0 commit comments

Comments
 (0)