3
3
# Copyright (c) IPython Development Team.
4
4
# Distributed under the terms of the Modified BSD License.
5
5
6
+ import asyncio
6
7
import atexit
7
8
import io
8
9
import os
14
15
from collections import deque
15
16
from io import StringIO , TextIOBase
16
17
from threading import local
17
- from typing import Any , Callable , Deque , Optional
18
- from weakref import WeakSet
18
+ from typing import Any , Callable , Deque , Dict , Optional
19
19
20
20
import zmq
21
21
from jupyter_client .session import extract_header
@@ -63,7 +63,10 @@ def __init__(self, socket, pipe=False):
63
63
self ._setup_pipe_in ()
64
64
self ._local = threading .local ()
65
65
self ._events : Deque [Callable [..., Any ]] = deque ()
66
- self ._event_pipes : WeakSet [Any ] = WeakSet ()
66
+ self ._event_pipes : Dict [threading .Thread , Any ] = {}
67
+ self ._event_pipe_gc_lock : threading .Lock = threading .Lock ()
68
+ self ._event_pipe_gc_seconds : float = 10
69
+ self ._event_pipe_gc_task : Optional [asyncio .Task ] = None
67
70
self ._setup_event_pipe ()
68
71
self .thread = threading .Thread (target = self ._thread_main , name = "IOPub" )
69
72
self .thread .daemon = True
@@ -73,7 +76,18 @@ def __init__(self, socket, pipe=False):
73
76
74
77
def _thread_main (self ):
75
78
"""The inner loop that's actually run in a thread"""
79
+
80
+ def _start_event_gc ():
81
+ self ._event_pipe_gc_task = asyncio .ensure_future (self ._run_event_pipe_gc ())
82
+
83
+ self .io_loop .run_sync (_start_event_gc )
76
84
self .io_loop .start ()
85
+ if self ._event_pipe_gc_task is not None :
86
+ # cancel gc task to avoid pending task warnings
87
+ async def _cancel ():
88
+ self ._event_pipe_gc_task .cancel () # type:ignore
89
+
90
+ self .io_loop .run_sync (_cancel )
77
91
self .io_loop .close (all_fds = True )
78
92
79
93
def _setup_event_pipe (self ):
@@ -88,6 +102,26 @@ def _setup_event_pipe(self):
88
102
self ._event_puller = ZMQStream (pipe_in , self .io_loop )
89
103
self ._event_puller .on_recv (self ._handle_event )
90
104
105
+ async def _run_event_pipe_gc (self ):
106
+ """Task to run event pipe gc continuously"""
107
+ while True :
108
+ await asyncio .sleep (self ._event_pipe_gc_seconds )
109
+ try :
110
+ await self ._event_pipe_gc ()
111
+ except Exception as e :
112
+ print (f"Exception in IOPubThread._event_pipe_gc: { e } " , file = sys .__stderr__ )
113
+
114
+ async def _event_pipe_gc (self ):
115
+ """run a single garbage collection on event pipes"""
116
+ if not self ._event_pipes :
117
+ # don't acquire the lock if there's nothing to do
118
+ return
119
+ with self ._event_pipe_gc_lock :
120
+ for thread , socket in list (self ._event_pipes .items ()):
121
+ if not thread .is_alive ():
122
+ socket .close ()
123
+ del self ._event_pipes [thread ]
124
+
91
125
@property
92
126
def _event_pipe (self ):
93
127
"""thread-local event pipe for signaling events that should be processed in the thread"""
@@ -100,9 +134,11 @@ def _event_pipe(self):
100
134
event_pipe .linger = 0
101
135
event_pipe .connect (self ._event_interface )
102
136
self ._local .event_pipe = event_pipe
103
- # WeakSet so that event pipes will be closed by garbage collection
104
- # when their threads are terminated
105
- self ._event_pipes .add (event_pipe )
137
+ # associate event pipes to their threads
138
+ # so they can be closed explicitly
139
+ # implicit close on __del__ throws a ResourceWarning
140
+ with self ._event_pipe_gc_lock :
141
+ self ._event_pipes [threading .current_thread ()] = event_pipe
106
142
return event_pipe
107
143
108
144
def _handle_event (self , msg ):
@@ -188,7 +224,7 @@ def stop(self):
188
224
# close *all* event pipes, created in any thread
189
225
# event pipes can only be used from other threads while self.thread.is_alive()
190
226
# so after thread.join, this should be safe
191
- for event_pipe in self ._event_pipes :
227
+ for _thread , event_pipe in self ._event_pipes . items () :
192
228
event_pipe .close ()
193
229
194
230
def close (self ):
0 commit comments