我有一个遗留的VB6应用程序,其中我们创建一个 Task 对象来执行我们收到事件后完成的工作,然后我订阅了这个任务的事件,最后我 WaitOneAutoResetEvent 我用来控制线程 . 我还将任务 ContinueWith 设置为 Set AutoResetEvent .

当我使用控制台应用程序(用.NET编写)测试此设置时,我没有一个问题 . 但是,当我通过VB6调用相同的方法时,有时它会工作,有时它不会,并且只会无限期地挂起等待该事件触发 .

我一直在使用WinDbg分开它,我注意到所有挂起的尝试,都会抛出访问冲突 . 我正在试图找出当前这种访问冲突的来源,但是在.NET代码中我们调用事件委托(如果不是null)的那一点是正确的 .

以下是来自WinDbg的示例:

实例化对象>>>启动事件线程>>>订阅OnCreateCtiConf >>>等待任务结果 . >>>排队OnCreateCTIConf:无消息(1e44.1c4c):访问冲突 - 代码c0000005(第一次机会)在任何异常处理之前报告第一次机会异常 . 可以预期和处理此异常 . eax = 0a61e9d0 ebx = 0a65bccc ecx = 00000000 edx = 0a7057a4 esi = 0a61e9d0 edi = 0caef804 eip = 091249a4 esp = 0caef7e0 ebp = 0caef810 iopl = 0 nv up ei pl nz na po nc cs = 001b ss = 0023 ds = 0023 es = 0023 fs = 003b gs = 0000 efl = 00010202 091249a4 3909 cmp dword ptr [ecx],ecx ds:0023:00000000 = ????????

这是我们调用的方法有时会挂起:

public bool AgentLogin(string extension, string agentId, string password)
{
    _extension = extension;
    _agentId = agentId;
    _eventQueue = new Queue<AvayaEvent>();
    try
    {
        _phone = new CTISoftphone($"ext={extension},logFile={Settings.LogLocation}");
    }
    catch (Exception ex)
    {
        addEvent(new AvayaEvent(CTISoftphone.CTIEvent.OnError, $"{_wrapperLogPrefix} Unable to create CTISoftPhone. The error was: {ex.Message}"));
        return false;
    }

    _phone.PhoneEvent += new CTISoftphone.PhoneEventHandler((evnt, msg) =>
    {
        Trace.WriteLine($">>> Enqueued {evnt} : {(string.IsNullOrEmpty(msg) ? "No message" : msg)}");
        addEvent(new AvayaEvent(evnt, msg));
    });
    Trace.WriteLine(">>> Starting events thread");
    t_avayaEvents.Start();

    var are = new AutoResetEvent(false);
    var task = new Task(() =>
    {
        try
        {
            Trace.WriteLine(">>> Calling LoginAgent");
            _phone.LoginAgent(agentId, password);
        }
        catch (Exception)
        {
        }
    });
    task.ContinueWith((t) => are.Set());

    var task_handler = new OnCreateCtiConfHandler(task.Start);

    Trace.WriteLine(">>> Subscribing to OnCreateCtiConf");
    OnCreateCtiConf += task_handler;
    Trace.WriteLine(">>> Waiting for task result.");
    are.WaitOne();
    Trace.WriteLine(">>> Unsubscribing from OnCreateCtiConf");
    OnCreateCtiConf -= task_handler;
     return true;
}

我们最初认为在我们订阅公开的 PhoneEvent 之前错过了事件,因此事件队列循环线程 . 基本上我们将 AvayaEvent 添加到 Queue<T> ,并且一个单独的线程处理这些事件 . 如果有一个委托,它会触发并忘记,否则,该事件将被重新排队 . 作为参考,我们在实现事件队列之前仍然遇到了这个问题 .

对于COM互操作,我们做了一些事情:

  • 已为 Phone 对象及其事件(成员和事件的单独接口)创建了接口

  • ComVisible(true) 已设置在我们需要向COM公开的任何内容上

已在每个接口的所有成员上设置

  • DispId

我们的主要对象 Phone 是这样装饰的:

[ComSourceInterfaces(typeof(IClickToDialEvents))]
[ComDefaultInterface(typeof(IClickToDial))]
[ClassInterface(ClassInterfaceType.None)]
[ComVisible(true)]
public class Phone : IClickToDial
{
    [...]
}

引用的接口 IClickToDialIClickToDialEvents 的装饰方式如下:

[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface IClickToDial
{
    [...]
}

[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface IClickToDialEvents
{
    [...]
}

这些事件是 Phone 类的成员,下面是它们的样子:

[ComVisible(false)]
public delegate void OnCreateCtiConfHandler();

public event OnCreateCtiConfHandler OnCreateCtiConf;

进程事件循环如下所示:

private void processEventsInQueue()
{
    while (true)
    {
        if (_eventQueue?.Count > 0)
        {
            var evnt = getNextEvent();
            Trace.WriteLine($">>> Dequeued {evnt.Type}");
            switch (evnt.Type)
            {
                case CTISoftphone.CTIEvent.OnCreateCtiConf:
                    if (OnCreateCtiConf != null)
                        OnCreateCtiConf();
                    else
                        addEvent(evnt);
                    break;
                [...]
            }
         }
     }
 }

最后, addEventgetNextEvent 方法如下所示:

private AvayaEvent getNextEvent()
{
    lock (this)
        return _eventQueue.Dequeue();
}
private void addEvent(AvayaEvent evnt)
{
    lock (this)
    {
        try
        {
            _eventQueue.Enqueue(evnt);
        }
        catch (NullReferenceException)
        {
            //swallow the nullrefexception since we null out _eventQueue when we dont want to queue anything
        }
    }
}

最后,当我们用COM注册这个DLL时,我们使用以下RegAsm命令: .\RegAsm.exe 'C:\Program Files\Avaya\ClickToDial\<OurDll>.dll' /codebase /tlb 然后我们在VB6应用程序中引用生成的tlb文件 .

就像我说的,有时这很好,我们从AgentLogin方法返回没有问题 . Othertimes,我们无限期挂起,我总是看到这些挂起的访问冲突 . 我说分裂大约是50/50 .

这是我参与的第一个COM互操作项目,所以我可能错过了一些明显的东西,但这让我发疯了!如果需要更多信息/说明,请告诉我 .

在此先感谢您的帮助!