【问题标题】:Concurrency architecture in an MVVM applicationMVVM 应用程序中的并发架构
【发布时间】:2011-10-04 09:17:32
【问题描述】:

我有一个客户端/服务器类型的应用程序设置,类似于 bittorrent 下载程序。但是,种子是远程发送到客户端的。

共享数据的主要部分是要下载的文件(种子)列表。

我必须同时处理这些情况:

  • 服务器(通过 WCF)发送更新的文件列表以供下载,这意味着一些新文件将被添加到列表中,一些新文件将从列表中删除(并且一些保持不变)
  • 同时文件可能会完成下载/更改状态,因此列表中的项目需要在本地更新为新状态
  • 客户端的本地事件可能会导致列表中的某些项目过期,因此应将其删除

我使用的是 MVVM 架构,但我认为视图模型应该紧密映射到视图,所以我添加了一个“服务”层,目前是一堆单例(我知道)。其中一个充当所述列表的共享资源,因此我有一个由多个线程更新的集合。

我想远离单例,转而支持依赖注入和不可变对象,以减少我一直看到的死锁、“删除/分离对象”和数据完整性错误。

但是,我不知道在哪里“保留”列表以及如何管理来自不同线程的传入事件,这些事件可能会取消/否定/覆盖列表的当前处理。

我正在寻找有关在高级别处理这种情况的指示。

我正在为列表中的项目使用实体框架,因为数据也需要持久化。

