9

Update - I have found the cause of lock() eating CPU cycles like crazy. I added this information after my original question. This all turned out to be a wall of text so:

TL;DR The c# built-in lock() mechanism will, under some circumstances, use an unusual amount of CPU time if your system is running with a high resolution system timer.

Original question:

I have an application that accesses a resource from multiple threads. The resource is a device attached to USB. Its a simple command/response interface and I use a small lock() block to ensure that the thread that sends a command also gets the response. My implementation uses the lock(obj) keyword:

lock (threadLock)
{
    WriteLine(commandString);
    rawResponse = ReadLine();
}

When I access this from 3 threads as fast as possible (in a tight loop) the CPU usage is about 24% on a high-end computer. Due to the nature of the USB port only about 1000 command/response operations are performed per second. Then I implemented the lock mechanism described here SimpleExclusiveLock and the code now looks similar to this (some try/catch stuff to release the lock in case of an I/O exception is removed):

Lock.Enter();
WriteLine(commandString);
rawResponse = ReadLine();
Lock.Exit();

Using this implementation the CPU usage drops to <1% with the same 3 thread test program while still getting the 1000 command/response operations per second.

The question is: What, in this case, is the problem using the built-in lock() keyword?

Have I accidentally stumbled upon a case where the lock() mechanism has exceptionally high overhead? The thread that enters the critical section will hold the lock for only about 1 ms.

Update: The cause of lock() eating CPU like crazy is that some application has increased the timer resolution for the whole system using timeBeginPeriod() in winmm.dll. The culprits in my case are Google Chrome and SQL Server - they requested a 1 ms system timer resolution using:

[DllImport("winmm.dll", EntryPoint = "timeBeginPeriod", SetLastError = true)]
private static extern uint TimeBeginPeriod(uint uMilliseconds);

I found this out by using the powercfg tool:

powercfg -energy duration 5 

Due to some sort of design flaw in the built-in lock() statement this increased timer resolution eats CPU like crazy (at least in my case). So, I killed the programs that request high resolution system timer. My application now runs a bit slower. Each request will now lock for 16.5 ms instead of 1 ms. The reason behind that I guess is that the threads are scheduled less frequently. The CPU usage (as shown in Task Manager) also dropped to zero. I have no doubt that lock() still uses quite a few cycles but that is now hidden.

In my project low CPU use is an important design factor. The low 1 ms latency of USB requests are also positive for the overall design. So (in my case) the solution is to discard the built-in lock() and replace it with a properly implemented lock mechanism. I already threw out the flawed System.IO.Ports.SerialPort in favor of WinUSB so I have no fears :)

I made a small console-application to demonstrate all of this, pm me if you are interested in a copy (~100 lines of code).

I guess I answered my own question so I´ll just leave this here in case someone is interested...

4
  • 2
    In intrigues me that the linked article uses Semaphore rather than lock for the waiting; that should be more expensive (it needs to go to the OS layer, etc). Incidentally, "only about 1ms": 1ms is a very long time to a computer. Commented Apr 2, 2014 at 8:54
  • Oh yes Marc, the "only 1 ms" came from the assumption that 1 ms is a short time for the thread scheduler (afaik) Commented Apr 2, 2014 at 9:08
  • Henk, the amount of work is small. But the "device" is a complex machine. I was under the impression that I could create a set of threads to control various (separate) parts of the machine without any unnecessary waits. Only 1000 control I/O operations are possible per second so most threads will be waiting to acquire the lock. Apparently, this is bad design using the built-in lock() but good design if I switch to a cheaper lock mechanism :) Its almost like the built in lock() uses some sort of busy wait? Commented Apr 2, 2014 at 9:22
  • 1
    I suppose it's possible that the lock is somehow causing a thread context switch, and that the context switches are eating the CPU. Replace the lock (threadLock) with a SpinLock, and see what the results are. I suspect you'll get results similar to your use of SimpleExclusiveLock. The spinning prevents a context switch unless the wait is exceptionally long. Commented Apr 2, 2014 at 16:38

1 Answer 1

5

No, sorry, this is not possible. There's no scenario where you have 3 threads with 2 of them blocking on the lock and 1 blocking on an I/O operation that takes a millisecond can get you 24% cpu utilization. The linked article is perhaps interesting, but the .NET Monitor class does the exact same thing. Including the CompareExchange() optimization and the wait queue.

The only way you can get to 24% is through other code that runs in your program. With the common cycle stealer being the UI thread that you pummel a thousand times per second. Very easy to burn core that way. A classic mistake, human eyes can't read that fast. With the further extrapolation that you then wrote a test program that doesn't update UI. And thus doesn't burn core.

A profiler will of course tell you exactly where those cycles go. It should be your next step.

Sign up to request clarification or add additional context in comments.

2 Comments

The profiler shows that Monitor.Enter uses an exceptionally high amount of cycles. This is probably due to my system running with 1 ms thread scheduling combined with some sort of design flaw in lock(). I added my findings in the original question.
There is nothing special about "1 ms thread scheduling", just use Chrome. It also calls timeBeginPeriod(1). Lots of SO users like to browse SO with Chrome, they never complain about a bug in Monitor.Enter or lock. I'm not buying. You need to take your assertion to Microsoft Support.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.