【问题标题】:Shared resources problem with multithreading in WPF C#WPF C#中多线程的共享资源问题
【发布时间】:2018-09-17 08:06:10
【问题描述】:

所以我正在开发一个简单的 WPF 应用程序,它应该从我们 LAN 上的“专用页面”读取信息并更有效地输出它们。所以现在我正在尝试在选择要从中获取数据的 ip 后自动更新标签内容。计划基本上是每分钟左右刷新一次页面以更新输出。 问题是我尝试了很多关于多线程的解决方案,但仍然得到相同的错误:InvalidOperationException:调用线程无法访问此对象,因为不同的线程拥有它。 基本上我从中了解到的是 mainWindows 线程拥有标签的内容,因此在从所述文件/字符串读取后我无法更新它们。

这是我的窗口 xaml:

  <Window x:Class="PartCountBello.MainWindow"
          xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
          xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
          xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
          xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
          xmlns:local="clr-namespace:PartCountBello"
          mc:Ignorable="d"
          Title="Controllo Macchine" Height="337.42" Width="366.586" 
  ResizeMode="CanMinimize">
      <Grid Margin="0,10,2,12">
          <Grid.ColumnDefinitions>
              <ColumnDefinition/>
              <ColumnDefinition Width="0*"/>
          </Grid.ColumnDefinitions>
          <ComboBox x:Name="cmbNomiMacchine" HorizontalAlignment="Left" 
  Margin="44,74,0,0" VerticalAlignment="Top" Width="129" Height="22" 
  SelectionChanged="cmbNomiMacchine_SelectionChanged"/>
          <Label x:Name="lblNomeDato" Content="PartCount :" 
  HorizontalAlignment="Left" Margin="44,162,0,0" VerticalAlignment="Top" 
  Height="26" Width="129"/>
          <Label x:Name="lblPartCount" Content="" HorizontalAlignment="Left" 
  VerticalAlignment="Top" Margin="178,162,0,0" Height="26" Width="144" 
  RenderTransformOrigin="0.071,0.731"/>
          <Label x:Name="lblSelectInfos" Content="Selezionare macchina" 
  HorizontalAlignment="Left" Margin="44,43,0,0" VerticalAlignment="Top" 
  Width="129" Height="26"/>
          <Label x:Name="lblLavoro" Content="Mansione : " 
  HorizontalAlignment="Left" Margin="44,210,0,0" VerticalAlignment="Top" 
  Width="129"/>
          <Label x:Name="lblMansione" Content="" HorizontalAlignment="Left" 
  Margin="178,210,0,0" VerticalAlignment="Top" Width="144"/>
          <Button x:Name="btnRefresh" Content="Aggiorna" 
  HorizontalAlignment="Left" Margin="247,74,0,0" VerticalAlignment="Top" 
  Width="75" Height="22" Click="btnRefresh_Click"/>
          <Label x:Name="lblMoreInfos" Content="" HorizontalAlignment="Left" 
  Margin="10,241,0,0" VerticalAlignment="Top" Width="339" Height="35"/>

      </Grid>
  </Window>

这是我的 mainWindows 代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.NetworkInformation;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace PartCountBello
{
    /// <summary>
    /// Logica di interazione per MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        List<string> datas = new List<string> { };      //list that contains machines names and relatives ips. each ip is in the previous index...
                                                        //respect to the machine. 
        public MainWindow()
        {
            InitializeComponent();
            try
            {
                ReadFile rf = new ReadFile();                       //creates the file reader we 're using to...
                datas = rf.getListFromFile("MyIPsFile");   //get all our datas from our file.. Not posting the real file name here cuz you never know. still contains only the ips to connect to.
                for(int i = 2;i<=datas.Count-1;i+=3)
                {
                    cmbNomiMacchine.Items.Add(datas[i]);
                }
            }
            catch (Exception ex)
            {
                lblMoreInfos.Content = ex.Message;
            }
            finally
            { 
            }
        }

        /// <summary>
        /// when the selection is changed to an item it will update the labels contents through a method in the class activityChecker.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void cmbNomiMacchine_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            updateLabelsContent();
        }

        /// <summary>
        /// gets the index the item selected in the combobox has in the file to pass it to the fileread and get the ip.
        /// </summary>
        /// <param name="index"></param>
        /// <returns></returns>
        private int getSelectedItemIndex(int index)
        {
            int FIRST_REAL_INDEX = 2;
            int realIndex;
            if (index == 0)
                return 0;
            else
                return realIndex=FIRST_REAL_INDEX+index*2;
        }

        /// <summary>
        /// checks if the machine we are trying to connect to is on, if so updates the dedicated lables' content, else prints a simple message down below.
        /// </summary>
        private void updateLabelsContent()
        {
            string toShow;//the auxiliary string we are going to use to output on labels.
            ActivityChecker ac = new ActivityChecker();
            lblMoreInfos.Content = "";
            if (getSelectedItemIndex(cmbNomiMacchine.SelectedIndex) != 0)
            {
                if (PingHost(datas[getSelectedItemIndex(cmbNomiMacchine.SelectedIndex) - 1]))
                {
                    toShow = ac.getPartCount(datas[getSelectedItemIndex(cmbNomiMacchine.SelectedIndex)]);
                    lblPartCount.Content = toShow;
                    toShow = ac.getJob(datas[getSelectedItemIndex(cmbNomiMacchine.SelectedIndex)]);
                    lblMansione.Content = toShow;
                }
                else
                {
                    lblMoreInfos.Content = "La macchina è al momento spenta.";
                }
            }
            else
            {
                lblMansione.Content = "";
                lblPartCount.Content = "";
            }
        }

        /// <summary>
        /// updates the content of the machine dedicated labels.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnRefresh_Click(object sender, RoutedEventArgs e)
        {
                updateLabelsContent(); 
        }

        /// <summary>
        /// allows to verify if the machine we are trying to connect to is on before we actually try to, avoids some freezes.
        /// </summary>
        /// <param name="nameOrAddress"></param>
        /// <returns></returns>
        public static bool PingHost(string nameOrAddress)
        {
            bool pingable = false;
            Ping pinger = new Ping();
            try
            {
                PingReply reply = pinger.Send(nameOrAddress);
                pingable = reply.Status == IPStatus.Success;
            }
            catch (PingException)
            {
                // Discard PingExceptions and return false;
            }
            return pingable;
        }
    }
}

