Hungry Mind , Blog about everything in IT - C#, Java, C++, .NET, Windows, WinAPI, ...

ReaderWriterLockSlim fails on dual-socket environments

This is yet another story of orphaned ReaderWriterLockSlim.

Another dump, the same problem - ReaderWriterLockSlim object state is corrupted:

0:173> !do 0x0000000001c679f8
Name:        System.Threading.ReaderWriterLockSlim
MethodTable: 000007f87ec7c1d8
EEClass:     000007f87e999448
Size:        96(0x60) bytes
File:        C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Core\v4.0_4.0.0.0__b77a5c561934e089\System.Core.dll
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
000007f880dbcbf0  4000755       50       System.Boolean  1 instance                1 fIsReentrant
000007f880dbe0c8  4000756       30         System.Int32  1 instance                0 myLock
000007f880db2308  4000757       34        System.UInt32  1 instance                1 numWriteWaiters
000007f880db2308  4000758       38        System.UInt32  1 instance               28 numReadWaiters
000007f880db2308  4000759       3c        System.UInt32  1 instance                0 numWriteUpgradeWaiters
000007f880db2308  400075a       40        System.UInt32  1 instance                0 numUpgradeWaiters
000007f880dbcbf0  400075b       51       System.Boolean  1 instance                0 fNoWaiters
000007f880dbe0c8  400075c       44         System.Int32  1 instance               -1 upgradeLockOwnerId
000007f880dbe0c8  400075d       48         System.Int32  1 instance               -1 writeLockOwnerId
000007f880db9138  400075e        8 ...g.EventWaitHandle  0 instance 000000000381eb38 writeEvent
000007f880db9138  400075f       10 ...g.EventWaitHandle  0 instance 00000000035a32e0 readEvent
000007f880db9138  4000760       18 ...g.EventWaitHandle  0 instance 0000000000000000 upgradeEvent
000007f880db9138  4000761       20 ...g.EventWaitHandle  0 instance 0000000000000000 waitUpgradeEvent
000007f880dd0398  4000763       28         System.Int64  1 instance 9 lockID
000007f880dbcbf0  4000765       52       System.Boolean  1 instance                0 fUpgradeThreadHoldingRead
000007f880db2308  4000766       4c        System.UInt32  1 instance       1073741824 owners
000007f880dbcbf0  4000767       53       System.Boolean  1 instance                0 fDisposed
000007f880dd0398  4000762      408         System.Int64  1   static 14118 s_nextLockID
000007f87ec99a20  4000764        8 ...ReaderWriterCount  0 TLstatic  t_rwc
0:173> .formats 0n1073741824 
Evaluate expression:
  Hex:     00000000`40000000

EnterReadLock, EnterWriteLock and other Enter operations waiting for an event which never goes off. Deadlock.

I must say that I checked possibilities of thread aborts in this code and found no signs of such scenarios happening. This made me desperately searching for another root cause of the problem.

So started searching ReaderWriterLockSlim.cs file for potential problems. I immediately became suspicious when I realyzed there is lack of synchronization when, for example, TryEnterUpgradeableReadLockCore method modified one of object fields:

uint owners;
...
private bool TryEnterReadLockCore(TimeoutTracker timeout)
{ 
...
       owners++;
}

Fields are not declared volatile, nor are they modified via interlocked operations. The only exception is the myLock field, which is used as a spin lock ang modified via Interlocked.CompareExchange:

[MethodImpl(MethodImplOptions.AggressiveInlining)] 
private void EnterMyLock()
{
   if (Interlocked.CompareExchange(ref myLock, 1, 0) != 0)
      EnterMyLockSpin(); 
}

Note, however, spin lock release method doesn't use Interlocked operation:

private void ExitMyLock()
{ 
   Debug.Assert(myLock != 0, "Exiting spin lock that is not held");
   myLock = 0; 
} 

This looks to be a mistake, possibly the root cause on one of root causes.

OK, lets go back to the problem - ReaderWriterLockSlim gets locked forever on 24-core dual socket Intel hardware. Threads are not aborted, the code is perfect. So what the hell is going on?

Well, the problem looks to be bad software (ReaderWriterLockSlim) on expensive hardware. Dell PowerEdge R720 has two psysical CPUs - 2x Intel Xeon E5-2620, 1200 MHz (12 x 100), 6 cores and 12 threads each. 24 logical cores total. And the problem is experienced only on such configurations.

I made a program that creates 24 (= Environment.ProcessorCount) threads with highest priority acquiring and releasing the lock in a tight loop:

using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
namespace RWLSTest
{
   internal class Program
   {
      private static readonly ReaderWriterLockSlim slim = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
      private static readonly List<object> objects = new List<object>();
      private static readonly Int32 processorCount = Environment.ProcessorCount;
      private static Int32 threadsCount;
      private static Int64 reads;
      private static Int64 writes;
      private static volatile Object[] threads = new Object[processorCount];
      private static Action loopAction;
      static Program()
      {
         // Let it JIT those methods
         using (var temp = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion)) {
            Thread.Yield();
            temp.EnterReadLock();
            temp.ExitReadLock();
         }
         var thread = new Thread(() =>
         {
            try {
               Thread.Sleep(Timeout.Infinite);
            }
            catch {
               return;
            }
            throw new InvalidOperationException();
         });
         thread.Start();
         try {
            thread.Abort();
         }
         catch (Exception e) {
            Console.WriteLine(e.Message);
         }
      }
      private static void LoopWithEmptryTryBlocks()
      {
         var random = new Random(Environment.TickCount);
         for (;;) {
            if (random.Next(processorCount) <= (processorCount / 4)) {
               Interlocked.Increment(ref writes);
               try {}
               finally {
                  slim.EnterWriteLock();
               }
               try {
                  ExclusiveLoop(random);
               }
               finally {
                  slim.ExitWriteLock();
               }
            }
            else {
               Interlocked.Increment(ref reads);
               try {}
               finally {
                  slim.EnterReadLock();
               }
               try {
                  SharedLoop(random);
               }
               finally {
                  slim.ExitReadLock();
               }
            }
         }
      }
      [MethodImpl(MethodImplOptions.AggressiveInlining)]
      private static void SharedLoop(Random random)
      {
         foreach (var o in objects) {
            var i = (Int32)o;
            if ((i % processorCount) == (random.Next() % processorCount) && random.Next(37) == 3) {
               break;
            }
         }
      }
      [MethodImpl(MethodImplOptions.AggressiveInlining)]
      private static void ExclusiveLoop(Random random)
      {
         if (objects.Count < 10240) {
            for (var i = 0; i < 19; ++i) {
               if (random.Next(13) == 7) {
                  objects.Add(random.Next());
               }
            }
         }
         for (var i = 0; i < 13; ++i) {
            if (objects.Count > 0 && random.Next(19) == 13) {
               objects.Remove(random.Next() % objects.Count);
            }
         }
      }
      private static void Loop()
      {
         var random = new Random(Environment.TickCount);
         for (;;) {
            if (random.Next(processorCount) <= (processorCount / 4)) {
               slim.EnterWriteLock();
               try {
                  ExclusiveLoop(random);
               }
               finally {
                  slim.ExitWriteLock();
               }
            }
            else {
               slim.EnterReadLock();
               try {
                  SharedLoop(random);
               }
               finally {
                  slim.ExitReadLock();
               }
            }
         }
      }
      private static void StartOneThread(Object state)
      {
         var thread = new Thread(() =>
         {
            try {
               Interlocked.Increment(ref threadsCount);
               loopAction();
            }
            catch (ThreadAbortException) {}
            finally {
               Interlocked.Decrement(ref threadsCount);
               ThreadPool.UnsafeQueueUserWorkItem(StartOneThread, state);
            }
         }) { Priority = ThreadPriority.Highest };
         thread.Start();
         Thread.VolatileWrite(ref threads[(Int32)state], thread);
      }
      private static void Main(string[] args)
      {
         var random = new Random(Environment.TickCount);
         var abortCycle = 0;
         if (args.Length > 0) {
            abortCycle = Int32.Parse(args[0]);
            loopAction = LoopWithEmptryTryBlocks;
         }
         else {
            loopAction = Loop;
         }
         for (var i = 0; i < processorCount; ++i) {
            StartOneThread(i);
         }
         for (var i = 0U;; ++i) {
            Thread.Sleep(1);
            if (abortCycle > 0 && i % abortCycle == 0) {
               var ti = random.Next(111) % processorCount;
               var thread = (Thread)Thread.VolatileRead(ref threads[ti]);
               if (thread != null) {
                  Console.WriteLine("Aborting thread #" + ti);
                  try {
                     thread.Abort();
                  }
                  catch (Exception e) {
                     Console.WriteLine(e.Message);
                  }
               }
            }
         }
      }
   }
}

I ran it several times and after about 1 hour all threads ended up waiting for lock event to fire. Voila! Have a look at the state of ReaderWriterLockSlim object:

0:000> !do 000000b343bd2860 
Name:        System.Threading.ReaderWriterLockSlim
MethodTable: 000007fbf887c1a8
EEClass:     000007fbf8599448
Size:        96(0x60) bytes
File:        C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Core\v4.0_4.0.0.0__b77a5c561934e089\System.Core.dll
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
000007fbfe6ac7b8  4000755       50       System.Boolean  1 instance                1 fIsReentrant
000007fbfe6adc90  4000756       30         System.Int32  1 instance                0 myLock
000007fbfe6a1ed0  4000757       34        System.UInt32  1 instance                1 numWriteWaiters
000007fbfe6a1ed0  4000758       38        System.UInt32  1 instance               23 numReadWaiters
000007fbfe6a1ed0  4000759       3c        System.UInt32  1 instance                0 numWriteUpgradeWaiters
000007fbfe6a1ed0  400075a       40        System.UInt32  1 instance                0 numUpgradeWaiters
000007fbfe6ac7b8  400075b       51       System.Boolean  1 instance                0 fNoWaiters
000007fbfe6adc90  400075c       44         System.Int32  1 instance               -1 upgradeLockOwnerId
000007fbfe6adc90  400075d       48         System.Int32  1 instance               -1 writeLockOwnerId
000007fbfe6a8d00  400075e        8 ...g.EventWaitHandle  0 instance 000000b343beb448 writeEvent
000007fbfe6a8d00  400075f       10 ...g.EventWaitHandle  0 instance 000000b343be2fd0 readEvent
000007fbfe6a8d00  4000760       18 ...g.EventWaitHandle  0 instance 0000000000000000 upgradeEvent
000007fbfe6a8d00  4000761       20 ...g.EventWaitHandle  0 instance 0000000000000000 waitUpgradeEvent
000007fbfe6bff60  4000763       28         System.Int64  1 instance 1 lockID
000007fbfe6ac7b8  4000765       52       System.Boolean  1 instance                0 fUpgradeThreadHoldingRead
000007fbfe6a1ed0  4000766       4c        System.UInt32  1 instance       1073741824 owners
000007fbfe6ac7b8  4000767       53       System.Boolean  1 instance                0 fDisposed
000007fbfe6bff60  4000762      408         System.Int64  1   static 2 s_nextLockID
000007fbf88999f0  4000764        8 ...ReaderWriterCount  0 TLstatic  t_rwc

There are 23 reader waiters, 1 writer waiter and owners field is 0x40000000 once again. All of 24 threads look like the following:

0:000> ~22e !CLRStack
OS Thread Id: 0xf28 (22)
        Child SP               IP Call Site
000000b361a1df78 000007fc137b315b [HelperMethodFrame_1OBJ: 000000b361a1df78] System.Threading.WaitHandle.WaitOneNative(System.Runtime.InteropServices.SafeHandle, UInt32, Boolean, Boolean)
000000b361a1e0a0 000007fbfe5195c4 System.Threading.WaitHandle.InternalWaitOne(System.Runtime.InteropServices.SafeHandle, Int64, Boolean, Boolean)
000000b361a1e0e0 000007fbf8af4c25 System.Threading.ReaderWriterLockSlim.WaitOnEvent(System.Threading.EventWaitHandle, UInt32 ByRef, TimeoutTracker)
000000b361a1e150 000007fbf8dd4c48 System.Threading.ReaderWriterLockSlim.TryEnterReadLockCore(TimeoutTracker)
000000b361a1e1b0 000007fbf8804d4a System.Threading.ReaderWriterLockSlim.TryEnterReadLock(TimeoutTracker)
000000b361a1e200 000007fbf8af55ad System.Threading.ReaderWriterLockSlim.TryEnterReadLock(Int32)
000000b361a1e250 000007fba0010a45 RWLSTest.Program.Loop()
000000b361a1e2c0 000007fba00106f7 RWLSTest.Program+<>c__DisplayClass4.<startonethread>b__3()
000000b361a1e330 000007fbfe4ff8a5 System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean)
000000b361a1e490 000007fbfe4ff609 System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean)
000000b361a1e4c0 000007fbfe4ff5c7 System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object)
000000b361a1e510 000007fbfe512d21 System.Threading.ThreadHelper.ThreadStart()
000000b361a1e828 000007fbff6bf713 [GCFrame: 000000b361a1e828] 
000000b361a1eb58 000007fbff6bf713 [DebuggerU2MCatchHandlerFrame: 000000b361a1eb58] 

They all WaitOnEvent, but who and when will fire the event? This never happens. Deadlock.

Now lets get back to ExitMyLock. ReaderWriterLockSlim contains many fields with at least 88 bytes storage space required (without extra aligning, if needed). Modern Intel CPUs have cache lines of 64 bytes which is too small to entirely hold ReaderWriterLockSlim object instance. So each one requires at least two cache lines to hold its data. Since the distance between myLock and owners fields is more than 64 bytes (both x86 and x64), releasing myLock without a memory barrier (or interlocked instruction) causes only a portion of object's storage invalidated on demand between CPU cores and/or CPUs. Invalidation is forced by EnterMyLock's interlocked instruction. But only 64 bytes of aligned memory where myLock resides. Other cache line's changes might not be visible at that point. So the core acquiring the lock may see inconsistent object state.

Very important note: ReaderWriterLockSlim.cs is a part of 4.5Update1 reference source. Vanilla .NET 4.5 and probably several updates following it has this code, for example 4.0.30319.17929, 4.0.30319.18408. Recent versions, for example 4.0.30319.33440, has fixed this:

private void ExitMyLock()
{ 
   Volatile.Write(ref myLock, 0);
}

Volatile write inserts explicit memory barriers and makes any changes visible to other cores and CPUs.

Conclusion: do not use ReaderWriterLockSlim class without .NET Framework updated to at least 4.0.30319.33440. Its will eventually fail, at least on dual-socket Intel system.

Windows 8.1 and Windows Server 2012 R2 have this issue fixed. Windows Server 2012 (nor R2) seems to stuck with buggy implementation of ReaderWriterLockSlim class. After installing all available updates, ExitMyLock looks the same (no volatile write operation).

Injustice: Gods Among Us

Игроки в онлайне делятся на два типа: одни спамят, а другие занимались сексом с их матерью

Visual Studio 2012 Update 4

Asshole in Range Rover

Сегодня красиво наказал энурезника очередного на белом коне. Стоял в левом ряду на Автозаводской (по 3 полосы в каждую сторону) первым, смотрю в зеркало - а там очередное хуйло, обгоняя аж 3-4 автомобиля, прет по встречке. Я дал по газам и не позволил белому Range Rover'у стать перед собой. А на другом конце перекрестка в том же левом ряду первыми стояли угадайте кто? Правильно - пацаны с красными и синими мигалками. Результат - быдло к тротуару. Энурез нужно лечить, господа, ЛЕЧИТЬ.

Cruel DateTime vs serialization

Недавно столкнулся с проблемой сериализации DateTime. protobuf-net, как и BinaryFormatter не сохраняют тип даты, перечисление DateTimeKind. В результате после чтения из архива тип даты становится Unspecified. Вот выдержка из исходного кода структуры DateTime:

// This value type represents a date and time.  Every DateTime
// object has a private field (Ticks) of type Int64 that stores the
// date and time as the number of 100 nanosecond intervals since
// 12:00 AM January 1, year 1 A.D. in the proleptic Gregorian Calendar. 
//
// Starting from V2.0, DateTime also stored some context about its time 
// zone in the form of a 3-state value representing Unspecified, Utc or 
// Local. This is stored in the two top bits of the 64-bit numeric value
// with the remainder of the bits storing the tick count. This information 
// is only used during time zone conversions and is not part of the
// identity of the DateTime. Thus, operations like Compare and Equals
// ignore this state. This is to stay compatible with earlier behavior
// and performance characteristics and to avoid forcing  people into dealing 
// with the effects of daylight savings. Note, that this has little effect
// on how the DateTime works except in a context where its specific time 
// zone is needed, such as during conversions and some parsing and formatting 
// cases.
// 
// There is also 4th state stored that is a special type of Local value that
// is used to avoid data loss when round-tripping between local and UTC time.
// See below for more information on this 4th state, although it is
// effectively hidden from most users, who just see the 3-state DateTimeKind 
// enumeration.
// 
// For compatability, DateTime does not serialize the Kind data when used in 
// binary serialization.
// 
// For a description of various calendar issues, look at
//
// Calendar Studies web site, at
// http://serendipity.nofadz.com/hermetic/cal_stud.htm. 
//
// 
[StructLayout(LayoutKind.Auto)] 
[Serializable]
public struct DateTime : IComparable, IFormattable, IConvertible, ISerializable, IComparable,IEquatable { 

Как видно из описания, дата представляет собой число типа Int64 в котором хранится количество 100нс интервалов от начала времен - 12:00 AM January 1, year 1 A.D. in the proleptic Gregorian Calendar. А вот до какой точки - здесь уже интересней. В случае DateTimeKind.Utc - до Гринвича, DateTimeKind.Local - до времени в локальной для операционной системы\программы\потока зоне. И последнее значение - DateTimeKind.Unspecified, до куда - неизвестно.

На что влияет тип даты? В первую очередь на методы ToLocalTime и ToUniversalTime, потом уже и на форматирующие методы. Самое неприятное происходит при вызове этих двух методов для дат с типом DateTimeKind.Unspecified - ToLocalTime считает, что дата имеет тип DateTimeKind.Utc, а ToUniversalTime - что тип DateTimeKind.Local. Логично, правда? В результате если сериализировать DateTime.UtcNow, вычитать его обратно и преобразовать в DateTimeKind.Utc методом ToUniversalTime - получаем сдвиг на временную зону. При этом ToLocalTime вернет правильный результат.

Обойти это недоразумение можно с помощью статического метода DateTime.SpecifyKind.

using System;

namespace CSharpLanguageInv
{
   internal class Program
   {
      private static void Main(string[] args)
      {
         var now = DateTime.UtcNow;
         var unspecified = DateTime.SpecifyKind(now, DateTimeKind.Unspecified);
         var localTime = unspecified.ToLocalTime();
         var universalTime = unspecified.ToUniversalTime();
         Console.WriteLine("Now               = " + now/*.Ticks*/);
         Console.WriteLine("unspecified       = " + unspecified/*.Ticks*/);
         Console.WriteLine("localTime         = " + localTime/*.Ticks*/);
         Console.WriteLine("universalTime     = " + universalTime/*.Ticks*/);
         Console.WriteLine("Now - unspecified = " + (now/*.Ticks*/ - unspecified/*.Ticks*/));
         Console.ReadLine();
      }
   }
}

Razer DeathStalker (not Ultimate)

Завидуйте, нищеброды! Американская раскладка, с не кастрированным левым шифтом и нормальным вводом! USA! USA! USA!

Maelstorm- Wild Dances

Windows Kernel Source code

Windows Research Kernel sources.

Freddie Mercury - Living On My Own

A story of orphaned ReaderWriterLockSlim

Recently I got 2 dumps of a resource intensive process. The customer complained about hangs in web UI so the application had been killed and restarted numerous times. Quick WinDbg analysis spotted thousands of working threads in the pool:

0:000> !ThreadPool
CPU utilization: 6%
Worker Thread: Total: 6304 Running: 6303 Idle: 1 MaxLimit: 12000 MinLimit: 24
Work Request in Queue: 0
--------------------------------------
Number of Timers: 2
--------------------------------------
Completion Port Thread:Total: 2 Free: 1 MaxFree: 48 CurrentLimit: 1 MaxLimit: 12000 MinLimit: 24

Most of the threads wait for ReaderWriterLockSlim read lock on ManualResetEvent instance:

System.Threading.WaitHandle.WaitOneNative(System.Runtime.InteropServices.SafeHandle, UInt32, Boolean, Boolean)
System.Threading.WaitHandle.InternalWaitOne(System.Runtime.InteropServices.SafeHandle, Int64, Boolean, Boolean)
System.Threading.ReaderWriterLockSlim.WaitOnEvent(System.Threading.EventWaitHandle, UInt32 ByRef, TimeoutTracker)
System.Threading.ReaderWriterLockSlim.TryEnterReadLockCore(TimeoutTracker)
System.Threading.ReaderWriterLockSlim.TryEnterReadLock(TimeoutTracker)

One thread was waiting for write lock on the same object. No other stacks observed executing while holding the lock, all lock usages seemed proper:

s.EnterXXXLock();
try
{
   // Do the job
}
finally
{
   s.ExitXXXLock();
}

Yet the process is fucked up. What the hell is wrong here? Well, sometimes things get very complicated...

Lets take a look on reader writer lock instance:

0:3444> !do 0x0000000001affe60
Name:        System.Threading.ReaderWriterLockSlim
MethodTable: 000007f87a91c1a8
EEClass:     000007f87a639448
Size:        96(0x60) bytes
File:        C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Core\v4.0_4.0.0.0__b77a5c561934e089\System.Core.dll
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
000007f8802fc7b8  4000755       50       System.Boolean  1 instance                1 fIsReentrant
000007f8802fdc90  4000756       30         System.Int32  1 instance                0 myLock
000007f8802f1ed0  4000757       34        System.UInt32  1 instance                1 numWriteWaiters
000007f8802f1ed0  4000758       38        System.UInt32  1 instance             6293 numReadWaiters
000007f8802f1ed0  4000759       3c        System.UInt32  1 instance                0 numWriteUpgradeWaiters
000007f8802f1ed0  400075a       40        System.UInt32  1 instance                0 numUpgradeWaiters
000007f8802fc7b8  400075b       51       System.Boolean  1 instance                0 fNoWaiters
000007f8802fdc90  400075c       44         System.Int32  1 instance               -1 upgradeLockOwnerId
000007f8802fdc90  400075d       48         System.Int32  1 instance               -1 writeLockOwnerId
000007f8802f8d00  400075e        8 ...g.EventWaitHandle  0 instance 00000000f8e8f9c0 writeEvent
000007f8802f8d00  400075f       10 ...g.EventWaitHandle  0 instance 00000000fa23f040 readEvent
000007f8802f8d00  4000760       18 ...g.EventWaitHandle  0 instance 0000000000000000 upgradeEvent
000007f8802f8d00  4000761       20 ...g.EventWaitHandle  0 instance 0000000000000000 waitUpgradeEvent
000007f88030ff60  4000763       28         System.Int64  1 instance 9 lockID
000007f8802fc7b8  4000765       52       System.Boolean  1 instance                0 fUpgradeThreadHoldingRead
000007f8802f1ed0  4000766       4c        System.UInt32  1 instance       1073741824 owners
000007f8802fc7b8  4000767       53       System.Boolean  1 instance                0 fDisposed
000007f88030ff60  4000762      408         System.Int64  1   static 17381 s_nextLockID
000007f87a9399f0  4000764        8 ...ReaderWriterCount  0 TLstatic  t_rwc
    >> Thread:Value c18:0000000001917410 d18:00000000025a51c8 e54:000000000245d5f0 e90:0000000000000000 e20:00000000f90a6ce8 [>6000 more values]

The most valuable information is the owners field:

0:000> ? 0n1073741824
Evaluate expression: 1073741824 = 00000000`40000000