【问题讨论】:

    标签: .net entity-framework architecture mvvm concurrency


    【解决方案1】:

    我最近为 Windows 服务检查器做了类似的事情。它最终也很容易实现。

    在您的情况下,我认为需要以下内容。

    文件 - 它的唯一目的是下载文件并通知更改。
    FileManager - 维护文件列表并添加新的,删除等。

    public class File : INotifyPropertyChanged
        {
            private readonly string _fileName;
            private Thread _thread;
            private Task _task;
            private bool _cancelled;
    
            private TaskStatus _taskStatus;
            private int _taskProgress;
            private int _taskTotal;
    
            public event PropertyChangedEventHandler PropertyChanged;
    
            public File(string fileName)
            {
                _fileName = fileName;
                TaskStatus = TaskStatus.NotStarted;
            }
    
            public TaskStatus TaskStatus
            {
                get { return _taskStatus; }
                private set
                {
                    _taskStatus = value;
                    PropertyChanged.Raise(this, x => x.TaskStatus);
                }
            }
    
            public int TaskProgress
            {
                get { return _taskProgress; }
                private set
                {
                    _taskProgress = value;
                    PropertyChanged.Raise(this, x => x.TaskProgress);
                }
            }
            public int TaskTotal
            {
                get { return _taskTotal; }
                private set
                {
                    _taskTotal = value;
                    PropertyChanged.Raise(this, x => x.TaskTotal);
                }
            }
    
            public void StartTask()
            {
                _cancelled = false;
    
                //.Net 4 - task parallel library - nice
                _task = new Task(DownloadFile, TaskCreationOptions.LongRunning);
                _task.Start();
    
                //.Net Other
                _thread = new Thread(DownloadFile);
                _thread.Start();
            }
    
            public void CancelTask()
            {
                _cancelled = true;
            }
    
            private void DownloadFile()
            {
                try
                {
                    TaskStatus = TaskStatus.Running;
    
                    var fileLength = _fileName.Length;
                    TaskTotal = fileLength;
    
                    for (var i = 0; i < fileLength; i++)
                    {
                        if (_cancelled)
                        {
                            TaskStatus = TaskStatus.Cancelled;
                            return;
                        }
    
                        //Some work to download the file
                        Thread.Sleep(1000); //sleep for the example instead 
    
                        TaskProgress = i;
                    }
    
                    TaskStatus = TaskStatus.Completed;
    
                }
                catch (Exception ex)
                {
                    TaskStatus = TaskStatus.Error;
                }
            }
        }
    
        public enum TaskStatus
        {
            NotStarted,
            Running,
            Completed,
            Cancelled,
            Error
        }
    
        public static class NotifyPropertyChangedExtention
        {
            public static void Raise<T, TP>(this PropertyChangedEventHandler pc, T source, Expression<Func<T, TP>> pe)
            {
                if (pc != null)
                {
                    pc.Invoke(source, new PropertyChangedEventArgs(((MemberExpression)pe.Body).Member.Name));
                }
            }
        }
    

    这样做的好处是您永远不需要从后台线程更新 UI。您正在更新的是只读属性,只有后台类也会写入。此类之外的任何内容都只能读取,因此您不必担心锁定。当引发 PropertyChanged 时,UI 绑定系统将收到属性已更改的通知,然后将读取该值。

    现在是经理

    public class FileManager
        {
            public ObservableCollection<File> ListOfFiles { get; set; }
    
            public void AddFile(string fileName)
            {
                var file = new File(fileName);
                file.PropertyChanged += FilePropertyChanged;
                file.StartTask();
                ListOfFiles.Add(file);
            }
    
            void FilePropertyChanged(object sender, PropertyChangedEventArgs e)
            {
                if (e.PropertyName == "TaskStatus")
                {
                    var file = (File) sender;
                    if (file.TaskStatus==TaskStatus.Completed)
                    {
                        RemoveFile(file);// ??? automatically remove file from list on completion??
                    }
                }
            }
    
            public void RemoveFile(File file)
            {
                if (file.TaskStatus == TaskStatus.Running)
                {
                    file.CancelTask();
                }
                //unbind event
                file.PropertyChanged -= FilePropertyChanged;
                ListOfFiles.Remove(file);
            }
        }
    

    现在您需要在视图模型中做的就是从 FileManager 公开 ListOfFiles,这是一个可观察的集合。来自它的通知将让绑定系统知道 UI 何时需要更新。

    只需将 ListOfFiles 绑定到 ListView 或类似的,为 File 类添加一个数据模板,这将使列表视图知道如何呈现每个文件。

    您的 WCF 服务器和视图模型应该具有对相同文件管理器的引用,WCF 添加和删除文件,视图模型使 ListOfFiles 可用于 UI。
    这只是一个粗略的技巧来理解这个概念。您需要添加您认为合适的内容。

    如果这有帮助,请告诉我。

    【讨论】:

    • +1 说的好! @DaveO 过去,类似的方法对我有用。这与我提供的答案相同,但更清晰且有据可查。 FileManager 类是您提到的单例。我唯一要添加的是使用 Dispatcher 来确保 ListOfFiles 集合的更新只发生在 UI 线程上。 @JohnPetrak 记录的内容消除了锁定 ListOfFiles 以便从多个线程更新它的麻烦。
    • 非常感谢!尝试这个需要一些时间,但会在我的重构完成后更新。
    • 如果我从一个单独的线程调用 Add/RemoveFile 到 WCF 线程,我应该锁定这些调用吗?另外,如果我从不同的线程迭代 ListOfFiles 是否还需要锁?
    • 添加/删除只能通过调度程序调用,所以不,您不需要锁定集合。但是迭代会很棘手。我正在考虑使用 Dispatcher 在 UI 线程上执行迭代和使用 AutoResetEvent 来通知等待 WCF 线程工作已完成。一个可能的缺点是引入两个线程必须共享的状态(迭代的结果),这可能必须被锁定。您也不想在 UI 线程上做任何繁重的工作,因为用户会注意到应用程序是否出现锁定,因此这仅在迭代速度快时才有效。
    • 感谢约翰/RMart。这对于所述问题非常有效,并且可以很好地将下载活动与列表管理隔离开来。我在管理我的列表时遇到了争用问题,我已经开始了另一个问题:stackoverflow.com/questions/6881242/…
    【解决方案2】:

    也许其他人可以提出您正在寻找的更简洁的设计,但我想知道您遇到的问题是否是多线程问题,而不是固有的设计缺陷。我实际上有点喜欢使用单例来管理列表的想法,但是让它为任何更改引发事件(这里想到一个 ObservableCollection),这将通知订阅这些事件的任何模型或 VM。订阅这些事件时,使用 Dispatcher 为您管理线程同步等。没有什么可以阻止您将单例实例注入任何需要它的模型或虚拟机中。

    不利的一面是管理可能成为一场噩梦的事情,但我想知道是否有任何替代方案不会有类似的不利方面。

    【讨论】:

    • 我目前正在审查我的锁定,但我觉得需要锁定的区域数量众多,这引出了一个问题,即必须有更好的方法。也许是某种任务排队。
    • @DaveO 您是否有多个线程试图管理要下载的文件列表?即您是否有一个列表被多个线程更新?还是多个列表,每个列表都由一个专用线程更新?
    • 多个线程更新列表。以及多个线程更新列表中的单个项目。
    猜你喜欢
    • 2012-11-30
    • 2019-01-01
    • 2012-01-20
    • 2014-03-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-07-04
    相关资源
    最近更新 更多