Skip to content

Conversation

@rainersigwald
Copy link
Member

Fix #4368 by using a ReaderWriterLockSlim instead of a full lock. This
will allow the case where a.Equals(b) and b.Equals(a) run concurrently,
since they both only want a reader lock on their _properties: to
enumerate their own properties, and on the other to index in.

@tmeschter suggested this approach offline, and it seems obviously correct (in hindsight!).

@rainersigwald rainersigwald force-pushed the propertydictionary-readerwriterlockslim branch from 7122e2c to 9194da0 Compare May 14, 2019 12:45
Fix dotnet#4368 by using a ReaderWriterLockSlim instead of a full lock. This
will allow the case where a.Equals(b) and b.Equals(a) run concurrently,
since they both only want a reader lock on their _properties: to
enumerate their own properties, and on the other to index in.
The previous implementation attempted to lock inside ToDictionary(), and
then again inside GetEnumerator() from the foreach. Instead of moving
to a recursive lock, which is discouraged in the ReaderWriterLockSlim
docs, access _properties.Values directly in ToDictionary().
@rainersigwald rainersigwald force-pushed the propertydictionary-readerwriterlockslim branch from 9194da0 to ddf872e Compare May 14, 2019 13:21
@tmeschter
Copy link
Contributor

tmeschter commented May 14, 2019

On a related note, is there any possibility that one of the PropertyDictionarys could be modified during an Equals call?

Clearly this won't happen in the particular case where a.Equals(b) and b.Equals(a) run concurrently, as each will hold a read lock on their own data. But if just a.Equals(b) is running then currently nothing prevents concurrent modifications to b. Is that a problem?

using System.Diagnostics;
using Microsoft.Build.Shared;
using Microsoft.Build.Evaluation;
using System.Threading;
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: ordering.

get
{
lock (_properties)
_lock.EnterReadLock();
Copy link
Member

Choose a reason for hiding this comment

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

Maybe worth creating a struct that implements IDisposable so you can use a using statement instead of adding a try/finally with new indent.

/// </summary>
~PropertyDictionary()
{
_lock?.Dispose();
Copy link
Member

Choose a reason for hiding this comment

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

Is this needed? If the finalizer thread is used to run dispose on the lock, why not just cut out the middleman and let the finalizer thread finalize the lock.

Copy link
Contributor

Choose a reason for hiding this comment

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

Does the slim version even require disposing?


In reply to: 285822040 [](ancestors = 285822040)

Copy link
Member Author

@rainersigwald rainersigwald Jul 10, 2019

Choose a reason for hiding this comment

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

It's not clear to me whether it does or not. I was aping the ReaderWriterLockSlim doc example. But I see several current examples in CoreFX that don't dispose it, like CodePagesEncodingProvider and Privilege. So I guess it's ok to skip it.

Copy link
Member

Choose a reason for hiding this comment

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

@stephentoub any reason we're inconsistent?

Copy link
Member

Choose a reason for hiding this comment

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

any reason we're inconsistent?

In Privilege, it's static.

And CodePagesEncodingProvider exposes a static singleton that contains the instance, so it's essentially static there as well.

Copy link
Member

Choose a reason for hiding this comment

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

Does the slim version even require disposing?

ReaderWriterLockSlim wraps several event wait handles. If the RWLS isn't disposed, they'll be left for finalization.

@rainersigwald
Copy link
Member Author

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

/// <summary>
/// The object used to synchronize access for copying.
/// </summary>
private readonly object _syncRoot;
Copy link
Member

Choose a reason for hiding this comment

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

You can eliminate this field now and use the RW lock? It doesn't matter what the object is so long as it's readonly and private. That will claw back the size increase in this object form adding the RW lock field.

Copy link
Member

Choose a reason for hiding this comment

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

Disregard, I didn't view the full diff. You still use that syncroot in another path.

/// <summary>
/// Lock object to guard access to backing collection.
/// </summary>
private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();
Copy link
Contributor

Choose a reason for hiding this comment

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

Would be nice to reuse the DisposableReaderWriterLockSlim, and maybe make it its own file.

Copy link
Contributor

Choose a reason for hiding this comment

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

This is necessary for the GetCopyOnReadEnumerable case; it gets a read lock in GetCopyOnReadEnumerable.GetEnumerator and then calls PropertyDictionary.GetEnumerator within that.
IEnumerator<KeyValuePair<string, T>> IEnumerable<KeyValuePair<string, T>>.GetEnumerator()
{
lock (_properties)
_lock.EnterReadLock();
Copy link
Member

@stephentoub stephentoub Jul 11, 2019

Choose a reason for hiding this comment

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

This method was and continues to be highly suspect. Maybe it's fine because you control all the callers as well, but the lock is going to be held while this method yields. In other words:

PropertyDictionary pd = ...;
var e = pd.GetEnumerator();
e.MoveNext();
... // lock is held here!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Deadlock in PropertyDictionary equality

6 participants