Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Console.Unix: avoid deadlock between LazyInitializer and Console.Out #34297

Merged
merged 32 commits into from
Apr 13, 2020
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
d0f5dc1
Console.Unix: avoid deadlock between LazyInitializer and Console.Out
tmds Mar 30, 2020
fae6ccb
Add assert for InternalSyncObject
tmds Mar 30, 2020
1ff6d97
Console: use Interlocked for lazy initialization
tmds Mar 31, 2020
69473a8
Cleanup unused 'using'
tmds Mar 31, 2020
d2b4e91
Limit locking to event handlers
tmds Mar 31, 2020
c4c09c2
Fix AllowNull -> NotNull
tmds Mar 31, 2020
7de5274
Localize LazyInitializer.EnsureInitialized logic
tmds Mar 31, 2020
af57dde
Remove lazy initialize delegates
tmds Mar 31, 2020
d5bf05f
Use static local functions for initializing
tmds Apr 1, 2020
2043770
Remove unused EnsureInitializedDisposableCore
tmds Apr 1, 2020
008d833
Remove unused arg from EnsureInitializedStdInReader
tmds Apr 1, 2020
8e0b37f
Also EnsureInitialize when not redirected
tmds Apr 1, 2020
dc533a7
Dispose Console Streams with their Reader/Writer
tmds Apr 2, 2020
3080288
Fix s_out -> s_error
tmds Apr 2, 2020
77c2c2f
leaveOpen streams, fix Encoding set race
tmds Apr 2, 2020
0f48a86
Fix races between Encoding en initializers of In/Out/Error
tmds Apr 2, 2020
f4656f0
Remove EnsureInitialized
tmds Apr 2, 2020
73d24c4
Cleanup
tmds Apr 2, 2020
5b1f471
Rename static local initializer functions to EnsureInitialized
tmds Apr 3, 2020
3500753
Use lock in Input/OutputEncoding
tmds Apr 3, 2020
614f621
Rename InternalSyncObject to s_syncObject
tmds Apr 3, 2020
1fe70bd
Cleanup space
tmds Apr 6, 2020
10ae846
Fix _isStdErrRedirected -> _isStdInRedirected
tmds Apr 6, 2020
f07e5ce
Remove space
tmds Apr 6, 2020
dcf7116
Improve comments
tmds Apr 6, 2020
b61ee9c
Fix potential CancelKeyPress deadlock
tmds Apr 7, 2020
705b104
Apply suggestions from code review
tmds Apr 7, 2020
33d5b5c
Remove unnecessary null forgiving
tmds Apr 7, 2020
61349ef
Add comment about why we're not Disposing StdInReader
tmds Apr 7, 2020
f19a34b
Add Volatile.Writes for Volatile.Read fields
tmds Apr 8, 2020
f009a74
More Volatile.Writes
tmds Apr 8, 2020
a633fdc
Add back lock to SetIn
tmds Apr 8, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 111 additions & 36 deletions src/libraries/System.Console/src/System/Console.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public static class Console
// there's little benefit to having a large buffer. So we use a smaller buffer size to reduce working set.
private const int WriteBufferSize = 256;

private static object InternalSyncObject = new object(); // for synchronizing changing of Console's static fields
private static readonly object s_syncObject = new object();
private static TextReader? s_in;
private static TextWriter? s_out, s_error;
private static Encoding? s_inputEncoding;
Expand All @@ -33,31 +33,57 @@ public static class Console
private static ConsoleCancelEventHandler? s_cancelCallbacks;
private static ConsolePal.ControlCHandlerRegistrar? s_registrar;

