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

${GDC} reading is now lockfree #3201

Merged
merged 6 commits into from
Mar 28, 2019

Conversation

snakefoot
Copy link
Contributor

@snakefoot snakefoot commented Mar 26, 2019

GlobalDiagnosticsContext - Use copy-on-write and lockfree read

See also #3197

Noticed that NLog 4.5.11 often showed threads in progress with GDC-Lookups. On NLog 4.6 one didn't see it because GDC is marked as ThreadAgnostic.

But if using GDC in a Layout with other LayoutRenderers that are not ThreadAgnostic, then the lock-punishment will be back.

Added GDC-Lookup to the file-target-layout and now they get stuck with NLog 4.6:
image

@snakefoot
Copy link
Contributor Author

ConcurrentDictionary doesn't make the same "promise" of ordering as normal Dictionary. So JsonLayout.IncludeGdc is not producing identical output.

@snakefoot snakefoot force-pushed the GdcConcurrentDictionary branch from 01c1462 to 8826d56 Compare March 26, 2019 19:25
@codecov
Copy link

codecov bot commented Mar 26, 2019

Codecov Report

Merging #3201 into dev will increase coverage by <1%.
The diff coverage is 98%.

@@          Coverage Diff           @@
##             dev   #3201    +/-   ##
======================================
+ Coverage     80%     80%   +<1%     
======================================
  Files        354     354            
  Lines      27927   27954    +27     
  Branches    3709    3713     +4     
======================================
+ Hits       22265   22295    +30     
+ Misses      4597    4595     -2     
+ Partials    1065    1064     -1

@snakefoot snakefoot force-pushed the GdcConcurrentDictionary branch from 8826d56 to 5160ae8 Compare March 26, 2019 19:52
@snakefoot snakefoot changed the title GlobalDiagnosticsContext + Config Variables - Use ConcurrentDictionary GlobalDiagnosticsContext - Use ConcurrentDictionary Mar 26, 2019
@snakefoot
Copy link
Contributor Author

snakefoot commented Mar 26, 2019

The ordering can be important in the expanding of variables that reference other variables. So reverted back to only using ConcurrentDictionary for GlobalDiagnosticsContext

@304NotModified
Copy link
Member

Isn't the concurrent queue much slower then the lock?

@snakefoot
Copy link
Contributor Author

snakefoot commented Mar 26, 2019

Isn't the concurrent queue much slower then the lock?

Not if you have two threads (Btw. it is a ConcurrentDictionary) and you don't call:

  • ConcurrentDictionary.Count
  • ConcurrentDictionary.IsEmpty
  • ConcurrentDictionary.Keys
  • ConcurrentDictionary.Values

See also https://ayende.com/blog/179329/public-service-announcement-concurrentdictionary-count-is-locking

@304NotModified
Copy link
Member

Not sure it's coincidence this tests fails now :

NLog.UnitTests.Targets.ConcurrentFileTargetTests.SimpleConcurrentTest(numProcesses: 2, numLogs: 500, mode: "none|archive") [FAIL]
At C:\projects\nlog\run-tests.ps1:33 char:1

  • & ${env:xunit20}\xunit.console.x86.exe .\tests\NLog.UnitTests\bin\rel ...

From
https://ci.appveyor.com/project/nlog/nlog/builds/23379399

@@ -506,7 +506,7 @@ public void IncludeGdcJsonProperties()
<targets>
<target name='asyncDebug' type='AsyncWrapper' timeToSleepBetweenBatches='0'>
<target name='debug' type='Debug' >
<layout type=""JsonLayout"" IncludeGdc='true' ExcludeProperties='Excluded1,Excluded2'>
<layout type=""JsonLayout"" IncludeGdc='true' IncludeAllProperties='true' ExcludeProperties='Excluded1,Excluded2'>
Copy link
Member

Choose a reason for hiding this comment

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

Even with your comment, I don't understand why this has changed :/

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Dictionary makes a very good promise to return items in the same order as inserted (Though not always). ConcurrentDictionary doesn't make this promise.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have modified the test so GDC only contains the first property, and rest of the properties comes from the LogEvent.

@snakefoot
Copy link
Contributor Author

Not sure it's coincidence this tests fails now :

Always been an unstable test. Concurrent processes writing to the same file test is not affected by this change.

