ZeroMQ’s inproc transport achieves zero-copy messaging by directly sharing memory between threads, bypassing the usual kernel-level data copying.
Let’s see it in action. Imagine two threads in the same process: one acting as a publisher and the other as a subscriber.
import zmq
import threading
import time
def publisher_thread(context):
socket = context.socket(zmq.PUB)
socket.bind("inproc://my_topic")
print("Publisher bound to inproc://my_topic")
for i in range(5):
message = f"Message {i}"
print(f"Publishing: {message}")
socket.send_string(message)
time.sleep(0.1)
socket.send_string("STOP")
socket.close()
def subscriber_thread(context):
socket = context.socket(zmq.SUB)
socket.connect("inproc://my_topic")
socket.setsockopt_string(zmq.SUBSCRIBE, "") # Subscribe to everything
print("Subscriber connected to inproc://my_topic")
while True:
message = socket.recv_string()
if message == "STOP":
print("Subscriber received STOP signal.")
break
print(f"Received: {message}")
socket.close()
if __name__ == "__main__":
context = zmq.Context()
pub_t = threading.Thread(target=publisher_thread, args=(context,))
sub_t = threading.Thread(target=subscriber_thread, args=(context,))
pub_t.start()
sub_t.start()
pub_t.join()
sub_t.join()
context.term()
print("ZeroMQ context terminated.")
When you run this, you’ll see the publisher sending messages and the subscriber receiving them with virtually no delay, even though they are distinct threads. The inproc:// prefix tells ZeroMQ to use a shared memory mechanism within the same process.
The fundamental problem inproc solves is the overhead of inter-thread communication. Traditionally, if one thread needs to send data to another within the same process, it might involve:
- Copying data: The sending thread copies data into a buffer.
- Context switch: The OS switches to the receiving thread.
- Copying data again: The receiving thread copies data from the buffer into its own memory.
- Synchronization: Locks or other mechanisms might be needed, adding further overhead.
inproc bypasses the kernel and the explicit copying steps. When a PUB socket sends a message to an inproc address, and a SUB socket is connected to that same inproc address, ZeroMQ can arrange for the message data to be placed directly into the receiving socket’s internal queue without any intermediate copies. It’s as if the sending thread’s buffer becomes the receiving thread’s buffer, or at least is directly accessible.
The key levers you control are:
- The
inproc://<address>URL: This is the magic string that signifies in-process communication.<address>is a unique identifier for the communication channel within the process. - Socket Types: You still use standard ZeroMQ socket types (
PUB/SUB,REQ/REP,PUSH/PULL, etc.) to define the communication pattern. Theinproctransport simply dictates how the data moves between sockets of compatible types. zmq.Context: Allinprocsockets must belong to the samezmq.Contextinstance. This is how ZeroMQ knows which sockets are in the same process and can share memory.
The most surprising thing about inproc is that it doesn’t just avoid copying; it can also avoid the need for explicit synchronization primitives like mutexes for basic message passing. When a message is sent via inproc, the underlying shared memory segment is managed by ZeroMQ in a way that allows the receiving socket to access the data directly. The send and recv operations are effectively atomic with respect to the message data itself, meaning you don’t need to wrap your socket.send() and socket.recv() calls in locks if you’re only passing messages between sockets on the same inproc transport and the messages themselves are the only shared mutable state.
The next concept you’ll likely encounter is how to manage multiple inproc endpoints within a single zmq.Context for more complex inter-thread communication topologies.