我的程序将从网站请求实时数据。这些数据随时可能发生变化,所以我需要反复频繁地请求它来监控变化。数据将与一些移动平均线一起以直方图的形式显示在 Windows 窗体图表上。基于此信息,用户需要能够与表单交互,以便为程序的一部分设置参数,以便根据传入的数据采取行动。我应该如何处理这些数据?我目前的计划是有一个单独的线程收集数据并将其写入主窗体,但我不确定如何在没有 A) 使界面无响应和 B) 启动另一个线程的情况下监视该数据的更改。由于显而易见的原因,A 是不可接受的,如果我要做 BI,我觉得我不妨把代码扔到收集数据的线程中。
2 回答
在这种情况下,您应该做的是让工作线程对网站进行轮询,并将它找到的所有数据排队到ConcurrentQueue
. 然后让您的 UI 线程定期轮询此队列以获取新数据。您根本不希望该工作线程与 UI 线程交互。不要Control.Invoke
在这种情况下使用其他编组技术。
public class YourForm : Form
{
private CancellationTokenSource cts = new CancellationTokenSource();
private ConcurrentQueue<YourData> queue = new ConcurrentQueue<YourData>();
private void YourForm_Load(object sender, EventArgs args)
{
Task.Factory.StartNew(Worker, TaskCreationOptions.LongRunning);
}
private void UpdateTimer_Tick(object sender, EventArgs args)
{
YourData item;
while (queue.TryDequeue(out item))
{
// Update the chart here.
}
}
private void Worker()
{
CancellationToken cancellation = cts.Token;
while (!cancellation.WaitHandle.WaitOne(YOUR_POLLING_INTERVAL))
{
YourData item = GetData();
queue.Enqueue(item);
}
}
}
上面的示例基于 WinForms,但相同的主体也将延续到 WPF。示例中的重点是。
- 使用 a
System.Windows.Timer.Timer
(或等效的,如果使用 WPF)使用 . 从队列中提取数据项TryDequeue
。将滴答频率设置为在快速刷新屏幕之间提供良好平衡的值,但不要太快以至于它支配 UI 线程的处理时间。 - 使用任务/线程从网站获取数据并使用
Enqueue
. - 使用
CancellationTokenSource
取消操作(我没有包括在示例中) ,并通过Cancel
调用WaitOne
.WaitHandle
CancellationToken
虽然使用Control.Invoke
或其他编组技术不一定是坏事,但它也不是通常被认为是的灵丹妙药。以下是使用这种技术的一些缺点。
- 它将 UI 和工作线程紧密耦合在一起。
- 工作线程决定了 UI 应该多久更新一次。
- 这是一项昂贵的手术。
- 工作线程必须等待 UI 线程处理消息,因此吞吐量会降低。
让 UI 线程轮询更新的优点如下。
- UI 和工作线程更加松散耦合。事实上,双方都对对方一无所知。
- UI 线程可以自行决定应用更新的频率。无论如何,这确实是应该的。
- 没有昂贵的编组操作。
- UI 和工作线程的执行都不会受到对方的阻碍。您可以在两个线程上获得更多吞吐量。
你真正需要的是一个类来保存数据......
class DataContainer
{
readonly byte[] _dataFromWeb;
DataContainer(byte[] data)
{
_dataFromWeb = data;
}
public byte this[int index]
{
get
{
return _dataFromWeb[index];
}
}
public int Length
{
get
{
return _dataFromWeb.GetUpperBound(0)+1;
}
}
}
...以及指向容纳数据的对象的线程安全对象指针。
class SafePointerContainer
{
static public SafePointerContainer Instance = new SafePointerContainer();
public DataContainer _data = null;
private SafePointerContainer() {}
public DataContainer Data
{
get
{
lock(this)
{
return _data;
}
}
set
{
lock(this)
{
_data = value;
}
}
}
当 UI 线程需要读取数据时,它应该获取指针(以线程安全的方式)并将其放入局部变量中。然后它可以使用指针随意访问所有成员变量。
DataContainer latestData = SafePointerContainer.Instance.Data;
for (int i=0; i<latestData.Length; i++)
{
DisplayData(i, latestData[i]);
}
当工作线程需要更新数据时,它应该实例化一个新的对象实例,然后更新指针(以线程安全的方式)指向新的实例。关键是在设置指针之前更新成员。为了强迫你这样做,我使用 readonly 关键字实现了数据,这意味着它只能在构造函数中设置。
DataContainer newData = new DataContainer(dataJustObtainedFromTheWeb);
SafePointerContainer.Instance.Data = newData;
为上述类赋予有意义的名称,并更改 DataContainer 的内容以适应您从 Web 检索到的任何数据。对于奖励积分,使用泛型重新实现。达达。
UI 线程如何知道数据已更改?
UI 线程可以像检查任何其他类或变量一样检查 DataContainer。ANY 线程如何知道任何变量是否已更改?检查它,并将其与最后一个值进行比较。
DataContainer _oldData = null;
while(!UserClickedExit())
{
DataContainer newData = SafePointerContainer.Instance.Data;
if (newData != _oldData)
{
RenderData(newData);
_oldData = newData;
}
else
{
System.Threading.Thread.Sleep(1000);
}
{