无效的跨线程访问问题

Posted

技术标签:

【中文标题】无效的跨线程访问问题【英文标题】:Invalid cross-thread access issue 【发布时间】:2009-12-17 20:36:24 【问题描述】:

我有两个 ViewModel 类:PersonViewModel 和 PersonSearchListViewModel。 PersonViewModel 实现的字段之一是通过 WCF 下载的配置文件图像(本地缓存在隔离存储中)。 PersonSearchListViewModel 是一个包含人员列表的容器类。由于加载图片比较繁重,PersonSearchListViewModel 只加载当前页面、下一页和上一页的图片(结果在 UI 上分页)……为了进一步提高图片的负载,我将图片的负载放在了另一个线程上。然而,多线程方法会导致跨线程访问问题。

PersonViewModel:

public void RetrieveProfileImage()

    Image profileImage = MemorialDataModel.GetImagePerPerson(Person);
    if (profileImage != null)
    
        MemorialDataModel.ImageManager imgManager = new MemorialDataModel.ImageManager();
        imgManager.GetBitmap(profileImage, LoadProfileBitmap);
    


private void LoadProfileBitmap(BitmapImage bi)

    ProfileImage = bi;
    // update 
    IsProfileImageLoaded = true;


private BitmapImage profileImage;
public BitmapImage ProfileImage

    get
    
        return profileImage;
    
    set
    
        profileImage = value;
        RaisePropertyChanged(new System.ComponentModel.PropertyChangedEventArgs("ProfileImage"));
    

PersonSearchListViewModel:

private void LoadImages()

    // load new images 
    Thread loadImagesThread = new Thread(new ThreadStart(LoadImagesProcess));
    loadImagesThread.Start();

    //LoadImagesProcess(); If executed on the same thread everything works fine 


private void LoadImagesProcess()

    int skipRecords = (PageIndex * PageSize);
    int returnRecords;

    if (skipRecords != 0)
    
        returnRecords = 3 * PageSize; // page before, cur page and next page 
    
    else
    
        returnRecords = 2 * PageSize;   // cur page and next page 
    

    var persons = this.persons.Skip(skipRecords).Take(returnRecords);

    // load images 
    foreach (PersonViewModel pvm in persons)
    
        if (!pvm.IsProfileImageLoaded)
        
            pvm.RetrieveProfileImage();
        
    

如何以多线程方式处理 ViewModel 类中的数据?我知道你必须在 UI 上使用调度程序来更新。如何更新绑定到 UI 的 ViewModel?

** 编辑 **

还有一个更奇怪的错误发生。在下面的代码中:

        public void GetBitmap(int imageID, Action<BitmapImage> callback)
        
            // Get from server 
            bitmapCallback = callback;

            memorialFileServiceClient.GetImageCompleted += new EventHandler<GetImageCompletedEventArgs>(OnGetBitmapHandler);
            memorialFileServiceClient.GetImageAsync(imageID);
        

        public void OnGetBitmapHandler(object sender, GetImageCompletedEventArgs imageArgs)
        
            if (!imageArgs.Cancelled)
            
                // I get cross-thread error right here 
                System.Windows.Media.Imaging.BitmapImage bi = new BitmapImage();
                ConvertToBitmapFromBuffer(bi, imageArgs.Result.Image);

                // call call back
                bitmapCallback.Invoke(bi);
            
        

尝试在后台线程中创建新的 BitmapImage 对象时出现跨线程错误。为什么我不能在后台线程上创建一个新的 BitmapImage 对象?

【问题讨论】:

【参考方案1】:

要更新 ViewModel 中的 DependencyProperty,请使用与访问任何其他 UIElement 相同的调度程序:

System.Windows.Deployment.Current.Dispatcher.BeginInvoke(() => ...);

此外,BitmapImages 必须在 UI 线程上实例化。这是因为它使用了只能在 UI 线程上使用的 DependencyProperties。我曾尝试在单独的线程上实例化 BitmapImages,但它不起作用。您可以尝试使用其他方式将图像存储在内存中。例如,当您下载图像时,将其存储在 MemoryStream 中。然后 UI 线程上的 BitmapImage 可以将其源设置为 MemoryStream。

您可以尝试在 UI 线程上实例化 BitmapImages,然后在另一个线程上使用 BitmapImage 执行其他所有操作……但这会变得很麻烦,我什至不确定它是否会起作用。下面是一个例子:

System.Windows.Media.Imaging.BitmapImage bi = null;
using(AutoResetEvent are = new AutoResetEvent(false))

    System.Windows.Deployment.Current.Dispatcher.BeginInvoke(() =>
    
        bi = new BitmapImage();
        are.Set();
    );
    are.WaitOne();


ConvertToBitmapFromBuffer(bi, imageArgs.Result.Image);
bitmapCallback.Invoke(bi);

【讨论】:

太好了,这正是我想要的。我用 byte[] 作为存储的手段。 当我在设置多个属性后必须为多个属性引发 PropertyChanged 事件时,这也对我有帮助。 我没有意识到需要在 UI 线程上制作 BitmapImage。我真的应该想到这一点,否则 DependencyProperty 将是一个问题。 ::facepalm::【参考方案2】:

我相信您的 UI 线程存在跨线程问题。

编辑绑定的对象可能会强制更新工作线程上的 UI,这不会成功。每当您更新绑定的类时,您可能需要执行 InvokeRequired/Invoke hokey-pokey。

你说你已经知道了,但仅供参考:

MSDN on thread-safe calls to UI

【讨论】:

【参考方案3】:

可以通过WriteableBitmap来实现。

    public void LoadThumbAsync(Stream src, 
                    WriteableBitmap bmp, object argument)  
      
        ThreadPool.QueueUserWorkItem(callback =>  
          
            bmp.LoadJpeg(src);  
            src.Dispose();  
            if (ImageLoaded != null)  
              
                Deployment.Current.Dispatcher.BeginInvoke(() =>  
                  
                    ImageLoaded(bmp, argument);  
                );  
              
        );  
    

但你必须在UI Thread中构造WriteableBitmap,然后才能在其他线程中进行加载。

    void DeferImageLoading( Stream imgStream )  
      
        // we have to give size  
        var bmp = new WriteableBitmap(80, 80);  
        imageThread.LoadThumbAsync(imgStream, bmp, this);  
      

查看blog post的更多解释

【讨论】:

我在链接的“博客文章”中找不到任何解释。可能是原始地址已经被其他东西重用 - 或者条目隐藏得很好 juhariis,我更新了博客链接,不便之处见谅

以上是关于无效的跨线程访问问题的主要内容,如果未能解决你的问题,请参考以下文章

C# 学习笔记 控件的跨线程访问

WPF中窗口控件的跨线程调用

跨线程操作无效:控件从创建它的线程以外的线程访问

跨线程操作无效:控件“statusStrip”从创建它的线程以外的线程访问

跨线程操作无效:控件“chart1”从创建它的线程以外的线程访问

例外:跨线程操作无效:控制'pgImportProcess(进度条)'从一个线程访问,而不是它在[重复]上创建的线程