And heres what it means:

//The uint, that contains info like if the writer lock is held, num of
//readers etc. 
uint owners; 

//Various R/W masks 
//Note:
//The Uint is divided as follows:
//
//Writer-Owned  Waiting-Writers   Waiting Upgraders     Num-REaders 
//    31          30                 29                 28.......0
// 
//Dividing the uint, allows to vastly simplify logic for checking if a 
//reader should go in etc. Setting the writer bit, will automatically
//make the value of the uint much larger than the max num of readers 
//allowed, thus causing the check for max_readers to fail.

private const uint WRITER_HELD = 0x80000000;
private const uint WAITING_WRITERS = 0x40000000; 
private const uint WAITING_UPGRADER = 0x20000000;

So, we are waiting for writers. Hold on, there are no writers! The lock is not held. Conslusion - the lock state is corrupted and could never recover. This is called orphaned lock.

The only thing (I am aware of) might have caused the orphan - asynchronous thread aborts. If a thread is interrupted while taking a lock via [Try]EnterXXXLock method - we might come into described problem since those methods are not atomic. In my case thread aborts are triggered by WCF runtime (or perhaps Http runtime, it doesn't matter).

Heres a simple code to simulate the situation:

using System;
using System.Threading;
 
namespace CLRInv
{
   internal class Program
   {
      private static readonly ReaderWriterLockSlim rwl = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
 
      private static void Main(string[] args)
      {
         rwl.EnterReadLock();
         do {
            rwl.ExitReadLock();
 
            var reader = new Thread(UseLockForRead);
            var writer = new Thread(UseLockForWrite);
            reader.Start();
            writer.Start();
 
            Thread.Sleep(TimeSpan.FromSeconds(2));
            writer.Abort();
            reader.Abort();
 
            reader.Join();
            writer.Join();
         }
         while (rwl.TryEnterReadLock(TimeSpan.FromSeconds(10)));
 
         Console.WriteLine("Gotcha!");
 
         // Forever young
         rwl.EnterWriteLock();
      }
 
      private static void UseLockForRead()
      {
         try {
            for (;;) {
               rwl.EnterReadLock();
               try {
               }
               finally {
                  rwl.ExitReadLock();
               }
            }
         }
         catch (ThreadAbortException) {
         }
      }
 
      private static void UseLockForWrite()
      {
         try {
            for (;;) {
               rwl.EnterWriteLock();
               try {
               }
               finally {
                  rwl.ExitWriteLock();
               }
            }
         }
         catch (ThreadAbortException) {
         }
      }
   }
}

The conclusion is not very optimistic - you can't use slim locks the way you normally use em if your application experiences timeouts and consequent thread aborts. Does this mean slim locks should be banned? Well, no. You just need to ensure special constructions are used to take and release locks.

First of all we need to prevent async aborts while executing [Try]EnterXXXLock. How to do that? You must take the lock inside so called protected region. Here they mention a protected region of code, such as a catch block, finally block, or constrained execution region. This basically means ThreadAbortExeption can't be thrown asynchronously while executing except and finally blocks of a try statement. So our [Try]EnterXXXLock should be wrapped like this:

try {} finally { rw.EnterXXXLock(); }

Weird? No, if you have .NET BCL source code. There are tonns of empty try blocks with excessive comments:

// prevent ThreadAbort while updating state
try { } 
finally
{
...
}

Proper slim lock usage turns out to be the following construction:

var lockIsHeld = false;
try {
   try {
   }
   finally {
      rwl.EnterReadLock();
      lockIsHeld = true;
   }
 
   // Do work here
}
finally {
   if (lockIsHeld) {
      rwl.ExitReadLock();
   }
}

Asynchronous ThreadAbortException is thrown either before lock is held or after lock is held making finally unlock the object if it has been locked.

Two things I havent studied yet - is it possible to observe the following situation:

try {
   // <-- Could it happen here, before finally block is run but after try has opened fault clause region?
   try {
   }
   finally {
      // Lock
   }
 
   // Use resource
}
finally {
   // Unlock
}

Thats why I used that condition flag to ensure the lock is held.

And the second one:

try {
}
finally {
   // Lock
}
try {
   // Use resource
}
finally {
   // Unlock
}

Is this one safe? Probably yes.

The bottom line is know your runtime environment, don't use new features cause they are cool or Mr. Jeff has fresh stuff in his brand new book you love so much. Or hire a professional like me [:-D].

Mortal Kombat 9 Komplete PC edition in steam

Oracle Database 12c Release 1

Available for download! Linux/Solaris only, as always...

Arithmetic overflows in C#/C/C++

Иногда самая банальная вещь может быть невероятно полезной. К примеру, как бы Вы решили проблему переполнения в результате арифметических операций, скажем, на языке C/C++? Уверен, пришлось бы анализировать операнды перед операций в поиске возможного переполнения, что есть не очень тривиальная задача. Еще один недостаток такого решения - производительность, анализ будет дороже самой операции. Конечно, можно использовать ассемблерные вставки типа _asm jo OverflowedLabel после каждой операции, но это скорее маразм. Я хочу заметить, что язык ассемблера позволяет постфактум проверять флаг переполнения последней арифметической операции.

Язык C#, IL код и CLR позволяют легко выявлять переполнения с помощью т.н. checked секций кода. Одноименное ключевое слово языка C# определяет блок кода в котором переполнения приводят к исключению OverflowException. Реализация проста до безобразия - JIT вставляет операцию jo SomeAddr после каждой операции внутри блока checked, а по адресу SomeAddr находится одна единственная инструкция - call clr!JIT_Overflow.

Интересно, а многие разработчики вообще следят за такими вещами? Обрабатываете ли вы возможные переполнения? Приходит ли вообще на ум, что это может быть проблемой?

Visual Studio 2012 Update 3 RC 2

Download Visual Studio 2012 Update 3 RC 2.

Mauritius Beachcomber Royal Palm

Вот сюда отправляемся в конце сентября. Отчет по приезду. Завидуйте! :)

