@@ -205,6 +205,9 @@ def _default_ident(self):
205205 # see https://github.com/jupyterlab/jupyterlab/issues/17785
206206 _parent_ident : Mapping [str , bytes ]
207207
208+ # Asyncio lock for main shell thread.
209+ _main_asyncio_lock : asyncio .Lock
210+
208211 @property
209212 def _parent_header (self ):
210213 warnings .warn (
@@ -327,6 +330,8 @@ def __init__(self, **kwargs):
327330 }
328331 )
329332
333+ self ._main_asyncio_lock = asyncio .Lock ()
334+
330335 async def dispatch_control (self , msg ):
331336 """Dispatch a control request, ensuring only one message is processed at a time."""
332337 # Ensure only one control message is processed at a time
@@ -539,7 +544,7 @@ async def do_one_iteration(self):
539544 This is now a coroutine
540545 """
541546 # flush messages off of shell stream into the message queue
542- if self .shell_stream :
547+ if self .shell_stream and not self . _supports_kernel_subshells :
543548 self .shell_stream .flush ()
544549 # process at most one shell message per iteration
545550 await self .process_one (wait = False )
@@ -649,56 +654,51 @@ async def shell_channel_thread_main(self, msg):
649654 """Handler for shell messages received on shell_channel_thread"""
650655 assert threading .current_thread () == self .shell_channel_thread
651656
652- if self .session is None :
653- return
654-
655- # deserialize only the header to get subshell_id
656- # Keep original message to send to subshell_id unmodified.
657- _ , msg2 = self .session .feed_identities (msg , copy = False )
658- try :
659- msg3 = self .session .deserialize (msg2 , content = False , copy = False )
660- subshell_id = msg3 ["header" ].get ("subshell_id" )
661-
662- # Find inproc pair socket to use to send message to correct subshell.
663- subshell_manager = self .shell_channel_thread .manager
664- socket = subshell_manager .get_shell_channel_to_subshell_socket (subshell_id )
665- assert socket is not None
666- socket .send_multipart (msg , copy = False )
667- except Exception :
668- self .log .error ("Invalid message" , exc_info = True ) # noqa: G201
657+ async with self .shell_channel_thread .asyncio_lock :
658+ if self .session is None :
659+ return
669660
670- if self .shell_stream :
671- self .shell_stream .flush ()
661+ # deserialize only the header to get subshell_id
662+ # Keep original message to send to subshell_id unmodified.
663+ _ , msg2 = self .session .feed_identities (msg , copy = False )
664+ try :
665+ msg3 = self .session .deserialize (msg2 , content = False , copy = False )
666+ subshell_id = msg3 ["header" ].get ("subshell_id" )
667+
668+ # Find inproc pair socket to use to send message to correct subshell.
669+ subshell_manager = self .shell_channel_thread .manager
670+ socket = subshell_manager .get_shell_channel_to_subshell_socket (subshell_id )
671+ assert socket is not None
672+ socket .send_multipart (msg , copy = False )
673+ except Exception :
674+ self .log .error ("Invalid message" , exc_info = True ) # noqa: G201
672675
673676 async def shell_main (self , subshell_id : str | None , msg ):
674677 """Handler of shell messages for a single subshell"""
675678 if self ._supports_kernel_subshells :
676679 if subshell_id is None :
677680 assert threading .current_thread () == threading .main_thread ()
681+ asyncio_lock = self ._main_asyncio_lock
678682 else :
679683 assert threading .current_thread () not in (
680684 self .shell_channel_thread ,
681685 threading .main_thread (),
682686 )
683- socket_pair = self .shell_channel_thread .manager .get_shell_channel_to_subshell_pair (
684- subshell_id
685- )
687+ asyncio_lock = self .shell_channel_thread .manager .get_subshell_asyncio_lock (
688+ subshell_id
689+ )
686690 else :
687691 assert subshell_id is None
688692 assert threading .current_thread () == threading .main_thread ()
689- socket_pair = None
690-
691- try :
692- # Whilst executing a shell message, do not accept any other shell messages on the
693- # same subshell, so that cells are run sequentially. Without this we can run multiple
694- # async cells at the same time which would be a nice feature to have but is an API
695- # change.
696- if socket_pair :
697- socket_pair .pause_on_recv ()
693+ asyncio_lock = self ._main_asyncio_lock
694+
695+ # Whilst executing a shell message, do not accept any other shell messages on the
696+ # same subshell, so that cells are run sequentially. Without this we can run multiple
697+ # async cells at the same time which would be a nice feature to have but is an API
698+ # change.
699+ assert asyncio_lock is not None
700+ async with asyncio_lock :
698701 await self .dispatch_shell (msg , subshell_id = subshell_id )
699- finally :
700- if socket_pair :
701- socket_pair .resume_on_recv ()
702702
703703 def record_ports (self , ports ):
704704 """Record the ports that this kernel is using.
@@ -739,7 +739,7 @@ def _publish_status(self, status, channel, parent=None):
739739 def _publish_status_and_flush (self , status , channel , stream , parent = None ):
740740 """send status on IOPub and flush specified stream to ensure reply is sent before handling the next reply"""
741741 self ._publish_status (status , channel , parent )
742- if stream and hasattr (stream , "flush" ):
742+ if stream and hasattr (stream , "flush" ) and not self . _supports_kernel_subshells :
743743 stream .flush (zmq .POLLOUT )
744744
745745 def _publish_debug_event (self , event ):
@@ -1382,7 +1382,7 @@ def _abort_queues(self, subshell_id: str | None = None):
13821382
13831383 # flush streams, so all currently waiting messages
13841384 # are added to the queue
1385- if self .shell_stream :
1385+ if self .shell_stream and not self . _supports_kernel_subshells :
13861386 self .shell_stream .flush ()
13871387
13881388 # Callback to signal that we are done aborting
0 commit comments