我正在使用 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];
}
}
}
由于您没有任何使用线程的经验,因此您将会很困难。多线程并不是微不足道的,常识也不适用。要编写正确的多线程程序,您必须知道自己在做什么,并且遵守纪律。编译器无法帮助您检测错误,您必须自己意识到它们。错误消息也不会帮助您,因为它们只是表明您的程序状态已损坏,并且实际的错误与引发错误的代码行无关。
多线程的基本规则是,在处理像
Dictionary<K,V>
这样的非线程安全组件时,对组件的每次访问都必须同步。这意味着一次只允许一个线程与组件交互。你不能挑剔,每一个操作都必须与lock
语句同步,始终使用相同的locker对象。连读字典的Count
也必须同步。即使使用字典的辅助外观,如 Keys
、Values
或枚举器也必须同步。对字典的一次不受保护的访问会使您的程序不正确,并且其行为未定义。
我的建议是花一些时间,也许一周,系统地学习多线程。 Joseph Albahari 的 C# 中的线程 是一个很好的在线资源。这将帮助您编写具有可预测行为的正确程序,并且您可以长期维护这些程序。如果你不知道自己在做什么,你只会制造混乱。