Xbox 360 Wireless Controller for Windows

Купил себе Xbox 360 Wireless Controller for Windows, для ПК. Некоторые игры засияли во всей красе, например заброшенный почти сразу Prince of Persia Forgotten Sands. Невероятно удобный манипулятор! Втыкнул ресивер в USB, засунул батарейки в устройство, включил - все, можно играть.

Xbox 360 Wireless Controller for Windows

Visual C++ concurrency runtime bug

#include <thread>
#include <mutex>
#include <condition_variable>
 
std::mutex m1;
std::mutex m2;
 
std::condition_variable e1;
std::condition_variable e2;
 
void t1()
{   
   for (;;) {
      {
      std::lock_guard<std::mutex> lg(m1);
      e1.notify_one();
      }
      std::unique_lock<std::mutex> m2Lock(m2);
      e2.wait_for(m2Lock, std::chrono::milliseconds(15));
   }
}
 
void t2()
{   
   for (;;) {
      {
      std::lock_guard<std::mutex> lg(m2);
      e2.notify_one();
      }
      std::unique_lock<std::mutex> m1Lock(m1);
      e1.wait_for(m1Lock, std::chrono::milliseconds(15));
   }
}
 
int _tmain(int argc, _TCHAR* argv[])
{
   const auto hc = std::thread::hardware_concurrency();
   std::thread thread1(&t1);
   std::thread thread2(&t2);
   thread1.join();
   thread2.join();
   return 0;
}
 

