并行加载时我应该使用什么数据结构来兑现我的资源?

问题描述 投票:0回答:1

我正在使用 C# 工作。对于我的一个项目,我需要加载一些资源,我没有任何使用线程的经验,所以经过一些谷歌搜索后,我构建了一个 90% 的时间都可以工作的系统。显然崩溃 10% 并不能解决问题,所以我希望线程锁定大师可以告诉我我做错了什么。

我认为我的主要问题是我不知道如何正确组织数据,以便多个线程可以访问它。任何意见表示赞赏:)

这是我所写内容的精简版本。有一个很大的资源树,许多资源依赖于其他资源,没有循环。我不知道哪些资源已经从之前的加载调用加载到现金中,所以我必须检查。问题是字典不支持并发操作,即使我锁定用于访问资源的 id 也是如此。

using System;
using System.Collections.Generic;
using System.Linq;
using Sourceage.IO.Interface;
using Sourceage.Element;
using System.ComponentModel;
using System.Threading.Tasks;
using System.Threading;

namespace NameSpace
{
    public class Progress
    {
        private float _value;
        private BackgroundWorker _worker;
        public Progress(BackgroundWorker worker)
        {
            _worker = worker;
        }
        
        public void Report(float value)
        {
            lock(this)
            {
                _value += value;
                _worker.ReportProgress((int)_value);
            }
        }
    }
    
    public class MyClass
    {
        //Id to factory function, accepting an array of dependencies
        private Dictionary<int, func<object[], object>> _idToFactory = new();
        
        //Id to function providing dependency ids
        private Dictionary<int, func<IEnumerable<int>>> _idToDeps = new();
        
        //Id to cashed instance of resource
        private Dictionary<int, object> _cashe = new();

        //Creates a background worker for loading the given resource
        public BackgroundWorker CreateLoadWorker(int id, Action postWork = null)
        {
            var worker = new BackgroundWorker();

            worker.WorkerReportsProgress = true;

            void worker_DoWork(object sender, DoWorkEventArgs e)
            {
                worker.ReportProgress(1);

                var progress = new Progress(worker);

        var idToDep = new Dictionary<int, IEnumerable<<int>>();
        CollectDeps(id, idToDep);
        LoadResource(id, idToDep, progress, 98f);

                e.Result = _cashe[id];

                worker.ReportProgress(100);
                postWork?.Invoke();
            }

            worker.DoWork += worker_DoWork;

            return worker;
        }

        //recursivly collects dependencies for the resource
        private void CollectDeps(int id, Dictionary<int, IEnumerable<<int>> idToDep)
        {
            if (!idToDep.ContainsKey(id))
            {
                var deps = _idToDeps[id]();
                idToDep.Add(id, deps);

                foreach (var dep in deps)
                {
                    CollectDeps(dep, idToDep);
                }
            }
        }

        //recursivly loads resource and its dependencies
        private void LoadResource(int id,
                                  Dictionary<int, IEnumerable<<int>> idToDep,
                                  Progress progress,
                                  float progressAlotment)
        {
            var deps = idToDep[id];
            lock (deps)
            {
                
                //this will crash due to concurrent operations not being supported,
                //but the lock above should mean only one thread can be using the 
                //id at a time
                if (_cashe.ContainsKey(id))
                {
                    //already loaded
                    progress.Report(progressAlotment);
                    return;
                }

                //dependencies
                var depProgressAlotment = progressAlotment / (deps.Count() + 1);

                Parallel.ForEach(deps, (dep) =>
                {
                    LoadResource(dep, idToDep, progress, depProgressAlotment);
                });
        
                //this will sometimes crash due to id 'x' not being present in the cashe
                //but I don't think I should be able to get here without each dep id 
                //already being assigned by the Parallel above
                _cashe[id] = _idToFactory[id](deps.Select(x => _cashe[x]));

                progress.Report(depProgressAlotment);
            }
        }

        public object GetResource(int id)
        {
            var doneEvent = new AutoResetEvent(false);
            var worker = CreateLoadWorker(assignedIds, () => doneEvent.Set());
            worker.RunWorkerAsync();
            doneEvent.WaitOne();

            return _cashe[id];
        }
    }
}
c# multithreading concurrency resources
1个回答
0
投票

由于您没有任何使用线程的经验,因此您将会很困难。多线程并不是微不足道的,常识也不适用。要编写正确的多线程程序,您必须知道自己在做什么,并且遵守纪律。编译器无法帮助您检测错误,您必须自己意识到它们。错误消息也不会帮助您,因为它们只是表明您的程序状态已损坏,并且实际的错误与引发错误的代码行无关。

多线程的基本规则是,在处理像

Dictionary<K,V>
这样的非线程安全组件时,对组件的每次访问都必须同步。这意味着一次只允许一个线程与组件交互。你不能挑剔,每一个操作都必须与
lock
语句同步,始终使用相同的locker对象。连读字典的
Count
也必须同步。即使使用字典的辅助外观,如
Keys
Values
或枚举器也必须同步。对字典的一次不受保护的访问会使您的程序不正确,并且其行为未定义。

我的建议是花一些时间,也许一周,系统地学习多线程。 Joseph Albahari 的 C# 中的线程 是一个很好的在线资源。这将帮助您编写具有可预测行为的正确程序,并且您可以长期维护这些程序。如果你不知道自己在做什么,你只会制造混乱。

© www.soinside.com 2019 - 2024. All rights reserved.