排队问题可能已经解决,因为我们已经能够同时运行多个请求,并且lib很好地报告了每个操作的进度。我们仍然面临关于并发性的其他问题可能是这种明显行为的原因,但这是一个设计问题。然而,要解决这个问题,对VB6中使用的类,模块和变量的内部工作方式有所了解是有帮助的。出现一个问题:是否将类中的所有内容(连接,组件等)封装起来,确保每个创建的对象不与其他实例共享任何数据?
我们对应用程序进行了多次重构以应对资源处理,尤其是在处理OCX时。显然,这解决了内存不足的问题。令我困扰的是,我不了解表面下发生的事情。在这方面,有没有办法看到当前内存中的对象以及它们有多少引用?我知道引用计数模型与基于垃圾收集器的系统不同。我仍然认为RCW包装我们的com对象会让我们保持清洁。在给出的模型中,这是一个安全的假设还是我们缺少的东西?
所以,我可能已经阅读了关于COM多线程主题的最多样化的文章和文档,但我仍然无法理解它应该如何工作,尤其是在与.Net技术交互时比如ASP.Net MVC。这可以被认为是我的一个简单的幻想,除了我们已经得到这个非常关键的项目,并且我们在尝试将所有事情联系起来时遇到了严重的问题。我们出现了内存错误(在VB6中),显然我们错了如何在COM中创建对象和在这些对象之间共享数据。继续阅读以了解故事的发展......
这里不多说。我们有一个由许多ActiveX DLL组成的遗留VB6桌面应用程序。这些配置为使用Apartment
作为线程模型,所有类都设置为MultiUse
。一切顺利,直到我们被要求在强大的网页上移植应用程序时,一切都很顺利:O
由于我们没有从头开始设计和开发解决方案的资源,我们使用基于第三方java(脚本)的框架来快速构建Web应用程序。但是,许多实际工作都是由遗留库完成的,因此我们需要一种方法来连接这两个组件。我们可以想到的最简单的方法是构建一个非常基本的(没有auth和w / o UI)Asp.Net MVC网站用作中间层。这将收到来自Web应用程序的请求,并将它们转换为COM lib来处理数据。
为此,由于libs从未打算用作服务器,我们尝试重构整个事情,以便现在可以以独立的方式使用大多数类:这包括将逻辑与UI分离并在可能的情况下消除所有模块和公共变量;不幸的是,一些前者仍然存在,特别是一些ComponentOne OCX来处理报告和打印。总而言之,这似乎工作得很好,直到我们不得不处理 COM线程模型:O
长话短说,经过大量的挖掘和头痛,我们设计了当前的解决方案,概述如下:
在我们的MVC解决方案中,我们使用System.Threading.Task
,每个请求一个,以异步方式启动请求的操作。我们为操作分配一个id并将此id返回给客户端。要开始这项任务,我们称之为:
protected Task<TReturn> StartSTATask<TReturn>(Func<TReturn> function)
{
var task = Task.Factory.StartNew(
function,
System.Threading.CancellationToken.None,
TaskCreationOptions.None,
STATaskScheduler // property to store the scheduler instance
);
return task;
}
使用STATaskScheduler
运行任务。我们对它进行了修改,以便在池中的线程数设置为0时生成一个新线程。
/// <summary>Initializes a new instance of the StaTaskScheduler class with the specified concurrency level.</summary>
/// <param name="numberOfThreads">The number of threads that should be created and used by this scheduler.</param>
public StaTaskScheduler(int numberOfThreads)
{
// Validate arguments
//if (numberOfThreads < 1) throw new ArgumentOutOfRangeException("concurrencyLevel");
// Initialize the tasks collection
_tasks = new BlockingCollection<Task>();
if (numberOfThreads > 0)
{
// Create the threads to be used by this scheduler
_threads = Enumerable.Range(0, numberOfThreads).Select(i =>
{
var thread = new Thread(() =>
{
// Continually get the next task and try to execute it.
// This will continue until the scheduler is disposed and no more tasks remain.
foreach (var t in _tasks.GetConsumingEnumerable())
{
TryExecuteTask(t);
}
});
thread.Name = "sta_thread_" + i;
thread.IsBackground = true;
thread.SetApartmentState(ApartmentState.STA);
return thread;
}).ToList();
// Start all of the threads
_threads.ForEach(t => t.Start());
}
}
/// <summary>Queues a Task to be executed by this scheduler.</summary>
/// <param name="task">The task to be executed.</param>
protected override void QueueTask(Task task)
{
if (_threads != null)
// Push it into the blocking collection of tasks
_tasks.Add(task);
else
{
var thread = new Thread(() => TryExecuteTask(task));
thread.Name = "sta_thread_task_" + task.Id;
thread.IsBackground = true;
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
}
}
在我们的基本控制器的OnActionExecuting
方法中,我们将其初始化为
STATaskScheduler = HttpContext.Application["STATaskScheduler"] as TaskScheduler;
if (null == STATaskScheduler)
{
STATaskScheduler = new StaTaskScheduler(0);
HttpContext.Application["STATaskScheduler"] = STATaskScheduler;
}
我们使用一个瘦包装器来实例化并通过反射调用我们的COM库:
// Libraries is a Dictionary containing the names of the registered dlls
protected object InitCom(Libraries lib)
{
return InitCom(lib, true);
}
protected virtual object InitCom(Libraries lib, bool setOperation)
{
var comObj = GetComInstance(lib);
var success = SetUpConnection(comObj);
if (!success)
throw new LeafOperationException(lib, "Errore durante la connessione: {1}".Printf(connectionString));
if(setOperation)
return InitOperation(comObj);
return comObj;
}
protected object GetComInstance(Libraries lib)
{
var comType = Type.GetTypeFromProgID(MALib[lib]);
var comObj = Activator.CreateInstance(comType);
return comObj;
}
protected virtual bool DisposeCom(object comObj)
{
var success = CloseConnection(comObj);
if(!success)
throw new LeafOperationException("Errore durante la chiusura della connessione: {1}".Printf(connectionString));
//Marshal.FinalReleaseComObject(comObj);
//comObj = null;
return success;
}
protected bool SetUpConnection(object comObj)
{
var serverName = connectionString.ServerName();
var catalogName = connectionString.CatalogName();
return Convert.ToBoolean(comObj.InvokeMethod("Set_ConnectionWeb", serverName, catalogName));
}
protected bool CloseConnection(object comObj)
{
return Convert.ToBoolean(comObj.InvokeMethod("Close_ConnectionWeb"));
}
protected object InitOperation(object comObj)
{
comObj.GetType().InvokeMember("OperationID", BindingFlags.SetProperty, null, comObj, new object[] { OperationId });
comObj.GetType().InvokeMember("OperationHash", BindingFlags.SetProperty, null, comObj, new object[] { OperationHash });
return comObj;
}
这背后的基本原理是我们使用每个请求创建一个类的新实例,最终在完成时释放它。阅读here,了解我们为什么评论了ReleaseComObject
部分。基本上,我们为了很多COM object that has been separated from its underlying RCW cannot be used
例外交易而没有内存。
然后在各种类的方法中使用这个对象:
public bool ChiusuraMese()
{
try
{
PulisciMessaggi();
var comObj = InitCom(Libraries.Chiusura);
var byRefArgs = new int[] { 2 };
var oReturn = comObj.InvokeMethodByRef("ChiusuraMese", byRefArgs, IdDitta, PeriodoGiornaliera, IdDipendenti.PadLeft(), IdGruppoInstallazione, CodGruppoGestione);
DisposeCom(comObj);
return Convert.ToInt32(oReturn) == 0;
}
catch (Exception ex)
{
using (ErrorLog Log = new ErrorLog(System.Reflection.Assembly.GetExecutingAssembly().FullName, ex)) { }
aErrorMessage = ex.Message;
return false;
}
}
其中InvokeMethodByRef
是以这种方式定义的扩展方法:
public static object InvokeMethodByRef(this object comObj, string methodName, int[] byRefArgs, params object[] args)
{
var modifiers = new ParameterModifier(args.Length);
byRefArgs.ToList().ForEach(index => { modifiers[index] = true; });
return comObj.GetType().InvokeMember(methodName, BindingFlags.InvokeMethod, null, comObj, args, new ParameterModifier[] { modifiers }, null, null);
}
根据我的理解,整个公寓的东西真的很难做到,它的跨线程编组,消息循环,yadda yadda诸如此类。除此之外,我们还使用旧的,不受支持的技术来开发一个没有为我们强制要求的目的而构建的应用程序。所有这一切,并认为.Net方面的事情正常工作,一些想法仍然在我们的脑海中浮现。特别是:
在您指向在线资源之前,我应该阅读,我在这里添加了一些我遇到过的随机顺序的主题:
http://www.vb-helper.com/howto_activex_dll.html
https://msdn.microsoft.com/en-us/library/aa242108(v=vs.60).aspx
如果我们想坚持使用带有表单的ActiveX DLL,那么这里选择的确不多。
https://msdn.microsoft.com/en-us/library/aa716297(v=vs.60).aspx
https://msdn.microsoft.com/en-us/library/aa716228(v=vs.60).aspx
。顺便说一句,这个可能暗示对对象的调用正被序列化以供其他线程访问。https://msdn.microsoft.com/en-us/library/windows/desktop/ms680112%28v=vs.85%29.aspx?f=255&MSPPError=-2147217396
https://msdn.microsoft.com/en-us/library/aa241684(v=vs.60).aspx
https://msdn.microsoft.com/en-us/library/aa716193%28v=vs.60%29.aspx?f=255&MSPPError=-2147217396
当我们遇到错误时,堆栈转储可以提供任何帮助吗?我甚至不知道如何使用WinDbg,所以我至少要知道这是否会浪费时间:D
我们有点被困在这里,因为我们不知道在哪里或者在寻找什么,所以任何形式的帮助都会非常感激。
所以我已经指出我应该阅读有关COM的线程模型的更多信息。我有点期待。无论如何,进一步详细说明,让我写一些评论。
首先,我无法控制CoInitialize
或其他什么,我只是实例化一些VB6 dll。我猜COM正在做这样的事情。事实是,我找不到任何地方(编辑 - 显然,.Net已经在为我处理这个问题,请看这个问题的答案:Do i need to call CoInitialize before interacting with COM in .NET? )。
回顾一下:
Activator.CreateInstance
假设它每次调用时实际上都在创建一个新对象。调用是在新的STA线程内完成的。让我们暂时搁置有关实际DLL中线程安全性的问题。我在这里理解的主要原因是,所描述的解决方案是一种正确的方式(可能不是最好的方式,我意识到这一点)来利用COM库进行多线程处理。
引用一些消息来源,就我目前所知,我应该处于图8.5所示的情况:https://msdn.microsoft.com/en-us/library/aa716228(v=vs.60).aspx
我无法找到任何原因,为什么这不起作用,因为我说我假设每个对象都在自己的公寓里,并有自己的变量,加上一个全局变量的副本(见这里:{{ 1}})。