使用串口通信时,.Net线程、线程池和任务(Task)有什么区别?

5
我在使用C# .Net 4.0应用程序的SerialPort类以及ThreadPool.QueueUserWorkItem或Tasks时遇到了有趣的问题。
仅当我同时使用2个或更多SerialPorts时才会出现问题。每个串口在自己的线程中运行,我可以通过以下3种方式之一创建线程:
1. new Thread(DoSerialCommX) 2. ThreadPool.QueueUserWorkItem(DoSerialCommX) 3. new Task(DoSerialCommX, TaskCreationOptions.LongRunning).Start()
为了说明这个问题,我创建了一个DoSerialCommX方法,在循环中永远读写串口。代码大致如下:(这不是我的真实程序,而是从测试程序中隔离和说明该问题的片段)
private void DoSerialCommX()
{
    SerialPort port = new SerialPort("ComX", 9600);
    port.Open();

    while(true)
    {
        //Read and write to serial port
    }
}

如果我使用方法2或3,串行通信就会出现卡顿,并且我会收到很多通信超时的错误。如果我使用方法1,一切都很好。此外,我应该提到,这似乎只发生在我的Intel Atom台式电脑上。桌面PC没有问题。
我知道线程池会重用线程,默认情况下,Task使用线程池。我知道线程池真正意图是为短期操作而设计的。但是我尝试使用TaskCreationOptions.LongRunning,我认为它会生成一个专用线程而不是使用线程池,但仍然无法解决问题。
所以问题是:在这种情况下,什么使“线程”如此特殊?有关“线程”的某些内容使其更适合IO操作吗?
编辑:到目前为止,答案似乎假定我正在尝试将ThreadPool或Tasks用于永无止境的过程。在我的实际应用程序中,情况并非如此。我只是在上面的代码中使用永无止境的循环来说明问题。我真的需要知道为什么“线程”有效,而“ThreadPool”和“Task”则不行。从技术上讲,它们之间有什么不同会导致串行通信出现问题?

我想这是一个愚蠢的问题,但你不是在尝试管理>= 25个线程,对吧? - Jeff
@JeffN825,不,每个串口只有1个线程,并且我最多使用4个串口。 - Verax
任务看起来可能与线程相同,最终导致线程被绑定执行,但它所采取的路线是通过一个独立于操作系统线程调度程序的单独任务调度程序。更多的负担和负荷,需要运行更多的代码。 - stephbu
@stephbu - 任务调度程序并不独立于操作系统线程调度程序。运行任务的池线程是内核线程,它们从队列中提取任务并运行它们。一个9600波特率的串口应该不会对线程池或专用线程造成问题,这几乎难以置信。要么是OP代码、OP驱动程序或OP硬件出了问题。 - Martin James
Windows线程调度程序完全不知道.NET任务TaskScheduler的存在,是的,我同意,但反过来不行。我认为我们实际上是同意的 :) - Martin James
显示剩余2条评论
4个回答

2
从执行线程的行为来看,1、2和3之间基本上没有什么区别。除非你手动覆盖,否则它们都共享相同的默认执行优先级 - 虚拟上,线程调度程序将使用相同的策略来调度它们。它们都会在循环中愉快地旋转。
我认为方法之间更大的区别在于:
1.支持线程池的基础设施成本(线程、内存等),这些线程池将与你的执行循环争夺时钟时间片。
2.对于运行能力较弱的Atom,线程上下文切换的成本更高。Atom具有较小的缓存和较短的处理管道。运行线程越多,上下文切换就越多,处理管道的丢弃和指令缓存效率的降低也越多。
从功能角度来看,使用方法2和3有点滥用 - 你的意图是永远不要退出该方法。这些策略针对原子、有限操作进行了优化,并且在异步网络、磁盘操作等IO完成端口任务中有着一定的执行效果(也许对于串行端口代码也是一个可能性?)。
除非你有兴趣适应异步IO,否则建议跳过2和3。专注于线程 - 似乎你想要更细粒度、可预测的执行控制,而不需要线程池带来的基础设施开销。

1
这是一个9600波特率的串口!每毫秒传输一个字符。UART有一个硬件缓冲区,驱动程序也有一个缓冲区。几乎难以置信这两个点在此处会成为问题。 - Martin James
我的华硕原子能笔记本电脑可以愉快地运行两个串口,速率为115Kbaud,没有任何问题。这并不是一般的硬件问题(当然,OP的设备可能有问题)。 - Martin James

1

我怀疑这是因为你正在读/写 COM 端口导致的。如果没有这个因素,所有这些应该表现相同,因为(如果你检查)它们都在具有普通优先级的线程中运行。

也许可以阅读一下I/O 完成端口和线程池,看看是否可以解释这种奇怪的行为。


我认为你说得非常正确。串口是这个问题的关键组件。您能否详细说明一下I/O完成端口建议? - Verax
请参阅http://hi.baidu.com/jrckkyy/blog/item/401422527c131b070df3e37b.html,了解更多关于I/O完成端口的工作原理的信息。 - Ian Mercer

1

你的代码、驱动程序或硬件中有一些问题,导致串口性能处于“边缘”状态,几乎无法正常工作。使用专用线程或线程池应该不会有问题,(修改:可以使用线程池线程来阻塞串口读取)。

两个9600波特率的串口,只要算盘上的珠子和线都涂好油,就可以运行。


哎呀,我可以轻松地在一个生锈的算盘上运行两个9600波特率的串行端口。 - Allon Guralnek
这对你来说没问题。你可能有一个带有超级珠的多线算盘。 - Martin James

0

有一些细微的差别 - 这些差别导致了超时:

  • 使用 Thread 显式地创建一个新线程,当 Thread.Start() 被调用时 - 它确保你的代码在那个特定的时刻运行。
  • 使用 ThreadPool 确保根据内部 ThreadPool 逻辑,在不久的将来会打开一个线程。由于 ThreadPool 数量有限,建议不要将其用于长时间(或无限)运行的操作。
  • 使用任务只能确定代码是否运行。它可能在另一个线程上甚至在主线程上运行,并不能确保它会立即运行或不同的任务会在不同的线程上运行。

因此,如果您希望任务快速启动,请始终使用 Thread。Tasks 和 ThreadPool 都具有一些额外的 "基础设施开销",这可能会导致您注意到的轻微延迟。

由于 ThreadPool 和任务都不适用于您的任务从未存在的情况,我建议您使用 Thread。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接