@@ -31,6 +31,7 @@ internal sealed class ConnectionManager : IDisposable, IChannelControlHelper
3131 private static readonly ChannelIdProvider _channelIdProvider = new ChannelIdProvider ( ) ;
3232
3333 private readonly object _lock ;
34+ private readonly object _subChannelStateChangedLock ;
3435 internal readonly Resolver _resolver ;
3536 private readonly ISubchannelTransportFactory _subchannelTransportFactory ;
3637 private readonly List < Subchannel > _subchannels ;
@@ -57,6 +58,7 @@ internal ConnectionManager(
5758 LoadBalancerFactory [ ] loadBalancerFactories )
5859 {
5960 _lock = new object ( ) ;
61+ _subChannelStateChangedLock = new object ( ) ;
6062 _nextPickerTcs = new TaskCompletionSource < SubchannelPicker > ( TaskCreationOptions . RunContinuationsAsynchronously ) ;
6163 _resolverStartedTcs = new TaskCompletionSource < object ? > ( TaskCreationOptions . RunContinuationsAsynchronously ) ;
6264 _channelId = _channelIdProvider . GetNextChannelId ( ) ;
@@ -204,7 +206,13 @@ internal void OnSubchannelStateChange(Subchannel subchannel, ConnectivityState s
204206 }
205207 }
206208
207- subchannel . RaiseStateChanged ( state , status ) ;
209+ // Lock to avoid parallel state change events.
210+ // This is here so consumers of the state changed API don't need to worry about synchronization.
211+ // Use a new lock to avoid deadlocks. See https://github.com/grpc/grpc-dotnet/issues/2589.
212+ lock ( _subChannelStateChangedLock )
213+ {
214+ subchannel . RaiseStateChanged ( state , status ) ;
215+ }
208216 }
209217
210218 public async Task ConnectAsync ( bool waitForReady , CancellationToken cancellationToken )
0 commit comments