internal static T EnsureInitialized<T>([NotNull] ref T? field, Func<T> initializer) where T : class =>
LazyInitializer.EnsureInitialized(ref field, ref InternalSyncObject, initializer);
public static TextReader In
{
get
{
return Volatile.Read(ref s_in) ?? EnsureInitialized();

public static TextReader In => EnsureInitialized(ref s_in, () => ConsolePal.GetOrCreateReader());
static TextReader EnsureInitialized()
{
// EnsureConsoleInitialized may lock Console.Out.
// We musn't call it under s_syncObject lock,
// otherwise there can be a deadlock when another
// thread locks these objects in opposite order.
ConsolePal.EnsureConsoleInitialized();

lock (s_syncObject) // Ensures In and InputEncoding are synchronized.
{
return s_in ??= ConsolePal.GetOrCreateReader();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Technically according to ECMA the write to s_in should be volatile as well, but coreclr doesn't require that (I assume mono, too?) and we don't consistently do it everywhere we lazily initialize.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of interest: does ECMA say there is a full fence at the lock exit? Isn't that enough?

Copy link
Member

@stephentoub stephentoub Apr 7, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of interest: does ECMA say there is a full fence at the lock exit? Isn't that enough?

There's a store release on lock exit. But if the s_in object reference gets published prior to writes that fully construct that published object, that could be visible before even exiting the lock, and readers can read that reference without taking the lock in the current set up.

Copy link
Member Author

@tmds tmds Apr 7, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the s_in object reference gets published prior to writes that fully construct that published object

Interesting! I had never thought of that.

Should I add Volatile.Write where needed for ECMA?

but coreclr doesn't require that

Does it not require this specifically when storing references for new objects? Or more general on writes to memory?
Is this behavior architecture specific?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stephentoub I still have some open questions here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VolatileRead is generally necessary only to order between unrelated variables.

Interesting. This means the Volatile.Reads in this PR aren't needed?

And when a method reads the same reference that is stored on heap multiple times, the JIT/compiler aren't allowed to re-use the values from a local?

Is this ECMA spec? Or coreclr implementation?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In .NET Core mode MonoVM follows coreclr memory model.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means the Volatile.Reads in this PR aren't needed?

It is a good idea to use Volatile.Read/Write in lock-free code as a form of documentation.

Copy link
Member

@VSadov VSadov Apr 8, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. This means the Volatile.Reads in this PR aren't needed?

I did not have a chance to look at the PR in details.

My general view on this is that pairing read fences with write fences is a good practice. If there was a need for a write fence, it is likely there is a need for a read fence. Unpaired fencing is relatively easy to spot and often requires explanation.

Extra read fences do not hurt much. They may interfere with code clarity or become full fences on ARM32, although the platform has been a bit on the downhill lately.

By default, I'd keep read/write fence symmetry, or add comments why not needed.

Having said that, I'd support a policy change for not using fences for publication purpose. That is the case where we already have a paired fence. (including Mono, it seems)

Having too many fences could be a distraction from the cases where fencing is actually needed and requires extraordinary care.

In cases where fencing is actually needed, it could be a good practice to add a comment "need to write Y after writing X".
From my experience - The person who reads or makes changes to the code can see that you are ordering the write of Y. But ordering relative to what?
Even if that person is you, just a few weeks later, you may find it valuable to have a comment about X. When a read fence is paired symmetrically, having the comment just on the write side could be enough.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for explaining!

I'll add the Volatile.Write and keep the corresponding Volatile.Read in place.

}
}
}
}