Вот такая простая программа падает со следующим сообщением:

Unhandled exception at 0x0000000077A32782 (ntdll.dll) in Win32ConInv.exe: 0xC000000D: An invalid parameter was passed to a service or function.

Stack:

 	ntdll.dll!string "Enabling heap debug options\n"()	Unknown
>	msvcr110d.dll!__crtWaitForThreadpoolTimerCallbacks(_TP_TIMER * pti, int fCancelPendingCallbacks) Line 539	C
 	msvcr110d.dll!Concurrency::details::DeleteAsyncTimerAndUnloadLibrary(_TP_TIMER * timer) Line 696	C++
 	msvcr110d.dll!Concurrency::details::TimedSingleWaitBlock::destroyTimer(bool waitForOutstandingCallback) Line 457	C++
 	msvcr110d.dll!Concurrency::details::TimedSingleWaitBlock::Satisfy(Concurrency::Context * * pContextOut, Concurrency::details::EventWaitNode * pNode) Line 484	C++
 	msvcr110d.dll!Concurrency::details::EventWaitNode::Satisfy(Concurrency::Context * * pContextOut) Line 330	C++
 	msvcr110d.dll!Concurrency::details::_Condition_variable::notify_one() Line 645	C++
 	msvcp110d.dll!do_signal(_Cnd_internal_imp_t * * cond, int all) Line 68	C++
 	msvcp110d.dll!_Cnd_signal(_Cnd_internal_imp_t * * cond) Line 84	C++
 	Win32ConInv.exe!std::_Cnd_signalX(_Cnd_internal_imp_t * * _Cnd) Line 108	C++
 	Win32ConInv.exe!std::condition_variable::notify_one() Line 51	C++
 	Win32ConInv.exe!t1() Line 21	C++
 	Win32ConInv.exe!std::_Bind<1,void,void (__cdecl*const)(void),std::_Nil,std::_Nil,std::_Nil,std::_Nil,std::_Nil,std::_Nil,std::_Nil>::operator()() Line 1152	C++
 	Win32ConInv.exe!std::_LaunchPad >::_Run(std::_LaunchPad > * _Ln) Line 196	C++
 	Win32ConInv.exe!std::_LaunchPad >::_Go() Line 188	C++
 	msvcp110d.dll!_Call_func(void * _Data) Line 52	C++
 	msvcr110d.dll!_callthreadstartex() Line 354	C
 	msvcr110d.dll!_threadstartex(void * ptd) Line 337	C
 	kernel32.dll!BaseThreadInitThunk()	Unknown
 	ntdll.dll!RtlUserThreadStart()	Unknown

А вот и дефект на Microsoft Connect со статусом Closed. Stephan T. Lavavej обещает the fix will be available in the next release of our C++ Standard Library implementation, 12/17/2012. Дефект открыт 9/13/2012, сейчас 04/28/2013. Пиздец!

MethodDescriptor from _methodPtrAux

.prefer_dml 1
r $t0 = 7fe95b5c088 + 5; .printf /D "<link cmd=\"!DumpMD %p\">DumpMD</link>", poi($t0 + 8*by($t0+2) + 3) + 8*by($t0+1)

CLR Execution Context

В очередной раз стало не хватать свежачка из внутренностей .NET Framework и CLR - решил поизучать потоки и домены, а точнее контекст, в котором выполняется наш прекрасный код.

Начнем с примера:

