反向P / Invoke(也)管理回调到非托管代码

时间:2012-01-07 15:06:18

标签: .net callback interop unmanaged managed

包含的C#单元测试和C代码文件尝试将托管回调传递给非托管代码。代码实际运行但count变量从不递增。所以测试失败了。

它运行的事实意味着它确实加载了dll,找到了DoCallBack()方法的引用,它似乎调用了该​​方法。但没有任何反应。所以有些事情已经结束了。

您可能想知道为什么要尝试这样做?你想知道是否有更好的方法?好吧,最终目标是创建一个“hack”,以便在AppDomains上运行线程的性能几乎与在同一个域中相同。

在以下链接中,您将发现目前为止更快的AppDomain交叉技术。 MS .Net AddIn团队提供“FastPath”,与简单的远程处理性能相比有很多改进。我们在.Net 3.5上运行了他们的示例,并且在将他们的AddIn合同放入GAC之后它的工作非常快。

http://blogs.msdn.com/b/clraddins/archive/2008/02/22/add-in-performance-what-can-you-expect-as-you-cross-an-isolation-boundary-and-how-to-make-it-better-jesse-kaplan.aspx

现在让我们讨论一些时间对比,看看为什么它仍然不够快,不能满足我们的需求。正常的跨域远程处理在零参数的方法上每秒提供大约10,000次调用。使用FastPath选项,每秒可增加200,000个呼叫。但是将它与调用具有零参数的接口方法的C#(在同一域中)进行比较,它在与同一机器相同的机器上每秒执行超过160,000,000次操作。

因此,即使FastPath技术仍然比简单的接口方法调用慢1000倍。但为什么我们需要更好的表现?

或者性能要求是从使用多核和分布式技术在几分钟内处理数十亿元组信息的CPU绑定应用程序中删除所有软件瓶颈。

但新的功能要求是能够提供AddIn或Plugin architure,以便无需停止系统的其余部分即可加载或卸载组件。在.Net上有效实现这一目标的唯一方法是使用单独的AppDomains。

请注意,我们不希望在AppDomains之间传递数据,它们都是并行独立运行。

但就线程而言,在数百个AppDomain中运行单独的线程效率非常低。如果是这样,他们就会争夺CPU并导致上下文切换带来巨大的性能损失。

所以再一次,这里的计划是拥有一个主域或主域,它有一个线程池,并轮流调用每个有工作要做的AppDomain,让它工作一段时间。这意味着合作多线程(以避免上下文切换)。因此,AppDomains将返回以允许主AppDomain转移到其他人。

不幸的是,每个AppDomain在失去工作之前不能独立运行很长时间并且需要返回主域以让不同的AppDomain工作。因此,FastPath技术每秒200,000的性能时间将导致由于跨AppDomain调用导致整体性能显着下降。

与下面的PInvoke相比,我们用StopWatch测量了时间,产生了超过90,000,000 - 与同一台机器上的每秒9000万次呼叫。所以希望通过反向P / Invoking到另一个AppDomain,它仍然允许每秒数百万次操作。

每秒9000万,更接近我们在AppDomains之间切换线程的需求。

好。现在回到这个单元测试。这个简单的单元测试的目的是首先获得从非托管代码到托管代码工作的简单回调....之后,下一步将创建一个单独的AppDomain并获取委托回调并传递给非托管代码测试跨域回调性能。

我们知道所有这些都是可能的,我们会在网上看到讨论和示例......但是下面的代码看起来很简单......它只是没有按预期工作。

这是使用/ CLR命令行选项构建为DLL的非托管代码:

#include <windows.h>

typedef void (CALLBACK *PFN_MYCALLBACK)();
int count = 0;

extern "C" {
    __declspec(dllexport) void __stdcall DoSomeStuff() {
        ++count;
    }
}

extern "C" {
    __declspec(dllexport) void __stdcall DoCallBack(PFN_MYCALLBACK callback) {
        PFN_MYCALLBACK();
    }
}

这是C#单元测试代码。

using System.Runtime.InteropServices;
using System.Security;
using NUnit.Framework;

namespace TickZoom.Callback
{
    [SuppressUnmanagedCodeSecurity]
    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    public delegate void MyCallback();

    [TestFixture]
    class CrossAppDomainTesting
    {
        public int count;
        [Test]
        public void TestCallback()
        {
            NativeMethods.DoCallBack(
                delegate()
                {
                    ++count;
                });
            Assert.AreEqual(1, count);
        }

        [Test]
        public void TestDate()
        {
            NativeMethods.DoSomeStuff();
        }
    }

    public static class NativeMethods
    {
        [SuppressUnmanagedCodeSecurity]
        [DllImport("CrossAppDomain.dll")]
        public static extern void DoSomeStuff();

        [SuppressUnmanagedCodeSecurity]
        [DllImport("CrossAppDomain.dll")]
        public static extern void DoCallBack(MyCallback callback);

    }
}

1 个答案:

答案 0 :(得分:2)

该职位的第一位评论者解决了编码问题。我等他发布它作为回答给他信用。如果他这样做,那么我会。

这只是你的DoCallback函数中的错字吗?你有PFN_MYCALLBACK()。我想你想要的 它是回调()。 - Jim Mischel

此外,时间的结果使得从一个AppDomain调用到另一个AppDomain的最快方式如下:

首先调用非托管方法将委托发送到非托管代码,该代码被封送到函数指针。

从那时起,你调用非托管代码没有任何参数,但重用函数指针调用其他AppDomain。

我们的测试显示,每秒9千万次呼叫,而接口上的简单C#呼叫每秒3亿次,或者每秒8000万次Interlocked.Increment()。

换句话说,这足以经常发生以跨越AppDomain边界转换线程。

注意:有几点需要注意。如果您保留指向卸载的AppDomains的指针,然后尝试调用它们,则会收到有关收集的委托的例外情况。原因是CLR给你的函数指针不是代码中的简单函数指针。相反,它是一个指向“thunking”代码的指针,该代码首先检查代理是否仍然存在,并且它为从非托管代码到托管代码的转换做了一些管理。

我们的计划是为每个AppDomain分配一个整数句柄。然后,非托管代码将获得“句柄”和函数指针以放入数组。

当我们卸载AppDomain时,我们还会通知非托管代码删除该句柄的函数指针。我们将释放的'handle'保留在一个已释放的列表中,以便为下一个创建的AppDomain重用。