public static Encoding InputEncoding
{
get
{
return EnsureInitialized(ref s_inputEncoding, () => ConsolePal.InputEncoding);
Encoding? encoding = Volatile.Read(ref s_inputEncoding);
if (encoding == null)
{
lock (s_syncObject)
{
encoding = s_inputEncoding ??= ConsolePal.InputEncoding;
}
}
return encoding;
tmds marked this conversation as resolved.
Show resolved Hide resolved
}
set
{
CheckNonNull(value, nameof(value));
lock (InternalSyncObject)

lock (s_syncObject)
{
// Set the terminal console encoding.
ConsolePal.SetConsoleInputEncoding(value);

Volatile.Write(ref s_inputEncoding, (Encoding)value.Clone());
s_inputEncoding = (Encoding)value.Clone();

// We need to reinitialize 'Console.In' in the next call to s_in
// This will discard the current StreamReader, potentially
// losing buffered data.
Volatile.Write(ref s_in, null);
s_in = null;
tmds marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
Expand All @@ -66,32 +92,40 @@ public static Encoding OutputEncoding
{
get
{
return EnsureInitialized(ref s_outputEncoding, () => ConsolePal.OutputEncoding);
Encoding? encoding = Volatile.Read(ref s_outputEncoding);
if (encoding == null)
{
lock (s_syncObject)
{
encoding = s_outputEncoding ??= ConsolePal.OutputEncoding;
}
}
return encoding;
}
set
{
CheckNonNull(value, nameof(value));

lock (InternalSyncObject)
lock (s_syncObject)
{
// Set the terminal console encoding.
ConsolePal.SetConsoleOutputEncoding(value);

// Before changing the code page we need to flush the data
// if Out hasn't been redirected. Also, have the next call to
// s_out reinitialize the console code page.
if (Volatile.Read(ref s_out) != null && !s_isOutTextWriterRedirected)
if (s_out != null && !s_isOutTextWriterRedirected)
{
s_out!.Flush();
Volatile.Write(ref s_out, null);
s_out.Flush();
s_out = null;
}
if (Volatile.Read(ref s_error) != null && !s_isErrorTextWriterRedirected)
if (s_error != null && !s_isErrorTextWriterRedirected)
{
s_error!.Flush();
Volatile.Write(ref s_error, null);
s_error.Flush();
s_error = null;
}

Volatile.Write(ref s_outputEncoding, (Encoding)value.Clone());
s_outputEncoding = (Encoding)value.Clone();
}
}
}
Expand Down Expand Up @@ -119,9 +153,37 @@ public static ConsoleKeyInfo ReadKey(bool intercept)
return ConsolePal.ReadKey(intercept);
}

public static TextWriter Out => EnsureInitialized(ref s_out, () => CreateOutputWriter(OpenStandardOutput()));
public static TextWriter Out
{
get
{
return Volatile.Read(ref s_out) ?? EnsureInitialized();

static TextWriter EnsureInitialized()
{
lock (s_syncObject) // Ensures Out and OutputEncoding are synchronized.
{
return s_out ??= CreateOutputWriter(OpenStandardOutput());
}
}
}
}

public static TextWriter Error
{
get
{
return Volatile.Read(ref s_error) ?? EnsureInitialized();

public static TextWriter Error => EnsureInitialized(ref s_error, () => CreateOutputWriter(OpenStandardError()));
static TextWriter EnsureInitialized()
{
lock (s_syncObject) // Ensures Error and OutputEncoding are synchronized.
{
return s_error ??= CreateOutputWriter(OpenStandardError());
}
}
}
}

private static TextWriter CreateOutputWriter(Stream outputStream)
{
Expand All @@ -145,26 +207,44 @@ public static bool IsInputRedirected
{
get
{
StrongBox<bool> redirected = EnsureInitialized(ref _isStdInRedirected, () => new StrongBox<bool>(ConsolePal.IsInputRedirectedCore()));
StrongBox<bool> redirected = Volatile.Read(ref _isStdInRedirected) ?? EnsureInitialized();
return redirected.Value;

static StrongBox<bool> EnsureInitialized()
{
Volatile.Write(ref _isStdInRedirected, new StrongBox<bool>(ConsolePal.IsInputRedirectedCore()));
return _isStdInRedirected!;
tmds marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

public static bool IsOutputRedirected
{
get
{
StrongBox<bool> redirected = EnsureInitialized(ref _isStdOutRedirected, () => new StrongBox<bool>(ConsolePal.IsOutputRedirectedCore()));
StrongBox<bool> redirected = Volatile.Read(ref _isStdOutRedirected) ?? EnsureInitialized();
return redirected.Value;

static StrongBox<bool> EnsureInitialized()
{
Volatile.Write(ref _isStdOutRedirected, new StrongBox<bool>(ConsolePal.IsOutputRedirectedCore()));
return _isStdOutRedirected!;
tmds marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

public static bool IsErrorRedirected
{
get
{
StrongBox<bool> redirected = EnsureInitialized(ref _isStdErrRedirected, () => new StrongBox<bool>(ConsolePal.IsErrorRedirectedCore()));
StrongBox<bool> redirected = Volatile.Read(ref _isStdErrRedirected) ?? EnsureInitialized();
return redirected.Value;

static StrongBox<bool> EnsureInitialized()
{
Volatile.Write(ref _isStdErrRedirected, new StrongBox<bool>(ConsolePal.IsErrorRedirectedCore()));
return _isStdErrRedirected!;
tmds marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

Expand Down Expand Up @@ -331,7 +411,7 @@ public static event ConsoleCancelEventHandler? CancelKeyPress
{
add
{
lock (InternalSyncObject)
lock (s_syncObject)
{
s_cancelCallbacks += value;

Expand All @@ -345,7 +425,7 @@ public static event ConsoleCancelEventHandler? CancelKeyPress
}
remove
{
lock (InternalSyncObject)
lock (s_syncObject)
{
s_cancelCallbacks -= value;
if (s_registrar != null && s_cancelCallbacks == null)
Expand Down Expand Up @@ -412,33 +492,28 @@ public static void SetIn(TextReader newIn)
{
CheckNonNull(newIn, nameof(newIn));
newIn = SyncTextReader.GetSynchronizedTextReader(newIn);
lock (InternalSyncObject)
{
Volatile.Write(ref s_in, newIn);
}
Volatile.Write(ref s_in, newIn);
}

public static void SetOut(TextWriter newOut)
{
CheckNonNull(newOut, nameof(newOut));
newOut = TextWriter.Synchronized(newOut);
Volatile.Write(ref s_isOutTextWriterRedirected, true);

lock (InternalSyncObject)
lock (s_syncObject)
{
Volatile.Write(ref s_out, newOut);
s_isOutTextWriterRedirected = true;
s_out = newOut;
}
}

public static void SetError(TextWriter newError)
{
CheckNonNull(newError, nameof(newError));
newError = TextWriter.Synchronized(newError);
Volatile.Write(ref s_isErrorTextWriterRedirected, true);

lock (InternalSyncObject)
lock (s_syncObject)
{
Volatile.Write(ref s_error, newError);
s_isErrorTextWriterRedirected = true;
s_error = newError;
}
}

Expand Down
42 changes: 28 additions & 14 deletions src/libraries/System.Console/src/System/ConsolePal.Unix.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,22 @@ private static SyncTextReader StdInReader
{
get
{
EnsureInitialized();

return Console.EnsureInitialized(
ref s_stdInReader,
() => SyncTextReader.GetSynchronizedTextReader(
new StdInReader(
encoding: Console.InputEncoding,
bufferSize: InteractiveBufferSize)));
return Volatile.Read(ref s_stdInReader) ?? EnsureInitialized();

static SyncTextReader EnsureInitialized()
{
EnsureConsoleInitialized();

SyncTextReader reader = SyncTextReader.GetSynchronizedTextReader(
new StdInReader(
encoding: Console.InputEncoding,
bufferSize: InteractiveBufferSize));

// Don't overwrite a set reader.
tmds marked this conversation as resolved.
Show resolved Hide resolved
Interlocked.CompareExchange(ref s_stdInReader, reader, null);

return s_stdInReader;
}
}
}

Expand All @@ -97,8 +105,7 @@ internal static TextReader GetOrCreateReader()
encoding: Console.InputEncoding,
detectEncodingFromByteOrderMarks: false,
bufferSize: Console.ReadBufferSize,
leaveOpen: true)
);
leaveOpen: true));
}
else
{
Expand Down Expand Up @@ -143,14 +150,14 @@ public static bool TreatControlCAsInput
if (Console.IsInputRedirected)
return false;

EnsureInitialized();
EnsureConsoleInitialized();
return !Interop.Sys.GetSignalForBreak();
}
set
{
if (!Console.IsInputRedirected)
{
EnsureInitialized();
EnsureConsoleInitialized();
if (!Interop.Sys.SetSignalForBreak(signalForBreak: !value))
throw Interop.GetExceptionForIoErrno(Interop.Sys.GetLastErrorInfo());
}
Expand Down Expand Up @@ -917,7 +924,7 @@ public static bool TryGetSpecialConsoleKey(char[] givenChars, int startIndex, in
internal static byte s_veofCharacter;

/// <summary>Ensures that the console has been initialized for use.</summary>
private static void EnsureInitialized()
internal static void EnsureConsoleInitialized()
{
if (!s_initialized)
{
Expand All @@ -928,6 +935,13 @@ private static void EnsureInitialized()
/// <summary>Ensures that the console has been initialized for use.</summary>
private static void EnsureInitializedCore()
{
// Initialization is only needed when input isn't redirected.
if (Console.IsInputRedirected)
tmds marked this conversation as resolved.
Show resolved Hide resolved
{
s_initialized = true;
return;
}

stephentoub marked this conversation as resolved.
Show resolved Hide resolved
lock (Console.Out) // ensure that writing the ANSI string and setting initialized to true are done atomically
{
if (!s_initialized)
Expand Down Expand Up @@ -1460,7 +1474,7 @@ internal sealed class ControlCHandlerRegistrar

internal void Register()
{
EnsureInitialized();
EnsureConsoleInitialized();

Debug.Assert(!_handlerRegistered);
Interop.Sys.RegisterForCtrl(c => OnBreakEvent(c));
Expand Down
4 changes: 4 additions & 0 deletions src/libraries/System.Console/src/System/ConsolePal.Windows.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ internal static class ConsolePal
{
private static IntPtr InvalidHandleValue => new IntPtr(-1);

/// <summary>Ensures that the console has been initialized for use.</summary>
internal static void EnsureConsoleInitialized()
{ }

private static bool IsWindows7()
{
// Version lies for all apps from the OS kick in starting with Windows 8 (6.2). They can
Expand Down
3 changes: 3 additions & 0 deletions src/libraries/System.Console/src/System/ConsolePal.iOS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ public override unsafe void Write(byte[] buffer, int offset, int count)

internal static class ConsolePal
{
internal static void EnsureConsoleInitialized()
{ }

public static Stream OpenStandardInput() => throw new PlatformNotSupportedException();

public static Stream OpenStandardOutput() => new NSLogStream();
Expand Down