public class ContextInspector : MarshalByRefObject
{
    public void DumpContext(Object state)
    {
        var domain = AppDomain.CurrentDomain;
        var domainName = domain.IsDefaultAppDomain() ? "Default Domain" : domain.FriendlyName;
        Console.WriteLine("#" + Thread.CurrentThread.ManagedThreadId + ": Domain = " + domainName);
    }
 
    public void QueueDumpContext()
    {
        ThreadPool.QueueUserWorkItem(DumpContext);
    }
}
 
public class Program1
{
    private static void Main(string[] args)
    {
        new ContextInspector().QueueDumpContext();
        Thread.Yield();
        var domain = AppDomain.CreateDomain("Custom Domain");
        var type = typeof(ContextInspector);
        var contextInspector = (ContextInspector)domain.CreateInstanceAndUnwrap(type.Assembly.FullNametype.FullName);
        // We just need the same thread pool thread at least once
        contextInspector.QueueDumpContext();
        contextInspector.QueueDumpContext();
        Console.ReadLine();
    }
}

Программа помещает вызов метода DumpContext в очередь пула потоков CLR из основного домена приложения (Default Domain), а после из специализированного (Custom Domain), созданного мною. Точный вывод программы не очень интересен, но следующие две строки порождают важные вопросы:

#6: Domain = Default Domain
#6: Domain = Custom Domain

Почему один и тот же рабочий поток находится в разных доменах? Чем мотивировано такое поведение и кто его обеспечивает?

Вполне логично ожидать выполнения рабочих единиц (Work Items) пула потоков в окружении, которое максимально напоминает исходное. Фактически, единственное различие должно быть в потоке выполнения и всем, что с этим прямо или косвенно связано. И здесь CLR вместе с BCL делают нам, разработчикам, большое одолжение - запоминают наиболее важные аспекты контекста выполнения и разворачивают (применяют) их в потоках пула перед выполнением пользовательских рабочих единиц. Необходимо напомнить, что классы mscorlib.dll (в т.ч. ThreadPool) загружаются единожды в т.н. Shared Domain и поэтому они (вместе с состоянием, результатом JIT компиляции IL кода и пр.) используются всеми классами всех без исключения доменов. Подробнее об этом можно почитать здесь.

Пул потоков CLR реализован как связка управляемого кода в лице класса ThreadPool и неуправляемой составляющей внутри библиотеки clr.dll, классы ThreadpoolMgr и ThreadPoolNative. Давайте рассмотрим первую часть с доступным исходным кодом из Reference Source, файл ThreadPool.cs, основные сущности:

  1. Статический класс ThreadPoolGlobals.

    Содержит глобальное состояние пула потоков для каждого домена. Напомню, что статические члены классов привязаны к доменам даже в случае загрузки их (классов) в Shared Domain. Одним из полей является экземпляр ThreadPoolWorkQueue. Хочу обратить внимание, что начиная с версии 4.0 CLR поддерживает отдельную очередь рабочих единиц для каждого пользовательского домена и равномерно распределяет время своих потоков между ними.

  2. Класс ThreadPoolWorkQueue.

    Умная очередь рабочих единиц - полностью управляемый код с плюшками типа Work Stealing.

  3. Статический класс ThreadPool

    Интерфейс взаимодействия между пользовательским кодом и всей инфраструктутой пула потоков CLR. Подавляющее большинство методов объявлены как static extern и реализованы в clr.dll.

Разберем процесс попадания рабочей единицы в пул и ее диспатчеризацию. Все начинается с метода QueueUserWorkItem, за которым следует добавление элемента в очередь и вызов RequestWorkerThread:

[InlinedCallFrame: 00000000004ec728] System.Threading.ThreadPool.RequestWorkerThread()
DomainNeutralILStubClass.IL_STUB_PInvoke()
System.Threading.ThreadPoolWorkQueue.Enqueue(System.Threading.IThreadPoolWorkItem, Boolean)
System.Threading.ThreadPool.QueueUserWorkItemHelper(System.Threading.WaitCallback, System.Object, System.Threading.StackCrawlMark ByRef, Boolean)
System.Threading.ThreadPool.QueueUserWorkItemHelper(System.Threading.WaitCallback, System.Object, System.Threading.StackCrawlMark ByRef, Boolean)
System.Threading.ThreadPool.QueueUserWorkItem(System.Threading.WaitCallback, System.Object)

Метод RequestWorkerThread объявлен как static extern, его реализация находится вот здесь:

bp clr!ThreadPoolNative::RequestWorkerThread

И до безобразия проста:

call    clr!GetThread
mov     rcx,qword ptr [rax+10h]
...
call    clr!ThreadpoolMgr::SetAppDomainRequestsActive
...
call    clr!ThreadpoolMgr::QueueUserWorkItem
...

Текущий домен маркируется как требующий диспатчеризации из очереди, а сбивающий с толку вызов ThreadpoolMgr::QueueUserWorkItem на самом деле взаимодействует с приложением-хостом через clr!CorHost2::m_HostThreadpoolManager.

Теперь проанализируем как пул обрабатывает запрос на рабочий поток. Каждый из них начинает свою жизнь следующим образом:

clr!ThreadpoolMgr::WorkerThreadStart
clr!Thread::intermediateThreadProc+0x7d
KERNEL32!BaseThreadInitThunk+0xd
ntdll!RtlUserThreadStart+0x1d

clr!ThreadpoolMgr::WorkerThreadStart производит некую инициализацию:

...
call    clr!ClrFlsSetThreadType
...
call    qword ptr [clr!_imp_CoInitializeEx]
...

Далее вызов clr!ThreadpoolMgr::ExecuteWorkRequest выполняет следующий код по стеку:

clr!Frame::Push+0x90
clr!Frame::Pop+0x86
clr!ManagedPerAppDomainTPCount::DispatchWorkItem+0x2bd
clr!ManagedPerAppDomainTPCount::DispatchWorkItem+0x23b
clr!ManagedPerAppDomainTPCount::DispatchWorkItem+0xb4

Инструкция clr!Frame::Push+0x90 - call clr!ManagedThreadCallState::IsAppDomainEqual проверяет текущий домен на соответствие необходимому:

