更新2

排队问题可能已经解决了,因为我们仍然面临着并发可能是这种明显行为的原因,但是对于了解VB6中使用的类,模块和变量的内部工作原理有帮助 . 出现一个问题:是否将类中的所有内容(连接,组件等)封装起来,确保每个创建的对象都不与其他实例共享任何数据?

更新1

我们对应用程序进行了多次重构以应对资源处理,尤其是在处理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

理解废话

长话短说,经过大量的挖掘和头痛,我们设计了当前的解决方案,概述如下:

  • 我们像往常一样安装遗留应用程序,以便在注册表中注册其dll;

  • 在我们的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方面的事情正常工作,一些想法仍然在我们的脑海中浮现 . 特别是:

  • 这是获取COM多线程的正确方法吗?有时,对同一对象的多个请求会像排队一样卡住 . 这让我们想知道COM是否实际上在线程之间共享一些实例;

  • 我们是否真的在每个请求中创建和处理对象,或者在引擎盖下COM以不同的方式处理事物?显然,我们可能会出现一些资源争用并重新进入某个我们不希望的地方;

  • 设置正确吗?是否有更容易维护和调试的替代方案?请记住,我们不要指望这一点 .

  • 在这种项目中使用OCX的方式是什么"least worse"(目前还不是使用它们)?我们应该以某种特定方式处置它们吗?我们已经检查过,在完成后我们将它们设置为空,但也许其他一些线程仍在使用它们;

  • 我们应该知道与内存不足问题相关的任何特定COM限制吗?我们在表单显示超过256个唯一控件之前遇到了问题 . 也许在某种程度上发生了同样的事情?该错误似乎与使用UI组件的类特别相关 .

我已经读过的东西(可能还不明白)

在你指向在线资源之前我应该阅读,我在这里添加一些我遇到的主题,按随机顺序:

关于SingleUse / MultiUse

  • 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正在做这样的事情 . 事实是,我找不到任何地方( edit - apparently, .Net is already taking care of that for me, see the answer to this question: Do i need to call CoInitialize before interacting with COM in .NET? ) .

回顾一下:

  • 我正在使用来自客户端应用程序的STA线程

  • 我正在使用 Activator.CreateInstance ,假设它实际上是在每次调用时创建一个新对象 . 该调用在新的STA线程内完成 .

让's set aside for a moment questions about thread-safety in the actual DLLs. What I' m主要感兴趣的是,如果所描述的解决方案是一种正确的方式(可能不是最好的方式,我知道这一点)来利用COM库进行多线程处理 .

引用一些来源,就我目前所知,我应该处于图8.5所示的情况: https://msdn.microsoft.com/en-us/library/aa716228(v=vs.60).aspx 我可以't find any reason why this should not work, since as I said I'假设每个对象都驻留在它自己的公寓中并且有自己的变量,加上一个全局变量的副本(见这里: https://msdn.microsoft.com/en-us/library/aa261361(v=vs.60).aspx ) .