【问题标题】:Is there a safe way for a quick exchange of data between threads?有没有一种安全的方法可以在线程之间快速交换数据?
【发布时间】:2019-06-17 07:50:53
【问题描述】:

我正在设置一个应用程序,它从称重传感器读取数据,并根据读取的数据实时中断电机的推力。必须从称重传感器获得高频读数。

我正在使用 c# 编程,我决定使用单独的线程从称重传感器获取数据。 我的问题是:如何以线程安全的方式使用线程中获取的数据?例如在图表中显示它们。

这是我调用的线程来获取队列中的数据。

Thread t = new Thread(() =>
            {
                Thread.CurrentThread.IsBackground = true;
                while (save_in_queue)
                {
                    Thread.Sleep(1);
                    if (queue.Count <= 1000)
                    {
                        queue.Enqueue(Frm_main.ComPh1.LeggiAnalogica(this.Address));
                    }
                    else
                    {
                        queue.Dequeue();
                        queue.Enqueue(Frm_main.ComPh1.LeggiAnalogica(this.Address));
                    }
                }
            });
            t.Name = "Queue " + this.name;
            t.Start();

这是我用来关联线程填充队列和主队列的方法

        public void SetData(Queue<int> q)
        {
            this.data = q;
        }

这是我在主应用程序中用来设置系列数据的计时器

private void timer1_Tick(object sender, EventArgs e)
        {
            List<int> dati = new List<int>();
            lock (data)
            {
                dati = data.ToList();
            }
            grafico.Series[serie.Name].Points.Clear();
            for (int x = 0; x < dati.Count; x++)
            {
                DataPoint pt = new DataPoint();
                pt.XValue = x;
                pt.YValues = new double[] { dati.ElementAt(x) };
                grafico.Series[serie.Name].Points.Add(pt);

            }
        }

此代码不起作用,因为有时我收到异常 dati = data.ToList(); 行上的“集合已修改;可能无法执行枚举操作”

我很清楚为什么我会收到此异常。但是如何解决呢?

我想避免使用过多的“锁”或过多的同步变量,以免降低采集性能,目前这非常好。

【问题讨论】:

  • 也许通过类+公共变量和/或方法?
  • 出现异常是因为你没有在线程中锁定队列。这个this.data = q; 只是参考的副本。整个区块if (queue.Count &lt;= 1000)...... 必须在锁内。
  • 如果线程内的锁率太高但您仍然需要这些值,您可以先将它们添加到本地队列中,然后将它们批量添加到共享队列中。 (例如每 20 个值)(queue.AddRange())但是当队列不是列表时,这不起作用。

标签: c# multithreading collections thread-safety


【解决方案1】:

不要在你的消费者线程中这样做:

lock (data) {
    dati = data.ToList();
}

您将队列用于两个不同的目的;您正在使用它在两个线程之间传递数据,这很好;但您也将其用作先前数据样本的历史缓冲区。这很糟糕。

更糟糕的是,每次计时器滴答时,您都会将队列锁定足够长的时间,以让消费者复制它之前在较早滴答时复制的数百个数据。

这也很糟糕:

if (queue.Count <= 1000) {
    queue.Enqueue(Frm_main.ComPh1.LeggiAnalogica(this.Address));
}
else {
    queue.Dequeue();  <== THIS IS BAD!
    queue.Enqueue(Frm_main.ComPh1.LeggiAnalogica(this.Address));
}

这样做的一个问题是,您让生产者管理历史缓冲区(例如,通过限制队列的长度),但关心长度的是消费者。

另一个问题是生产者没有锁定队列。如果任何线程需要锁定一个数据结构,那么每个线程都需要锁定它。


生产者应该只做一件事:它应该从传感器读取数据,并将数据填充到队列中。

队列应该只用于一个目的:在线程之间传递新数据。

生产者应该将队列锁定足够长的时间以从队列中获取新数据,并将其复制到自己的私有集合中。


多线程编程通常是违反直觉的。一个例子是;如果您可以通过增加每个线程必须完成的工作量来减少线程花费在访问共享对象上的时间,这通常会提高程序的整体性能。那是因为锁定是昂贵的,并且因为访问已被其他线程触及的内存位置是昂贵的。

【讨论】:

    【解决方案2】:

    您可能需要检查 Concurrent Collections 命名空间,它提供了一些集合的线程安全实现

    System.Collections.Concurrent 命名空间提供了几个 应该使用的线程安全集合类来代替 System.Collections 中的相应类型和 System.Collections.Generic 命名空间,只要有多个线程 同时访问集合。

    https://docs.microsoft.com/en-us/dotnet/api/system.collections.concurrent

    因此您可以使用 System.Collections.Concurrent.ConcurrentQueue 而不是 System.Collections.Queue 以提供 无锁解决方案你的问题。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-02-16
      • 2021-12-13
      • 2013-08-12
      • 2011-05-15
      • 2012-04-04
      • 2020-01-11
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多