Windows 计时器队列计时器在 .NET 断点后继续
Windows Timer Queue Timer continues after .NET breakpoint
我开发了一个 .NET 应用程序,它必须轮询 120 Hz 的压力设备。标准 .NET 计时器似乎只能达到 60 Hz,因此我决定通过 PInvoke 使用 Win32 CreateTimerQueueTimer API。它工作得很好,但调试体验非常糟糕,因为当我在程序暂停时单步执行程序时甚至会触发计时器。我在 C 和 C# 中写了一个最小的例子,不希望的行为只发生在 C# 上。当调试器暂停程序时,C 程序不会创建计时器回调线程。谁能告诉我,我可以做些什么来在 C# 中实现相同的调试行为?
C代码:
#include <stdio.h>
#include <assert.h>
#include <Windows.h>
int counter = 0;
VOID NTAPI callback(PVOID lpParameter, BOOLEAN TimerOrWaitFired)
{
printf("Just in time %i\n", counter++);
}
int main()
{
HANDLE timer;
BOOL success = CreateTimerQueueTimer(&timer, NULL, callback, NULL, 0, 1000, WT_EXECUTEDEFAULT);
assert(FALSE != success); // set breakpoint at this line and wait 10 seconds
Sleep(1000);
success = DeleteTimerQueueTimer(NULL, timer, NULL); // step to this line
assert(FALSE != success);
return 0;
}
等效的 C# 代码:
using System;
using System.Runtime.InteropServices;
class TimerQueue
{
delegate void WAITORTIMERCALLBACK(IntPtr lpParameter, bool TimerOrWaitFired);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool CreateTimerQueueTimer(
out IntPtr phNewTimer,
IntPtr TimerQueue,
WAITORTIMERCALLBACK Callback,
IntPtr Parameter,
uint DueTime,
uint Period,
uint Flags);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool DeleteTimerQueueTimer(
IntPtr TimerQueue,
IntPtr Timer,
IntPtr CompletionEvent);
static int counter = 0;
static void Callback(IntPtr lpParameter, bool TimerOrWaitFired)
{
Console.WriteLine("Just in time {0}", counter++);
}
static void Main(string[] args)
{
WAITORTIMERCALLBACK callbackWrapper = Callback;
IntPtr timer;
bool success = CreateTimerQueueTimer(out timer, IntPtr.Zero, callbackWrapper, IntPtr.Zero, 0, 1000, 0);
System.Diagnostics.Debug.Assert(false != success); // set breakpoint at this line and wait 10 seconds
System.Threading.Thread.Sleep(1000);
success = DeleteTimerQueueTimer(IntPtr.Zero, timer, IntPtr.Zero); // step to this line
System.Diagnostics.Debug.Assert(false != success);
}
}
顺便说一句,我知道从多个线程使用不受保护的计数器变量时会出现竞争条件,现在这并不重要。
休眠一秒钟意味着与断点命中后的等待无关,这似乎是必要的,因为回调不会立即在进程中排队,即使在调试器中步进程序时也是如此,但只是在短暂的延迟之后。
对 DeleteTimerQueueTimer 的调用并不是显示我的问题所必需的,因为它发生在执行此行之前。
您应该能够进入调试->Windows->线程windows并在单步执行时冻结所有线程。
由于C语言可以直接访问系统(kernel32)的函数,所以相对于C#来说更直接,访问也更不安全。 C# 在 运行 时编译,所有对外界的调用(DLL/COM),它必须使用 OS (Windows).
提供的 Wrapper
正如 PhillipH 所写:在断点期间检查线程 window。在 C# 代码中,您会看到像 RunParkingWindow
和 .NET SystemEvent
这样的线程。
与 C 程序相比,只有 1 个线程 - 您刚刚停止调试的线程。
意识到这一点你会明白,调试中的断点只会停止你当前所在的线程,但其他线程会进一步运行。这也意味着 Wrappers provided by OS
仍在监听任何事件并将它们排入队列。一旦您的应用程序继续 运行,他们将全力以赴,回调方法将完成剩下的工作。
我试图找到任何有数据流的相关文章/图片,但我没有成功。请将以上文字视为意见而非技术段落。
我记得我 4 年前问过这个问题,我想我现在知道答案了:.NET 中的计时器 classes 在内部只使用一次性计时器。因此,当您在断点处暂停程序时,重新设置一次性计时器的代码不会 运行 因此在您让 program/debugger 继续之前不会有回调堆积。
几个月前,我编写了自己的定时器 class,它可以达到高达 1000 Hz 的回调频率。它使用重复设置一次性定时器和忙等待的组合。忙等待会消耗 CPU 个周期,但有助于减少 Windows 线程调度程序引入的抖动。
代码如下:
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
/// <summary>
/// Executes a function periodically.
/// Timer expirations are not queued during debug breaks or
/// if the function takes longer than the period.
/// </summary>
/// <remarks>
/// Uses a Windows one-shot timer together
/// with busy waiting to achieve good accuracy
/// at reasonable CPU usage.
/// Works even on Windows 10 Version 2004.
/// </remarks>
public sealed class Timer : IDisposable
{
/// <summary>
/// Statistics:
/// How much time was 'wasted' for busy waiting.
/// Does not include time passed by using a system timer.
/// </summary>
public TimeSpan TimeSpentWithBusyWaiting => new TimeSpan(Interlocked.Read(ref timeSpentWithBusyWaiting));
private long timeSpentWithBusyWaiting = 0;
private readonly TimeCaps timeCaps;
private readonly Func<bool> function;
private readonly TimeSpan period;
/// <summary>
/// We must keep a reference to this callback so that it does not get garbage collected.
/// </summary>
private readonly TIMECALLBACK callback;
private readonly Stopwatch stopwatch = new Stopwatch();
private volatile bool stopTimer = false;
private ManualResetEvent timerStopped = new ManualResetEvent(false);
private TimeSpan desiredNextTime;
/// <summary>
/// Immediately starts the timer.
/// </summary>
/// <param name="function">
/// What to do after each <paramref name="period"/>.
/// The timer will stop if the function returns <see langword="false"/>.
/// </param>
/// <param name="period">
/// How long to wait between executing each <paramref name="function"/>.
/// </param>
public Timer(Func<bool> function, TimeSpan period)
{
uint? timerDelay = TimeSpanToPositiveMillisecondsWithRoundingDown(period);
if (timerDelay == null)
{
throw new ArgumentOutOfRangeException(nameof(period));
}
uint timeGetDevCapsErrorCode = TimeGetDevCaps(out timeCaps, (uint)Marshal.SizeOf(typeof(TimeCaps)));
if (timeGetDevCapsErrorCode != 0)
{
throw new Exception($"{nameof(TimeGetDevCaps)} returned error code {timeGetDevCapsErrorCode}");
}
Debug.Assert(timeCaps.wPeriodMin >= 1);
this.function = function ?? throw new ArgumentNullException(nameof(function));
this.period = period;
callback = new TIMECALLBACK(OnTimerExpired);
TimeBeginPeriod(timeCaps.wPeriodMin);
stopwatch.Start();
Schedule(desiredNextTime = period, forceTimer: true);
}
/// <summary>
/// Does not cancel the timer and instead
/// blocks until the function passed to
/// the constructor returns <see langword="false"/>.
/// </summary>
public void WaitUntilFinished()
{
if (timerStopped != null)
{
timerStopped.WaitOne();
timerStopped.Dispose();
timerStopped = null;
TimeEndPeriod(timeCaps.wPeriodMin);
}
}
/// <summary>
/// Stops timer and blocks until the
/// last invocation of the function has finished.
/// </summary>
public void Dispose()
{
stopTimer = true;
WaitUntilFinished();
}
private void OnTimerExpired(
uint uTimerID,
uint uMsg,
UIntPtr dwUser,
UIntPtr dw1,
UIntPtr dw2)
{
while (!stopTimer)
{
TimeSpan startOfBusyWaiting = stopwatch.Elapsed;
TimeSpan endOfBusyWaiting = desiredNextTime;
TimeSpan timeThatWillBeSpentWithBusyWaiting = endOfBusyWaiting - startOfBusyWaiting;
if (timeThatWillBeSpentWithBusyWaiting > TimeSpan.Zero)
{
Interlocked.Add(ref timeSpentWithBusyWaiting, timeThatWillBeSpentWithBusyWaiting.Ticks);
}
if (desiredNextTime > stopwatch.Elapsed)
{
while (desiredNextTime > stopwatch.Elapsed)
{
// busy waiting until time has arrived
}
desiredNextTime += period;
}
else
{
// we are too slow
desiredNextTime = stopwatch.Elapsed + period;
}
bool continueTimer = function();
if (continueTimer)
{
if (Schedule(desiredNextTime, forceTimer: false))
{
return;
}
}
else
{
stopTimer = true;
}
}
timerStopped.Set();
}
/// <param name="desiredNextTime">
/// Desired absolute time for next execution of function.
/// </param>
/// <param name="forceTimer">
/// If <see langword="true"/>, a one-shot timer will be used even if
/// <paramref name="desiredNextTime"/> is in the past or too close to
/// the system timer resolution.
/// </param>
/// <returns>
/// <see langword="true"/> if timer was set or <paramref name="forceTimer"/> was <see langword="true"/>.
/// <see langword="false"/> if <paramref name="desiredNextTime"/> was in the past.
/// </returns>
/// <remarks>
/// Increases accuracy by scheduling the timer a little earlier and
/// then do busy waiting outside of this function.
/// </remarks>
private bool Schedule(TimeSpan desiredNextTime, bool forceTimer)
{
TimeSpan currentTime = stopwatch.Elapsed;
TimeSpan remainingTimeUntilNextExecution = desiredNextTime - currentTime;
uint? timerDelay = TimeSpanToPositiveMillisecondsWithRoundingDown(remainingTimeUntilNextExecution);
timerDelay =
timerDelay < timeCaps.wPeriodMin ? timeCaps.wPeriodMin :
timerDelay > timeCaps.wPeriodMax ? timeCaps.wPeriodMax :
timerDelay;
if (forceTimer && timerDelay == null)
{
timerDelay = timeCaps.wPeriodMin;
}
if (forceTimer || timerDelay >= timeCaps.wPeriodMin)
{
// wait until next execution using a one-shot timer
uint timerHandle = TimeSetEvent(
timerDelay.Value,
timeCaps.wPeriodMin,
callback,
UIntPtr.Zero,
0);
if (timerHandle == 0)
{
throw new Exception($"{nameof(TimeSetEvent)} failed");
}
return true;
}
else // use busy waiting
{
return false;
}
}
/// <returns><see langword="null"/> if <paramref name="timeSpan"/> is negative</returns>
private static uint? TimeSpanToPositiveMillisecondsWithRoundingDown(TimeSpan timeSpan)
{
if (timeSpan.Ticks >= 0)
{
long milliseconds = timeSpan.Ticks / TimeSpan.TicksPerMillisecond;
if (milliseconds <= uint.MaxValue)
{
return unchecked((uint)milliseconds);
}
}
return null;
}
private delegate void TIMECALLBACK(
uint uTimerID,
uint uMsg,
UIntPtr dwUser,
UIntPtr dw1,
UIntPtr dw2);
// https://docs.microsoft.com/en-us/previous-versions//dd757634(v=vs.85)
//
// This is the only timer API that seems to work for frequencies
// higher than 60 Hz on Windows 10 Version 2004.
//
// The uResolution parameter has the same effect as
// using the timeBeginPeriod API, so it can be observed
// by entering `powercfg.exe /energy /duration 1` into
// a command prompt with administrator privileges.
[DllImport("winmm", EntryPoint = "timeSetEvent")]
private static extern uint TimeSetEvent(
uint uDelay,
uint uResolution,
TIMECALLBACK lpTimeProc,
UIntPtr dwUser,
uint fuEvent);
[DllImport("winmm", EntryPoint = "timeBeginPeriod")]
private static extern uint TimeBeginPeriod(
uint uPeriod);
[DllImport("winmm", EntryPoint = "timeEndPeriod")]
private static extern uint TimeEndPeriod(
uint uPeriod);
[DllImport("winmm", EntryPoint = "timeGetDevCaps")]
private static extern uint TimeGetDevCaps(
out TimeCaps ptc,
uint cbtc);
[StructLayout(LayoutKind.Sequential)]
private struct TimeCaps
{
public uint wPeriodMin;
public uint wPeriodMax;
}
}
我开发了一个 .NET 应用程序,它必须轮询 120 Hz 的压力设备。标准 .NET 计时器似乎只能达到 60 Hz,因此我决定通过 PInvoke 使用 Win32 CreateTimerQueueTimer API。它工作得很好,但调试体验非常糟糕,因为当我在程序暂停时单步执行程序时甚至会触发计时器。我在 C 和 C# 中写了一个最小的例子,不希望的行为只发生在 C# 上。当调试器暂停程序时,C 程序不会创建计时器回调线程。谁能告诉我,我可以做些什么来在 C# 中实现相同的调试行为?
C代码:
#include <stdio.h>
#include <assert.h>
#include <Windows.h>
int counter = 0;
VOID NTAPI callback(PVOID lpParameter, BOOLEAN TimerOrWaitFired)
{
printf("Just in time %i\n", counter++);
}
int main()
{
HANDLE timer;
BOOL success = CreateTimerQueueTimer(&timer, NULL, callback, NULL, 0, 1000, WT_EXECUTEDEFAULT);
assert(FALSE != success); // set breakpoint at this line and wait 10 seconds
Sleep(1000);
success = DeleteTimerQueueTimer(NULL, timer, NULL); // step to this line
assert(FALSE != success);
return 0;
}
等效的 C# 代码:
using System;
using System.Runtime.InteropServices;
class TimerQueue
{
delegate void WAITORTIMERCALLBACK(IntPtr lpParameter, bool TimerOrWaitFired);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool CreateTimerQueueTimer(
out IntPtr phNewTimer,
IntPtr TimerQueue,
WAITORTIMERCALLBACK Callback,
IntPtr Parameter,
uint DueTime,
uint Period,
uint Flags);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool DeleteTimerQueueTimer(
IntPtr TimerQueue,
IntPtr Timer,
IntPtr CompletionEvent);
static int counter = 0;
static void Callback(IntPtr lpParameter, bool TimerOrWaitFired)
{
Console.WriteLine("Just in time {0}", counter++);
}
static void Main(string[] args)
{
WAITORTIMERCALLBACK callbackWrapper = Callback;
IntPtr timer;
bool success = CreateTimerQueueTimer(out timer, IntPtr.Zero, callbackWrapper, IntPtr.Zero, 0, 1000, 0);
System.Diagnostics.Debug.Assert(false != success); // set breakpoint at this line and wait 10 seconds
System.Threading.Thread.Sleep(1000);
success = DeleteTimerQueueTimer(IntPtr.Zero, timer, IntPtr.Zero); // step to this line
System.Diagnostics.Debug.Assert(false != success);
}
}
顺便说一句,我知道从多个线程使用不受保护的计数器变量时会出现竞争条件,现在这并不重要。
休眠一秒钟意味着与断点命中后的等待无关,这似乎是必要的,因为回调不会立即在进程中排队,即使在调试器中步进程序时也是如此,但只是在短暂的延迟之后。
对 DeleteTimerQueueTimer 的调用并不是显示我的问题所必需的,因为它发生在执行此行之前。
您应该能够进入调试->Windows->线程windows并在单步执行时冻结所有线程。
由于C语言可以直接访问系统(kernel32)的函数,所以相对于C#来说更直接,访问也更不安全。 C# 在 运行 时编译,所有对外界的调用(DLL/COM),它必须使用 OS (Windows).
提供的 Wrapper正如 PhillipH 所写:在断点期间检查线程 window。在 C# 代码中,您会看到像 RunParkingWindow
和 .NET SystemEvent
这样的线程。
与 C 程序相比,只有 1 个线程 - 您刚刚停止调试的线程。
意识到这一点你会明白,调试中的断点只会停止你当前所在的线程,但其他线程会进一步运行。这也意味着 Wrappers provided by OS
仍在监听任何事件并将它们排入队列。一旦您的应用程序继续 运行,他们将全力以赴,回调方法将完成剩下的工作。
我试图找到任何有数据流的相关文章/图片,但我没有成功。请将以上文字视为意见而非技术段落。
我记得我 4 年前问过这个问题,我想我现在知道答案了:.NET 中的计时器 classes 在内部只使用一次性计时器。因此,当您在断点处暂停程序时,重新设置一次性计时器的代码不会 运行 因此在您让 program/debugger 继续之前不会有回调堆积。
几个月前,我编写了自己的定时器 class,它可以达到高达 1000 Hz 的回调频率。它使用重复设置一次性定时器和忙等待的组合。忙等待会消耗 CPU 个周期,但有助于减少 Windows 线程调度程序引入的抖动。
代码如下:
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
/// <summary>
/// Executes a function periodically.
/// Timer expirations are not queued during debug breaks or
/// if the function takes longer than the period.
/// </summary>
/// <remarks>
/// Uses a Windows one-shot timer together
/// with busy waiting to achieve good accuracy
/// at reasonable CPU usage.
/// Works even on Windows 10 Version 2004.
/// </remarks>
public sealed class Timer : IDisposable
{
/// <summary>
/// Statistics:
/// How much time was 'wasted' for busy waiting.
/// Does not include time passed by using a system timer.
/// </summary>
public TimeSpan TimeSpentWithBusyWaiting => new TimeSpan(Interlocked.Read(ref timeSpentWithBusyWaiting));
private long timeSpentWithBusyWaiting = 0;
private readonly TimeCaps timeCaps;
private readonly Func<bool> function;
private readonly TimeSpan period;
/// <summary>
/// We must keep a reference to this callback so that it does not get garbage collected.
/// </summary>
private readonly TIMECALLBACK callback;
private readonly Stopwatch stopwatch = new Stopwatch();
private volatile bool stopTimer = false;
private ManualResetEvent timerStopped = new ManualResetEvent(false);
private TimeSpan desiredNextTime;
/// <summary>
/// Immediately starts the timer.
/// </summary>
/// <param name="function">
/// What to do after each <paramref name="period"/>.
/// The timer will stop if the function returns <see langword="false"/>.
/// </param>
/// <param name="period">
/// How long to wait between executing each <paramref name="function"/>.
/// </param>
public Timer(Func<bool> function, TimeSpan period)
{
uint? timerDelay = TimeSpanToPositiveMillisecondsWithRoundingDown(period);
if (timerDelay == null)
{
throw new ArgumentOutOfRangeException(nameof(period));
}
uint timeGetDevCapsErrorCode = TimeGetDevCaps(out timeCaps, (uint)Marshal.SizeOf(typeof(TimeCaps)));
if (timeGetDevCapsErrorCode != 0)
{
throw new Exception($"{nameof(TimeGetDevCaps)} returned error code {timeGetDevCapsErrorCode}");
}
Debug.Assert(timeCaps.wPeriodMin >= 1);
this.function = function ?? throw new ArgumentNullException(nameof(function));
this.period = period;
callback = new TIMECALLBACK(OnTimerExpired);
TimeBeginPeriod(timeCaps.wPeriodMin);
stopwatch.Start();
Schedule(desiredNextTime = period, forceTimer: true);
}
/// <summary>
/// Does not cancel the timer and instead
/// blocks until the function passed to
/// the constructor returns <see langword="false"/>.
/// </summary>
public void WaitUntilFinished()
{
if (timerStopped != null)
{
timerStopped.WaitOne();
timerStopped.Dispose();
timerStopped = null;
TimeEndPeriod(timeCaps.wPeriodMin);
}
}
/// <summary>
/// Stops timer and blocks until the
/// last invocation of the function has finished.
/// </summary>
public void Dispose()
{
stopTimer = true;
WaitUntilFinished();
}
private void OnTimerExpired(
uint uTimerID,
uint uMsg,
UIntPtr dwUser,
UIntPtr dw1,
UIntPtr dw2)
{
while (!stopTimer)
{
TimeSpan startOfBusyWaiting = stopwatch.Elapsed;
TimeSpan endOfBusyWaiting = desiredNextTime;
TimeSpan timeThatWillBeSpentWithBusyWaiting = endOfBusyWaiting - startOfBusyWaiting;
if (timeThatWillBeSpentWithBusyWaiting > TimeSpan.Zero)
{
Interlocked.Add(ref timeSpentWithBusyWaiting, timeThatWillBeSpentWithBusyWaiting.Ticks);
}
if (desiredNextTime > stopwatch.Elapsed)
{
while (desiredNextTime > stopwatch.Elapsed)
{
// busy waiting until time has arrived
}
desiredNextTime += period;
}
else
{
// we are too slow
desiredNextTime = stopwatch.Elapsed + period;
}
bool continueTimer = function();
if (continueTimer)
{
if (Schedule(desiredNextTime, forceTimer: false))
{
return;
}
}
else
{
stopTimer = true;
}
}
timerStopped.Set();
}
/// <param name="desiredNextTime">
/// Desired absolute time for next execution of function.
/// </param>
/// <param name="forceTimer">
/// If <see langword="true"/>, a one-shot timer will be used even if
/// <paramref name="desiredNextTime"/> is in the past or too close to
/// the system timer resolution.
/// </param>
/// <returns>
/// <see langword="true"/> if timer was set or <paramref name="forceTimer"/> was <see langword="true"/>.
/// <see langword="false"/> if <paramref name="desiredNextTime"/> was in the past.
/// </returns>
/// <remarks>
/// Increases accuracy by scheduling the timer a little earlier and
/// then do busy waiting outside of this function.
/// </remarks>
private bool Schedule(TimeSpan desiredNextTime, bool forceTimer)
{
TimeSpan currentTime = stopwatch.Elapsed;
TimeSpan remainingTimeUntilNextExecution = desiredNextTime - currentTime;
uint? timerDelay = TimeSpanToPositiveMillisecondsWithRoundingDown(remainingTimeUntilNextExecution);
timerDelay =
timerDelay < timeCaps.wPeriodMin ? timeCaps.wPeriodMin :
timerDelay > timeCaps.wPeriodMax ? timeCaps.wPeriodMax :
timerDelay;
if (forceTimer && timerDelay == null)
{
timerDelay = timeCaps.wPeriodMin;
}
if (forceTimer || timerDelay >= timeCaps.wPeriodMin)
{
// wait until next execution using a one-shot timer
uint timerHandle = TimeSetEvent(
timerDelay.Value,
timeCaps.wPeriodMin,
callback,
UIntPtr.Zero,
0);
if (timerHandle == 0)
{
throw new Exception($"{nameof(TimeSetEvent)} failed");
}
return true;
}
else // use busy waiting
{
return false;
}
}
/// <returns><see langword="null"/> if <paramref name="timeSpan"/> is negative</returns>
private static uint? TimeSpanToPositiveMillisecondsWithRoundingDown(TimeSpan timeSpan)
{
if (timeSpan.Ticks >= 0)
{
long milliseconds = timeSpan.Ticks / TimeSpan.TicksPerMillisecond;
if (milliseconds <= uint.MaxValue)
{
return unchecked((uint)milliseconds);
}
}
return null;
}
private delegate void TIMECALLBACK(
uint uTimerID,
uint uMsg,
UIntPtr dwUser,
UIntPtr dw1,
UIntPtr dw2);
// https://docs.microsoft.com/en-us/previous-versions//dd757634(v=vs.85)
//
// This is the only timer API that seems to work for frequencies
// higher than 60 Hz on Windows 10 Version 2004.
//
// The uResolution parameter has the same effect as
// using the timeBeginPeriod API, so it can be observed
// by entering `powercfg.exe /energy /duration 1` into
// a command prompt with administrator privileges.
[DllImport("winmm", EntryPoint = "timeSetEvent")]
private static extern uint TimeSetEvent(
uint uDelay,
uint uResolution,
TIMECALLBACK lpTimeProc,
UIntPtr dwUser,
uint fuEvent);
[DllImport("winmm", EntryPoint = "timeBeginPeriod")]
private static extern uint TimeBeginPeriod(
uint uPeriod);
[DllImport("winmm", EntryPoint = "timeEndPeriod")]
private static extern uint TimeEndPeriod(
uint uPeriod);
[DllImport("winmm", EntryPoint = "timeGetDevCaps")]
private static extern uint TimeGetDevCaps(
out TimeCaps ptc,
uint cbtc);
[StructLayout(LayoutKind.Sequential)]
private struct TimeCaps
{
public uint wPeriodMin;
public uint wPeriodMax;
}
}