0:005> g
Breakpoint 1 hit
clr!Frame::Push+0x95:
000007fe`f38035d1 85c0            test    eax,eax
0:005> r eax
eax=0

и изменяет его в случае неравенства вызовом clr!AppDomain::EnterContext:

clr!AppDomain::EnterContext
clr!Thread::DoADCallBack+0x21d
clr!Frame::Push+0xd6
clr!Frame::Pop+0x86
clr!ManagedPerAppDomainTPCount::DispatchWorkItem+0x2bd
clr!ManagedPerAppDomainTPCount::DispatchWorkItem+0x23b
clr!ManagedPerAppDomainTPCount::DispatchWorkItem+0xb4
clr!ThreadpoolMgr::ExecuteWorkRequest+0x4c
clr!ThreadpoolMgr::WorkerThreadStart+0xf3
clr!Thread::intermediateThreadProc+0x7d
KERNEL32!BaseThreadInitThunk+0xd
ntdll!RtlUserThreadStart+0x1d

который просто прыгает в другую функцию:

jmp     clr!Thread::EnterContextRestricted

И в упрощенной форме смена домена осуществляется записью его в Thread Local Storage:

mov     ecx,dword ptr [clr!gAppDomainTLSIndex]
call    qword ptr [clr!_imp_TlsSetValue]

Далее мы возвращаемся к clr!Frame::Push+0x90 и еще раз проверяем текущий домен:

clr!Frame::Push+0x90
clr!Frame::Pop+0x86
clr!ManagedPerAppDomainTPCount::DispatchWorkItem+0x2bd
clr!ReturnToPreviousAppDomainHolder::Init+0x39
clr!Thread::DoADCallBack+0x234

На этот раз имеет место равенство и мы попадаем в clr!QueueUserWorkItemManagedCallback, где происходят следующие вызовы:

mscorlib_ni+0x49c790
clr!CallDescrWorkerInternal+0x83
clr!CallDescrWorkerWithHandler+0x4a
clr!MethodDescCallSite::CallTargetWorker+0x2e6
clr!QueueUserWorkItemManagedCallback+0x2a

mscorlib_ni+0x49c790 - это на самом деле метод PerformWaitCallback класса _ThreadPoolWaitCallback:

// 
// This type is necessary because VS 2010's debugger looks for a method named _ThreadPoolWaitCallbacck.PerformWaitCallback 
// on the stack to determine if a thread is a ThreadPool thread or not.  We have a better way to do this for .NET 4.5, but
// still need to maintain compatibility with VS 2010.  When compat with VS 2010 is no longer an issue, this type may be 
// removed.
//
internal static class _ThreadPoolWaitCallback
{ 
    [System.Security.SecurityCritical]
    static internal bool PerformWaitCallback() 
    { 
        return ThreadPoolWorkQueue.Dispatch();
    } 
}

Как вы видите - один из потоков пула был выделен для диспатчеризации рабочих единиц одного из пользовательских доменов.

Рассмотрим ключевые моменты метода Dispatch:

  1. Извлекает и исполняет элементы из очереди пока не кончился квант времени отведенный пулом: while ((Environment.TickCount - quantumStartTime) < ThreadPoolGlobals.tpQuantum).

    Это скорее оптимизация, призванная уменьшить количество переходов между clr.dll и mscorlib.dll, точнее не тратить больше времени на эти переходы нежели на сами рабочие единицы.

    Кстати, ТэПэ квантум по-умолчанию равен 30 миллисекундам, а это два кванта операционной системы (в обычных условиях). Наверное это попытка гарантировать минимум один целый квантум для метода.

  2. После исполнения каждой рабочей единицы пул потоков уведомляется об этом; пул может приказать диспатчеру немедленно выйти из метода:
    //
    // Notify the VM that we executed this workitem.  This is also our opportunity to ask whether Hill Climbing wants 
    // us to return the thread to the pool or not.
    // 
    if (!ThreadPool.NotifyWorkItemComplete()) 
        return false;
    
  3. Каждый раз в случае непустой очереди вызывает workQueue.EnsureThreadRequested(), тем самым запрашивая дополнительные потоки у пула (но следит чтобы количество неудовлетворенных запросов было всегда меньше количества процессоров в системе):
    //
    // If we found work, there may be more work.  Ask for another thread so that the other work can be processed 
    // in parallel.  Note that this will only ask for a max of #procs threads, so it's safe to call it for every dequeue.
    //
    workQueue.EnsureThreadRequested();
       
  4. Первым действием уменьшает счетчик неудовлетворенных запросов с помощью workQueue.MarkThreadRequestSatisfied():
    //
    // Update our records to indicate that an outstanding request for a thread has now been fulfilled. 
    // From this point on, we are responsible for requesting another thread if we stop working for any 
    // reason, and we believe there might still be work in the queue.
    // 
    // Note that if this thread is aborted before we get a chance to request another one, the VM will
    // record a thread request on our behalf.  So we don't need to worry about getting aborted right here.
    //
    workQueue.MarkThreadRequestSatisfied(); 
       

Резюме:

  • ThreadPool управляет очередью задач своего домена, запрашивает ресурсы у пула потоков и за отведенное время исполняет выполняет максимальное количество единиц из очереди.
  • CLR управляет пулом потоков и распределяет процессорное время между очередями всех существующих доменов, которые инициализировали ThreadPool (фактически минимум один раз поместили задачу в очередь). Кроме этого, CLR накатывает правильный низкоуровневый контекст прежде чем передавать управление в класс ThreadPool.

Кстати, clr!ThreadpoolMgr::WorkerThreadStart в цикле обработки запросов передает управление clr!Thread::SetApartment, что приводит к последовательным вызовам CoUninitialize и CoInitializeEx(NULL, COINIT_MULTITHREADED). А значит состояние ole32.dll постоянно очищается и все потоки пула живут в MTA.

Следующий шаг - более тщательное изучение класса ThreadPool, начну с метода QueueUserWorkItemHelper (кстати, обратите внимание на коментарии к коду, они подтверждают часть написанного выше):

//ThreadPool has per-appdomain managed queue of work-items. The VM is
//responsible for just scheduling threads into appdomains. After that 
//work-items are dispatched from the managed queue. 
[System.Security.SecurityCritical]  // auto-generated
private static bool QueueUserWorkItemHelper(WaitCallback callBack, Object state, ref StackCrawlMark stackMark, bool compressStack ) 
{
    bool success =  true;

    if (callBack != null) 
    {
                //The thread pool maintains a per-appdomain managed work queue. 
        //New thread pool entries are added in the managed queue. 
        //The VM is responsible for the actual growing/shrinking of
        //threads. 

        EnsureVMInitialized();

        // 
        // If we are able to create the workitem, we need to get it in the queue without being interrupted
        // by a ThreadAbortException. 
        // 
        try { }
        finally 
        {
            QueueUserWorkItemCallback tpcallBack = new QueueUserWorkItemCallback(callBack, state, compressStack, ref stackMark);
            ThreadPoolGlobals.workQueue.Enqueue(tpcallBack, true);
            success = true; 
        }
    } 
    else 
    {
        throw new ArgumentNullException("WaitCallback"); 
    }
    return success;
}

Экземпляры QueueUserWorkItemCallback добавляются в очередь и на этом финита. Но интерес здесь вызывает уже конструктор QueueUserWorkItemCallback:

internal QueueUserWorkItemCallback(WaitCallback waitCallback, Object stateObj, bool compressStack, ref StackCrawlMark stackMark) 
{
    callback = waitCallback; 
    state = stateObj; 
    if (compressStack && !ExecutionContext.IsFlowSuppressed())
    { 
        // clone the exection context
        context = ExecutionContext.Capture(
            ref stackMark,
            ExecutionContext.CaptureOptions.IgnoreSyncCtx | ExecutionContext.CaptureOptions.OptimizeDefaultCase); 
    }
} 

Обратите внимание на использование параметров compressStack и stackMark вместе с классом ExecutionContext. Без дальнейшего углубления читатель может сообразить что здесь происходит - захват текущего контекста выполнения вместе с неким анализом стека в переменную-член QueueUserWorkItemCallback. Возможности ExecutionContext мимолетно описаны Рихтером в известной книге CLR via C#, а нам самое время переместиться в файл ExecutionContext.cs и проанализировать класс более детально.

Во-первых, он предлагает шесть доступных пользовательскому коду методов:

  1. static ExecutionContext Capture()
  2. ExecutionContext CreateCopy()
  3. static bool IsFlowSuppressed()
  4. static AsyncFlowControl SuppressFlow()
  5. static void RestoreFlow()
  6. static void Run(ExecutionContext executionContext, ContextCallback callback, Object state)

Все эти методы описаны в документации к классу; ключевые особенности:

  1. Capture захватывает текущий контекст в экземпляр своего же класса.

    Здесь стоит остановиться для изучения тела метода (MSDN сокрушает наше мировозрение одним предложением Captures the execution context from the current thread - прям захватывает и пиздец, без компромиссов):

    public static ExecutionContext Capture()
    { 
        // set up a stack mark for finding the caller
        StackCrawlMark stackMark = StackCrawlMark.LookForMyCaller; 
        return ExecutionContext.Capture(ref stackMark, CaptureOptions.None); 
    }
    
    static internal ExecutionContext Capture(ref StackCrawlMark stackMark, CaptureOptions options)
    { 
        ExecutionContext.Reader ecCurrent = Thread.CurrentThread.GetExecutionContextReader();
    
        // check to see if Flow is suppressed 
        if (ecCurrent.IsFlowSuppressed)
            return null; 
    
        //
        // Attempt to capture context.  There may be nothing to capture...
        // 
    
    #if FEATURE_IMPERSONATION || FEATURE_COMPRESSEDSTACK 
        // capture the security context 
        SecurityContext secCtxNew = SecurityContext.Capture(ecCurrent, ref stackMark);
    #endif // #if FEATURE_IMPERSONATION || FEATURE_COMPRESSEDSTACK 
    #if FEATURE_CAS_POLICY
         // capture the host execution context
        HostExecutionContext hostCtxNew = HostExecutionContextManager.CaptureHostExecutionContext();      
    #endif // FEATURE_CAS_POLICY 
    
    #if FEATURE_SYNCHRONIZATIONCONTEXT 
        SynchronizationContext syncCtxNew = null; 
    #endif
        LogicalCallContext logCtxNew = null; 
    
        if (!ecCurrent.IsNull)
        {
    #if FEATURE_SYNCHRONIZATIONCONTEXT 
            // capture the [....] context
            if (0 == (options & CaptureOptions.IgnoreSyncCtx)) 
                syncCtxNew = (ecCurrent.SynchronizationContext == null) ? null : ecCurrent.SynchronizationContext.CreateCopy(); 
    #endif // #if FEATURE_SYNCHRONIZATIONCONTEXT
    
            // copy over the Logical Call Context
            if (ecCurrent.LogicalCallContext.HasInfo)
                logCtxNew = ecCurrent.LogicalCallContext.Clone();
        } 
    
        // 
        // If we didn't get anything but defaults, and we're allowed to return the 
        // dummy default EC, don't bother allocating a new context.
        // 
        if (0 != (options & CaptureOptions.OptimizeDefaultCase) &&
    #if FEATURE_IMPERSONATION || FEATURE_COMPRESSEDSTACK
            secCtxNew == null &&
    #endif 
    #if FEATURE_CAS_POLICY
            hostCtxNew == null && 
    #endif // FEATURE_CAS_POLICY 
    #if FEATURE_SYNCHRONIZATIONCONTEXT
            syncCtxNew == null && 
    #endif // #if FEATURE_SYNCHRONIZATIONCONTEXT
            (logCtxNew == null || !logCtxNew.HasInfo))
        {
            return s_dummyDefaultEC; 
        }
    
        // 
        // Allocate the new context, and fill it in.
        // 
        ExecutionContext ecNew = new ExecutionContext();
    #if FEATURE_IMPERSONATION || FEATURE_COMPRESSEDSTACK
        ecNew.SecurityContext = secCtxNew;
        if (ecNew.SecurityContext != null) 
            ecNew.SecurityContext.ExecutionContext = ecNew;
    #endif 
    #if FEATURE_CAS_POLICY 
        ecNew._hostExecutionContext = hostCtxNew;
    #endif // FEATURE_CAS_POLICY 
    #if FEATURE_SYNCHRONIZATIONCONTEXT
        ecNew._syncContext = syncCtxNew;
    #endif // #if FEATURE_SYNCHRONIZATIONCONTEXT
        ecNew.LogicalCallContext = logCtxNew; 
        ecNew.isNewCapture = true;
    
        return ecNew; 
    }
    

    Без оглядки на все эти #if FEATURE_XYZ происходит следующее:

    1. SecurityContext.Capture(ecCurrent, ref stackMark)

      Класс SecurityContext по структуре и функциям напоминает ExecutionContext, но работает лишь с безопасностью и ее политиками. Лень углубляться сюда, но ключевые аспекты - Impersonation, WindowsIdentity и CAS Security.

    2. HostExecutionContextManager.CaptureHostExecutionContext()

      Производит захват контекста приложения-хоста текущего домена.

    3. ecCurrent.SynchronizationContext.CreateCopy()

      При условии соответствующих опций, текущий SynchronizationContext потока клонируется.

    4. ecCurrent.LogicalCallContext.Clone()

      Если текущий LogicalCallContext не пустой - он также клонируется.

    5. Всяческие оптимизации и сборка всего этого в один объект.
  2. Методы IsFlowSuppressed, SuppressFlow и RestoreFlow позволяют запретить захват контекста с помощью Capture, что внятно описано Рихтером в упомянутой выше книге.
  3. Метод Run, естественно, применяет переданный контекст на текущий поток, исполняет код делегата и в конце восстанавливает контекст до исходного состояния. Применяет - фактически прописывает его в поле m_ExecutionContext класса Thread (да-да, именно там хранится вся эта байда).

Класс ThreadPool содержит еще один метод для помещения рабочей задачи в очередь - UnsafeQueueUserWorkItem. Взглянем на пару похожих методов:

[System.Security.SecuritySafeCritical]  // auto-generated 
[MethodImplAttribute(MethodImplOptions.NoInlining)] // Methods containing StackCrawlMark local var has to be marked non-inlineable
public static bool QueueUserWorkItem(
     WaitCallback           callBack,     // NOTE: we do not expose options that allow the callback to be queued as an APC
     Object                 state 
     )
{ 
    StackCrawlMark stackMark = StackCrawlMark.LookForMyCaller; 
    return QueueUserWorkItemHelper(callBack,state,ref stackMark,true);
} 

[System.Security.SecurityCritical]  // auto-generated_required
[MethodImplAttribute(MethodImplOptions.NoInlining)] // Methods containing StackCrawlMark local var has to be marked non-inlineable 
public static bool UnsafeQueueUserWorkItem(
     WaitCallback           callBack,     // NOTE: we do not expose options that allow the callback to be queued as an APC 
     Object                 state 
     )
{ 
    StackCrawlMark stackMark = StackCrawlMark.LookForMyCaller;
    return QueueUserWorkItemHelper(callBack,state,ref stackMark,false);
}

Разница в поведении - параметр compressStack, он влияет на захват и развертывание контекста. Метод UnsafeQueueUserWorkItem не оперирует контекстом - не сохраняет его как часть рабочего элемента очереди, не восстанавливает перед исполнением задачи. Также отключить перенос контекста можно выполнив ThreadPool.SuppressFlow(). Но здесь есть небольшой нюанс - оба метода отмечены как [SecurityCritical], что требует Full Trust у вызывающего метода. Безопасность здесь на высоте - код без Full Trust не может использовать пул для выполнения задач вне собственного контекста. Те же ограничения касаются других классов, например SecurityContext.

Разработчик обязан знать о наличии пар таких методов и разнице между ними. ThreadPool не так прост как кажется, затраты его обслуживания могут оказаться выше затрат исполнения элементов очереди.

Где еще используется ExecutionContext?

  • System.Threading.Thread
  • System.Threading.Timer
  • System.Windows.Forms.Control
  • System.Windows.Threading.DispatcherOperation
  • Классы семейства Task Parallel Library
  • System.IO.Stream
  • System.Net.Sockets.Socket
  • System.Data.SqlClient.SqlDataReader
  • И многие другие классы с асинхронными операциями.

У перечисления CaptureOptions есть элемент IgnoreSyncCtx. Он предотвращает захват и клонирование SynchronizationContext следующими классами:

  • System.IO.Stream
  • System.Threading.Overlapped, который используется многими классами с поддержкой Overlapped IO - Socket, MessageQueue, Pipe, PipeStream, HttpListener, HttpListenerRequest и т.д.
  • System.Threading.Tasks.*
  • System.Threading.Thread
  • System.Threading.ThreadPool
  • System.Threading.Timer
  • System.Runtime.CompilerServices.AsyncTaskMethodBuilder
  • System.Runtime.CompilerServices.AsyncVoidMethodBuilder

CaptureOptions могут использовать лишь классы самой mscorlib.dll, соответствующие методы объявлены как internal. Пользовательский код может захватить лишь весь контекст целиком.

Теперь настало время для двух важных граждан ExecutionContext - LogicalCallContext и IllogicalCallContext из пространства имен (внимание!) System.Runtime.Remoting.Messaging. Сразу напишу, что честных механизмов получить сами эти объекты нет, все спрятано как internal. Для чтения и модификации этих контейнеров нам с барского плеча подарили класс CallContext со следующими операциями (статическими):

  1. void FreeNamedDataSlot(String name) - удаляет элемент из контейнера с соответствующим именем.
  2. Object LogicalGetData(String name) - читает значение из текущего LogicalCallContext.
  3. void LogicalSetData(String name, Object data) - записывает туда значение.
  4. Object GetData(String name) - делегирует в первую очередь в LogicalGetData, а в случае неудачи читает из текущего IllogicalCallContext.
  5. void SetData(String name, Object data) - здесь немного интересней:
    [System.Security.SecurityCritical]  // auto-generated 
    public static void SetData(String name, Object data)
    { 
        if (data is ILogicalThreadAffinative) 
        {
            LogicalSetData(name, data); 
        }
        else
        {
            ExecutionContext ec = Thread.CurrentThread.GetMutableExecutionContext(); 
            ec.LogicalCallContext.FreeNamedDataSlot(name);
            ec.IllogicalCallContext.SetData(name, data); 
        } 
    }
    

    Значение проверяется на интерфейс-маркер ILogicalThreadAffinative, после вызов делегируется в LogicalCallContext (маркер присутствует) или IllogicalCallContext (маркер отсутствует).

  6. Пара методов Header[] GetHeaders() иvoid SetHeaders(Header[] headers) - оперируют заголовками типа Header, внеполосные данные, которые исполняемая среда присоединяет к запросу на вызов метода из другого контекста.

Теперь рассмотрим классы, методы и всеразличные манипуляции, связанные с этими контейнерами:

  • Пара методов BeginInvoke/EndInvoke у классов-делегатов.

    Да-да, нубасики, эти методы не так просты какими они кажутся. Во-первых, код Invoke, BeginInvoke и EndInvoke порождает JIT компилятор во время выполнения (в сборках эти методы отмечены атрибутом [MethodImpl(0, MethodCodeType=MethodCodeType.Runtime)]):

    0:000> !DumpMT -md 000007fef1c680c0
    EEClass:         000007fef15c45d8
    Module:          000007fef15c1000
    Name:            System.Action
    mdToken:         0000000002000015
    File:            C:\Windows\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
    BaseSize:        0x40
    ComponentSize:   0x0
    Slots in VTable: 16
    Number of IFaces in IFaceMap: 2
    --------------------------------------
    MethodDesc Table
               Entry       MethodDesc    JIT Name
    000007fef1a2c480 000007fef15ca9d0 PreJIT System.Object.ToString()
    000007fef1ab75e0 000007fef16a1898 PreJIT System.MulticastDelegate.Equals(System.Object)
    000007fef1a36c40 000007fef16a1908 PreJIT System.MulticastDelegate.GetHashCode()
    000007fef1aad790 000007fef15caa18 PreJIT System.Object.Finalize()
    000007fef1abeb70 000007fef16a7c38 PreJIT System.Delegate.DynamicInvokeImpl(System.Object[])
    000007fef1b24010 000007fef16a18e0 PreJIT System.MulticastDelegate.GetInvocationList()
    000007fef19ba2c8 000007fef16a1918 PreJIT System.MulticastDelegate.GetMethodImpl()
    000007fef1ab70d0 000007fef16a18d0 PreJIT System.MulticastDelegate.CombineImpl(System.Delegate)
    000007fef1ab7450 000007fef16a18d8 PreJIT System.MulticastDelegate.RemoveImpl(System.Delegate)
    000007fef2287eb0 000007fef16a7ce0 PreJIT System.Delegate.Clone()
    000007fef22886e0 000007fef16a1890 PreJIT System.MulticastDelegate.GetObjectData(System.Runtime.Serialization.SerializationInfo, System.Runtime.Serialization.StreamingContext)
    000007fef1a2eb30 000007fef16a1910 PreJIT System.MulticastDelegate.GetTarget()
    000007fef19f08c8 000007fef16ad2d0   NONE System.Action.Invoke()
    000007fef19f08a0 000007fef16ad2e8   NONE System.Action.BeginInvoke(System.AsyncCallback, System.Object)
    000007fef19f08b0 000007fef16ad300   NONE System.Action.EndInvoke(System.IAsyncResult)
    000007fef19f08c0 000007fef16ad2b8   NONE System.Action..ctor(System.Object, IntPtr)
    

    В результате, например, код

    Action action = () => Thread.Yield();
    var asyncResult = action.BeginInvoke(nullnull);
    action.EndInvoke(asyncResult);
    return;

    приводит к следующему стеку вызовов:

    (MethodDesc 000007fef15cff58 +0x21 System.Threading.ThreadPool.QueueUserWorkItem(System.Threading.WaitCallback, System.Object)), calling (MethodDesc 000007fef15d01c0 +0 System.Threading.ThreadPool.QueueUserWorkItemHelper(System.Threading.WaitCallback, System.Object, System.Threading.StackCrawlMark ByRef, Boolean))
    (MethodDesc 000007fef1796e78 +0x305 System.Runtime.Remoting.Proxies.RemotingProxy.Invoke(System.Object, System.Runtime.Remoting.Proxies.MessageData ByRef)), calling (MethodDesc 000007fef15cff58 +0 System.Threading.ThreadPool.QueueUserWorkItem(System.Threading.WaitCallback, System.Object))
    clr!CTPMethodTable__CallTargetHelper3+0x12
    clr!MemberLoader::FM_GetStrCompFunc+0x8b, calling clr!CTPMethodTable__CallTargetHelper3
    clr!MethodDesc::IsCtor+0x12, calling clr!MethodDesc::GetAttrs
    clr!CTPMethodTable::OnCall+0x1ce, calling clr!MemberLoader::FM_GetStrCompFunc+0x1c
    clr!CTPMethodTable::PreCall+0x6a, calling clr!TPMethodFrame::GetSlotNumber
    clr!TransparentProxyStub_CrossContextPatchLabel+0xa, calling clr!CTPMethodTable::OnCall
    clr!ThePreStub+0x5a, calling clr!PreStubWorker
    (MethodDesc 000007fe93493968 +0x75 ConsoleApplication.Program1.Main(System.String[])), calling 000007fef19f08a0 (stub for System.Action.BeginInvoke(System.AsyncCallback, System.Object))
    

    Или в упрощенной форме:

    System.Threading.ThreadPool.QueueUserWorkItem(System.Threading.WaitCallback, System.Object)
    System.Runtime.Remoting.Proxies.RemotingProxy.Invoke(System.Object, System.Runtime.Remoting.Proxies.MessageData ByRef)
    [TPMethodFrame: 00000000005febb8] System.Action.BeginInvoke(System.AsyncCallback, System.Object)
    ConsoleApplication.Program1.Main(System.String[]) [d:\Development\Projects\CLRExecutionContext\ConsoleApplication\Program.cs @ 52]
    

    Обратите внимание на обилие всяких ТэПэ в стеках. Открыв файл remotingproxy.cs, можно обнаружить следующий метод:

    // Invoke for case where call is in the same context as the server object
    // (This special static method is used for AsyncDelegate-s ... this is called 
    // directly from the EE) 
    private static void Invoke(Object NotUsed, ref MessageData msgData)
    

    И такой незамысловатый его код:

    case Message.BeginAsync:
    case Message.BeginAsync | Message.OneWay: 
        // pick up call context from the thread 
        m.Properties[Message.CallContextKey] =
            Thread.CurrentThread.GetMutableExecutionContext().LogicalCallContext.Clone();
        ar = new AsyncResult(m);
        AgileAsyncWorkerItem  workItem =
            new AgileAsyncWorkerItem(
                    m, 
                    ((callType & Message.OneWay) != 0) ?
                        null : ar, d.Target); 
    
        ThreadPool.QueueUserWorkItem(
            new WaitCallback( 
                    AgileAsyncWorkerItem.ThreadPoolCallBack),
            workItem);
    
        if ((callType & Message.OneWay) != 0) 
        {
            ar.SyncProcessMessage(null); 
        } 
        m.PropagateOutParameters(null, ar);
        break; 
    case (Message.EndAsync | Message.OneWay):
        return;
    
    case Message.EndAsync: 
        // This will also merge back the call context
        // onto the thread that called EndAsync 
        RealProxy.EndInvokeHelper(m, false);
        break;
    

    AgileAsyncWorkerItem находится в том же файле:

    internal class AgileAsyncWorkerItem 
    {
        private IMethodCallMessage _message; 
        private AsyncResult        _ar;
        private Object             _target;
    
        [System.Security.SecurityCritical]  // auto-generated 
        public AgileAsyncWorkerItem(IMethodCallMessage message, AsyncResult ar, Object target)
        { 
            _message = new MethodCall(message); 
            _ar = ar;
            _target = target; 
        }
    
        [System.Security.SecurityCritical]  // auto-generated
        public static void ThreadPoolCallBack(Object o) 
        {
            ((AgileAsyncWorkerItem) o).DoAsyncCall(); 
        } 
    
    
        [System.Security.SecurityCritical]  // auto-generated
        public void DoAsyncCall()
        {
            (new StackBuilderSink(_target)).AsyncProcessMessage(_message, _ar); 
        }
    }
    

    Как видите, для выполнения вызова используется класс StackBuilderSink, метод AsyncProcessMessage, который применяет LogicalCallContext на поток выполнения:

    LogicalCallContext callCtx =  (LogicalCallContext) 
        mcMsg.Properties[Message.CallContextKey];
    
    ...
    
    // install call context onto the thread, holding onto 
    // the one that is currently on the thread 
    
    oldCallCtx = CallContext.SetLogicalCallContext(callCtx); 
    isCallContextSet = true;
    

    Обратите внимение как метод void Invoke(Object NotUsed, ref MessageData msgData) возвращает экземпляр IAsyncResult с помощью m.PropagateOutParameters(null, ar); он объявлен в файле RealProxy.cs:

    [MethodImplAttribute(MethodImplOptions.InternalCall)]
    public extern void PropagateOutParameters(Object[] OutArgs, Object retVal); 
    

    EndInvoke приводит к вызову RealProxy.EndInvokeHelper(m, false), который выполняет слияние текущего контекста и удаленного:

    // Merge the call context back into the thread that 
    // called EndInvoke
    Thread.CurrentThread.GetMutableExecutionContext().LogicalCallContext.Merge( 
        mrm.LogicalCallContext); 
    

    Из-за всего этого производительность предложенной APM модели для делегатов может, мягко говоря, не порадовать любителей многопоточного программирования. Уверен, польза от LogicalCallContext в одном случае из миллиона, а страдают все. Браво, Microsoft! Особенно порадовала эта бредятина в стеке вместе с PropagateOutParameters.

  • Семейство классов пространства ммен System.Runtime.Remoting (в котором и объявлен LogicalCallContext), естественно, используют его в любых удаленных операциях, поддерживаемых подсистемой Remoting. Вспоминаем ContextBoundObject, __TransparentProxy, всевозможные XyzSink и пр.

На этом пока все. До новых встреч, мой любимый бред bread!

Visual Studio 2012 Update 2 Now Available

How to create useful process dump

Step by step turorial for How to Create Useful Dumps of Running Processes:

  1. Download http://technet.microsoft.com/en-us/sysinternals/dd996900
  2. Open command prompt as administrator
  3. Run 'procdump ApplicationToDump.exe -ma X:\Path\To\Dumps\Folder' OR 'procdump %PROCESS_ID% -ma X:\Path\To\Dumps\Folder'

Dumps created by Task Manager, Process Explorer or other user friendly utilities lack important information like handle table.

Copyright 2007-2011 Chabster