@snakefoot snakefoot force-pushed the GdcConcurrentDictionary branch from 4b53bf0 to 85f92d4 Compare March 26, 2019 21:38
@snakefoot snakefoot changed the title GlobalDiagnosticsContext - Use ConcurrentDictionary PrecalculateVolatileLayouts without lock for initial Layout Mar 26, 2019
@snakefoot
Copy link
Contributor Author

snakefoot commented Mar 26, 2019

I think the trouble and the surprises with ConcurrentDictionary will outweight the gain unless in a truly multithreaded environment that is doing nothing but GDC-lookups.

Even using readerwriterlockslim will probably have some side-effects:

https://blogs.msdn.microsoft.com/pedram/2007/10/07/a-performance-comparison-of-readerwriterlockslim-with-readerwriterlock/

@snakefoot
Copy link
Contributor Author

snakefoot commented Mar 26, 2019

@304NotModified Now modified GDC to perform copy on write. This gives high concurrency for loggers but more allocation for writers (if they write a lot while others are reading).

Have the nice side-effect that GlobalDiagnosticsContext.GetNames() suddenly becomes threadsafe.

@snakefoot snakefoot changed the title PrecalculateVolatileLayouts without lock for initial Layout GlobalDiagnosticsContext - Use copy-on-write and lockfree read Mar 26, 2019
@304NotModified
Copy link
Member

@snakefoot is this one safe for NLog 4.6.1? (So low impact)?

@snakefoot
Copy link
Contributor Author

I think it is safe for NLog 4.6.1. One could also consider using the same approach for NLog Configuration Variables.

@snakefoot snakefoot force-pushed the GdcConcurrentDictionary branch from ade07df to 37aa6ec Compare March 27, 2019 06:35
@304NotModified 304NotModified added enhancement Improvement on existing feature performance labels Mar 27, 2019
@304NotModified 304NotModified added this to the 4.6.1 milestone Mar 27, 2019
@@ -69,7 +68,7 @@ public static void Set(string item, object value)
{
lock (dict)
{
dict[item] = value;
GetWritableDict(dictReadOnly != null && !dict.ContainsKey(item))[item] = value;
Copy link
Member

Choose a reason for hiding this comment

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

Duplicate

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes one could probably make one of the Setters call the other.

{
dict.Clear();
var newDict = new Dictionary<string, object>(clone ? _dict.Count + 1 : 0);
Copy link
Member

Choose a reason for hiding this comment

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

Is it correct that clear has the init capacity of 0, and the field has the default capacity (16?). It that on purpose?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Don't understand your question. But Clear (clone=false) calls with capacity = 0 that means do not allocate internal arrays until insert happens.

Copy link
Member

@304NotModified 304NotModified Mar 27, 2019

Choose a reason for hiding this comment

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

indeed, but the init doesn't set capacity to 0

private static Dictionary<string, object> _dict = new Dictionary<string, object>();

Copy link
Contributor Author

@snakefoot snakefoot Mar 27, 2019

Choose a reason for hiding this comment

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

Still don't understand your question. The default constructor for Dictionary uses capacity = 0. See also https://referencesource.microsoft.com/#mscorlib/system/collections/generic/dictionary.cs,85 (Means do not allocate until insert happens)

@304NotModified
Copy link
Member

please check snakefoot#5 :)

@snakefoot
Copy link
Contributor Author

@304NotModified Have now extracted the clone-logic from GetWritableDict

@304NotModified 304NotModified self-assigned this Mar 28, 2019
@snakefoot snakefoot force-pushed the GdcConcurrentDictionary branch from f75267d to f913a69 Compare March 28, 2019 06:14
@snakefoot snakefoot force-pushed the GdcConcurrentDictionary branch from f913a69 to 3d1d030 Compare March 28, 2019 18:13
@304NotModified 304NotModified changed the title GlobalDiagnosticsContext - Use copy-on-write and lockfree read ${GDC} reading is now lockfree Mar 28, 2019
Copy link
Member

@304NotModified 304NotModified left a comment

Choose a reason for hiding this comment

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

thanks for the refactoring!

@304NotModified 304NotModified merged commit 80b1e85 into NLog:dev Mar 28, 2019
@snakefoot
Copy link
Contributor Author

Happy to do it. And also very happy about the result :)

@snakefoot snakefoot deleted the GdcConcurrentDictionary branch April 4, 2020 12:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement Improvement on existing feature performance
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants