516 words
3 minutes
Playing with the CPU Usage Curve

In the book “The Beauty Of Programming”, which talks about Microsoft interview questions, there is a interesting section: Control the CPU curve of Windows Task Manager.

image

The above image is from the book.

Nowadays, when someone buys a mainstream CPU, it should be dual core by default. My laptop has a quad core Q9000 CPU.

Control the CPU curve in a multi-core CPU by make a thread spin / sleep is different from solo core CPU. For example, the spin a thread cause 100% CPU usage on solo core CPU, but cause 50% CPU usage in a dual core CPU.

image

Thread affinity#

On a multi-core CPU, Windows shares time-slice from a random core to the thread. This AssignCurrentThreadInCpu() method is defined to help assign a thread to a specified CPU:

internal static class NativeMethods
{
public static void AssignCurrentThreadInCpu(int cpuIndex)
{
SetThreadAffinityMask(GetCurrentThread(), new IntPtr(1 << cpuIndex));
}
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
internal static extern IntPtr SetThreadAffinityMask(
IntPtr hThread,
IntPtr dwThreadAffinityMask);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
internal static extern IntPtr GetCurrentThread();
}

Draw curve on specified CPU#

This reusable method is used to draw CPU curve:

private static void DrawCpu(
int timePerPeriod,
int timePerFrame, Func<int, int, double> getCpuUsage)
{
if (timePerFrame <= 0)
{
throw new ArgumentOutOfRangeException("timePerPeriod");
}
if (timePerFrame <= 0)
{
throw new ArgumentOutOfRangeException("timePerFrame");
}
int frameCountPerPeriod = timePerPeriod / timePerFrame;
if (frameCountPerPeriod < 1)
{
throw new InvalidOperationException();
}
while (true)
{
for (int frameIndex = 0; frameIndex < frameCountPerPeriod; frameIndex++)
{
// If the target CPU usage is 70%,
double cpuUsage = getCpuUsage(frameIndex, frameCountPerPeriod);
if (cpuUsage < 0 || cpuUsage > 1)
{
throw new InvalidOperationException();
}
// the thread spins for 70% of the time,
double busyTimePerFrame = timePerFrame * cpuUsage;
double busyStartTime = Environment.TickCount;
while (Environment.TickCount - busyStartTime <= busyTimePerFrame)
{
}
// and sleeps for the rest 30% of time.
int idleTimePerFrame = (int)(timePerFrame - busyTimePerFrame);
Thread.Sleep(idleTimePerFrame);
}
}
}

It takes an Func<int, int, double> parameter (x, y) => z to calculate that, in one period, at the xth frame of the total y frames, the cpu usage should be z.

Now it is ready to draw a specific curve on a specific CPU:

private static void Main()
{
Thread thread0 = new Thread(() =>
{
NativeMethods.AssignCurrentThreadInCpu(0);
DrawCpu(
20 * 1000, // One period is 20 seconds.
500, // One frame takes 0.5 seconds.
(index, count) => // Calculates the CPU usage.
Math.Sin((2 * Math.PI) * ((double)index / count)) / 2 + 0.5);
});
Thread thread1 = new Thread(() =>
{
NativeMethods.AssignCurrentThreadInCpu(1);
DrawCpu(20 * 1000, 500, (index, count) => 0.5);
});
Thread thread2 = new Thread(() =>
{
NativeMethods.AssignCurrentThreadInCpu(2);
DrawCpu(
20 * 1000,
500,
(index, count) => (double)index / (count - 1));
});
Thread thread3 = new Thread(() =>
{
NativeMethods.AssignCurrentThreadInCpu(3);
DrawCpu(
20 * 1000,
500,
(index, count) => index < count / 2 ? 0 : 1);
});
thread0.Start();
thread1.Start();
thread2.Start();
thread3.Start();
Console.Read(); // Exits.
thread0.Abort();
thread1.Abort();
thread2.Abort();
thread3.Abort();
}

Running the above code draws the following curves in Task Manager:

image

This solution has an flaw that, it assumes one managed thread runs on one Windows thread. This solution is not 100% stable because a managed thread can also run on a fiber.

Playing with the CPU Usage Curve
https://dixin.github.io/posts/playing-with-the-cpu-usage-curve/
Author
Dixin
Published at
2010-04-11
License
CC BY-NC-SA 4.0