我通过端口和接收器将15个异步操作链接在一起。这让我非常关注线程间的消息传递时间,特别是任务将数据发布到端口与新任务开始在不同线程上处理相同数据之间所花费的时间。假设每个线程在启动时处于空闲状态的最佳情况,我已经生成了一个测试,该测试使用秒表类来测量两个不同调度程序的时间,每个调度程序以最高优先级运行一个线程。
我发现让我感到惊讶的是,我的开发平台是运行Windows 7 x64的Q6600四核2.4 Ghz计算机,我测试的平均上下文切换时间为5.66微秒,标准偏差为5.738微秒,最大值接近1.58毫秒(因子为282!)。秒表频率为427.7纳秒,因此我仍然远离传感器噪音。
我想要做的是尽可能减少线程间的消息传递时间,同样重要的是,减少上下文切换的标准偏差。我意识到Windows不是实时操作系统,并且没有保证,但是Windows调度程序是基于公平循环优先级的计划,并且此测试中的两个线程都处于最高优先级(唯一的线程应该是高),所以线程上不应该有任何上下文切换(很明显是1.58 ms最大的时间......我相信windows quanta是15.65 ms?)我唯一能想到的是OS调用时间的变化CCR用于在线程之间传递消息的锁定机制。
如果有其他人已经测量了线上留言时间,请告诉我,并对如何改进它有任何建议。
以下是我测试的源代码:
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using Microsoft.Ccr.Core;
using System.Diagnostics;
namespace Test.CCR.TestConsole
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Starting Timer");
var sw = new Stopwatch();
sw.Start();
var dispatcher = new Dispatcher(1, ThreadPriority.Highest, true, "My Thread Pool");
var dispQueue = new DispatcherQueue("Disp Queue", dispatcher);
var sDispatcher = new Dispatcher(1, ThreadPriority.Highest, true, "Second Dispatcher");
var sDispQueue = new DispatcherQueue("Second Queue", sDispatcher);
var legAPort = new Port<EmptyValue>();
var legBPort = new Port<TimeSpan>();
var distances = new List<double>();
long totalTicks = 0;
while (sw.Elapsed.TotalMilliseconds < 5000) ;
int runCnt = 100000;
int offset = 1000;
Arbiter.Activate(dispQueue, Arbiter.Receive(true, legAPort, i =>
{
TimeSpan sTime = sw.Elapsed;
legBPort.Post(sTime);
}));
Arbiter.Activate(sDispQueue, Arbiter.Receive(true, legBPort, i =>
{
TimeSpan eTime = sw.Elapsed;
TimeSpan dt = eTime.Subtract(i);
//if (distances.Count == 0 || Math.Abs(distances[distances.Count - 1] - dt.TotalMilliseconds) / distances[distances.Count - 1] > 0.1)
distances.Add(dt.TotalMilliseconds);
if(distances.Count > offset)
Interlocked.Add(ref totalTicks,
dt.Ticks);
if(distances.Count < runCnt)
legAPort.Post(EmptyValue.SharedInstance);
}));
//Thread.Sleep(100);
legAPort.Post(EmptyValue.SharedInstance);
Thread.Sleep(500);
while (distances.Count < runCnt)
Thread.Sleep(25);
TimeSpan exTime = TimeSpan.FromTicks(totalTicks);
double exMS = exTime.TotalMilliseconds / (runCnt - offset);
Console.WriteLine("Exchange Time: {0} Stopwatch Resolution: {1}", exMS, Stopwatch.Frequency);
using(var stw = new StreamWriter("test.csv"))
{
for(int ix=0; ix < distances.Count; ix++)
{
stw.WriteLine("{0},{1}", ix, distances[ix]);
}
stw.Flush();
}
Console.ReadKey();
}
}
}
答案 0 :(得分:2)
Windows不是实时操作系统。但是你已经知道了。杀死你的是上下文切换时间,不一定是消息时间。您没有真正指定您的进程间通信如何工作。如果你真的只是运行多个线程,你可以通过不使用Windows消息作为通信协议来获得一些收益,而是尝试使用应用程序托管的消息队列来滚动自己的IPC。
当发生上下文切换时,对于任何版本的Windows,您可以期望的最佳平均值是1ms。当您的应用程序必须屈服于内核时,您可能会看到1ms的时间。这是针对Ring-1应用程序(用户空间)的设计。如果低于1ms绝对是至关重要的,则需要将部分应用程序切换为Ring-0,这意味着要编写设备驱动程序。
设备驱动程序不会遇到与用户应用程序相同的上下文切换时间,并且还可以访问纳秒级分辨率计时器和睡眠呼叫。如果您确实需要这样做,可以从Microsoft免费获得DDK(设备驱动程序开发工具包),但我强烈建议您投资第三方开发工具包。他们通常拥有非常好的样本和许多向导来设置正确的东西,这将花费你几个月阅读DDK文档来发现。你也想得到像SoftIce这样的东西,因为普通的Visual Studio调试器不会帮助你调试设备驱动程序。
答案 1 :(得分:2)
15个异步操作是否是异步的?也就是说,您是否因某些库的限制而被迫以这种方式操作,或者您是否可以选择进行同步通话?
如果您可以选择,则需要构建应用程序,以便异步性的使用由配置参数控制。在不同线程上返回的异步操作与在同一线程上返回的同步操作之间的区别应该在代码中是透明的。这样你就可以在不改变代码结构的情况下调整它。
短语“令人尴尬的并行”描述了一种算法,其中所做的大部分工作显然是独立的,因此可以按任何顺序进行,使其易于并行化。
但你是“通过端口和接收器将15个异步操作链接在一起”。这可以被描述为“令人尴尬的顺序”。换句话说,可以在单个线程上逻辑地编写相同的程序。但是,对于异步操作之间发生的CPU限制工作,你会失去任何并行性(假设有任何重要性)。
如果您编写一个简单的测试来删除任何重要的CPU限制工作并只测量上下文切换时间,那么猜测一下,您将测量上下文切换时间的变化,就像您发现的那样
运行多个线程的唯一原因是因为您需要为CPU做大量工作,因此您希望在多个CPU之间共享它。如果单个计算块足够短,那么上下文切换将是任何 OS的重要开销。通过将计算分解为15个阶段,每个阶段非常短,您实际上是要求操作系统进行大量不必要的上下文切换。
答案 2 :(得分:0)
ThreadPriority.Highest并不仅仅意味着线程调度程序本身具有更高的优先级。 Win32 API具有更细粒度的线程优先级(clicky),其中有几个级别高于最高级别(IIRC最高级别通常是可以运行的最高优先级非管理员代码,管理员可以安排更高的优先级,任何硬件驱动程序都可以/ kernel mode code)所以不能保证它们不会被抢占。
即使线程以最高优先级运行,窗口也可以提升其基本优先级以上的其他线程,如果它们持有优先级较高的线程所需的资源锁,这是您可能遇到上下文切换的另一种可能性。
即便如此,正如您所说,Windows不是实时操作系统,并且无论如何都无法保证支持线程优先级。
答案 3 :(得分:0)
要以不同的方式解决此问题,您是否需要进行如此多的解耦异步操作?考虑以下内容可能是有用的:垂直分区工作(异步处理numCores数据块的端到端)而不是水平分区工作(现在,在15个解耦阶段处理每个数据块);同步耦合15个阶段中的一些阶段以将总数减少到更小的数字。
线程间通信的开销总是非常重要的。如果你的15个操作中的某些操作只做了一小部分工作,那么上下文切换会让你感到厌烦。