@@ -59,14 +59,10 @@ class TaskResult:
59
59
value : Any
60
60
61
61
62
- def _run_impl (in_q : 'queue.Queue[Task]' , out_q : 'queue.Queue[TaskResult]' ,
62
+ def _run_impl (pipe : multiprocessing . connection . Connection ,
63
63
worker_class : 'type[Worker]' , * args , ** kwargs ):
64
64
"""Worker process entrypoint."""
65
- # Note: the out_q is typed as taking only TaskResult objects, not
66
- # Optional[TaskResult], despite that being the type it is used on the Stub
67
- # side. This is because the `None` value is only injected by the Stub itself.
68
65
69
- # `threads` is defaulted to 1 in LocalWorkerPool's constructor.
70
66
# A setting of 1 does not inhibit the while loop below from running since
71
67
# that runs on the main thread of the process. Urgent tasks will still
72
68
# process near-immediately. `threads` only controls how many threads are
@@ -75,27 +71,34 @@ def _run_impl(in_q: 'queue.Queue[Task]', out_q: 'queue.Queue[TaskResult]',
75
71
pool = concurrent .futures .ThreadPoolExecutor (max_workers = 1 )
76
72
obj = worker_class (* args , ** kwargs )
77
73
74
+ # Pipes are not thread safe
75
+ pipe_lock = threading .Lock ()
76
+
77
+ def send (task_result : TaskResult ):
78
+ with pipe_lock :
79
+ pipe .send (task_result )
80
+
78
81
def make_ondone (msgid ):
79
82
80
83
def on_done (f : concurrent .futures .Future ):
81
84
if f .exception ():
82
- out_q . put (TaskResult (msgid = msgid , success = False , value = f .exception ()))
85
+ send (TaskResult (msgid = msgid , success = False , value = f .exception ()))
83
86
else :
84
- out_q . put (TaskResult (msgid = msgid , success = True , value = f .result ()))
87
+ send (TaskResult (msgid = msgid , success = True , value = f .result ()))
85
88
86
89
return on_done
87
90
88
91
# Run forever. The stub will just kill the runner when done.
89
92
while True :
90
- task = in_q . get ()
93
+ task : Task = pipe . recv ()
91
94
the_func = getattr (obj , task .func_name )
92
95
application = functools .partial (the_func , * task .args , ** task .kwargs )
93
96
if task .is_urgent :
94
97
try :
95
98
res = application ()
96
- out_q . put (TaskResult (msgid = task .msgid , success = True , value = res ))
99
+ send (TaskResult (msgid = task .msgid , success = True , value = res ))
97
100
except BaseException as e : # pylint: disable=broad-except
98
- out_q . put (TaskResult (msgid = task .msgid , success = False , value = e ))
101
+ send (TaskResult (msgid = task .msgid , success = False , value = e ))
99
102
else :
100
103
pool .submit (application ).add_done_callback (make_ondone (task .msgid ))
101
104
@@ -114,33 +117,30 @@ class _Stub():
114
117
"""Client stub to a worker hosted by a process."""
115
118
116
119
def __init__ (self ):
117
- self . _send : 'queue.Queue[Task]' = multiprocessing .get_context ().Queue ()
118
- self ._receive : 'queue.Queue[Optional[TaskResult]]' = \
119
- multiprocessing . get_context (). Queue ()
120
+ parent_pipe , child_pipe = multiprocessing .get_context ().Pipe ()
121
+ self ._pipe = parent_pipe
122
+ self . _pipe_lock = threading . Lock ()
120
123
121
124
# this is the process hosting one worker instance.
122
125
# we set aside 1 thread to coordinate running jobs, and the main thread
123
126
# to handle high priority requests. The expectation is that the user
124
127
# achieves concurrency through multiprocessing, not multithreading.
125
128
self ._process = multiprocessing .Process (
126
129
target = functools .partial (
127
- _run ,
128
- worker_class = cls ,
129
- in_q = self ._send ,
130
- out_q = self ._receive ,
131
- * args ,
132
- ** kwargs ))
130
+ _run , worker_class = cls , pipe = child_pipe , * args , ** kwargs ))
133
131
# lock for the msgid -> reply future map. The map will be set to None
134
132
# when we stop.
135
133
self ._lock = threading .Lock ()
136
134
self ._map : Dict [int , concurrent .futures .Future ] = {}
137
135
138
- # thread drainig the receive queue
136
+ # thread draining the pipe
139
137
self ._pump = threading .Thread (target = self ._msg_pump )
140
138
139
+ # Set the state of this worker to "dead" if the process dies naturally.
141
140
def observer ():
142
141
self ._process .join ()
143
- self ._receive .put (None )
142
+ # Feed the parent pipe a poison pill, this kills msg_pump
143
+ child_pipe .send (None )
144
144
145
145
self ._observer = threading .Thread (target = observer )
146
146
@@ -156,8 +156,8 @@ def observer():
156
156
157
157
def _msg_pump (self ):
158
158
while True :
159
- task_result = self ._receive . get ()
160
- if task_result is None :
159
+ task_result : Optional [ TaskResult ] = self ._pipe . recv ()
160
+ if task_result is None : # Poison pill fed by observer
161
161
break
162
162
with self ._lock :
163
163
future = self ._map [task_result .msgid ]
@@ -189,20 +189,23 @@ def remote_call(*args, **kwargs):
189
189
if self ._is_stopped ():
190
190
result_future .set_exception (concurrent .futures .CancelledError ())
191
191
else :
192
- self ._send .put (
193
- Task (
194
- msgid = msgid ,
195
- func_name = name ,
196
- args = args ,
197
- kwargs = kwargs ,
198
- is_urgent = cls .is_priority_method (name )))
192
+ with self ._pipe_lock :
193
+ self ._pipe .send (
194
+ Task (
195
+ msgid = msgid ,
196
+ func_name = name ,
197
+ args = args ,
198
+ kwargs = kwargs ,
199
+ is_urgent = cls .is_priority_method (name )))
199
200
self ._map [msgid ] = result_future
200
201
return result_future
201
202
202
203
return remote_call
203
204
204
205
def shutdown (self ):
205
206
try :
207
+ # Killing the process triggers observer exit, which triggers msg_pump
208
+ # exit
206
209
self ._process .kill ()
207
210
except : # pylint: disable=bare-except
208
211
pass
0 commit comments