【问题讨论】:

  • 您能否使用本地磁盘上的文件副本进行测试.. 看看是否正常。这个测试可以消除存储在局域网上的文件的权限或权限问题吗?
  • 最懒惰的方式:查看 SynchronizationContext
  • @Frenchy 是的,问题不在于读取文件的问题,而只是不愿意在 2 个线程内共享的标签。文件和阅读部分工作正常。
  • @AlexandruClonțea 我应该如何进入 sychronizationContext?我是多线程编程的新手,所以更详细的东西真的会有所帮助。

标签: c# wpf multithreading xaml


【解决方案1】:

好的,我找到了解决方案。

将此添加到 mainwindows.cs:

        /// <summary>
        /// private method that manages the thread for the automatic update.
        /// </summary>
        private void updateLabelsContentThread()
        {
            while(true)
            {
                Thread.Sleep(TimeSpan.FromSeconds(10));
                Dispatcher.Invoke(new Action(() => { updateLabelsContent(); }));
            }
        }

并将机器选择的事件方法更改为此。

/// <summary>
        /// when the selection is changed to an item it will update the labels contents through a method in the class activityChecker.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void cmbNomiMacchine_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            updateLabelsContent();
            Thread t = new Thread(updateLabelsContentThread);
            t.Start();

        }

非常适合我,把它留在这里以防它帮助某人。

【讨论】:

  • 很高兴您找到了解决方案 :)
  • @AlexandruClonțea 仍然很高兴了解您将如何使用 SynchronizationContext 修复它。我查看了一些解释,但无法真正弄清楚。
  • 我在你之后发布了一个答案!让我知道什么有意义或没有意义。我还在想为什么一开始就出现了跨线程操作异常,可能是屏幕外代码……或者我昨天累了
【解决方案2】:

我不提倡这种类似 WinForms 的方法,使用绑定可能有更好的解决方案,但它需要进行大量更改。如果您现在需要这样做:

public partial class MainWindow : Window
{
    ....

    private readonly SynchronizationContext uiContext;

    public MainWindow()
    {
        InitializeComponent();

        //controls created on a specific thread can only be modified by that thread.
        //(99.99999%) of the time controls are created and modified on the UI thread
        //SynchronizationContext provides a way to capture and delegate work to the UI thread (it provides more functionality than this, but in your case this is what interests you)
        //We capture the UI Synchronization Context so that we can queue items for execution to the UI thread. We know this is the UI thread because we are in the constructor for our main window
        uiContext = SynchronizationContext.Current;
        ....
    }

    private void cmbNomiMacchine_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        UpdateOnUIThread();
    }

    ...............

    ///I Chose to write another method for clarity, feel free to rework the code anyway you like. Ideally you want to only delegate short work to the UI thread (say textbox.Text = "". This is just here to show the concept
   private void UpdateOnUIThread()
   {
       //Post is asynchronous so will give controll back immediately. If you want synchronous operation, use Send
       uiContext.Post(new SendOrPostCallback((o) => { updateLabelsContent(); }), null);
   }

   ..............
}

}

【讨论】:

  • 好的,我在您回答之前对同步上下文进行了一些实验。我很高兴我发现我必须在初始化组件之后获取当前上下文。我仍然不确定在这种情况下我得到了什么帖子,但我会做一些实验并弄清楚:)谢谢:)
  • @Cicciopalla010 Post 表示调用线程可以继续其非 UI 工作。如果您在同步上下文中调用了 .Send 而不是 .Post,则调用线程将被阻塞,等待 UI 更新操作完成。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-08-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多