【问题标题】:How to make C (P/invoke) code called from C# "Thread-safe"如何制作从 C#“线程安全”调用的 C (P/invoke) 代码
【发布时间】:2012-04-30 18:04:11
【问题描述】:

我有一些使用单个全局变量的简单 C 代码。显然这不是线程安全的,所以当我使用 P/invoke 从 C# 中的多个线程调用它时,事情就搞砸了。

如何为每个线程单独导入此函数,或使其成为线程安全的?

我尝试声明变量__declspec(thread),但这导致程序崩溃。我也尝试过创建一个 C++/CLI 类,但它不允许成员函数为 __declspec(naked),这是我需要的(我正在使用内联汇编)。我在编写多线程 C++ 代码方面不是很有经验,所以我可能缺少一些东西。


下面是一些示例代码:

C#

[DllImport("MyDll.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern int SomeFunction(int parameter1, int parameter2);

C++

extern "C"
{
    int someGlobalVariable;
    int __declspec(naked) _someFunction(int parameter1, int parameter2)
    {
        __asm
        {
            //someGlobalVariable read/written here
        }
    }
    int __declspec(dllexport) SomeFunction(int parameter1, int parameter2)
    {
        return _someFunction(parameter1, parameter2);
    }
}

[编辑]SomeFunction() 的结果必须按照基于someGlobalVariable 的规定顺序进行(例如,一个 PRNG,someGlobalVariable 作为内部状态)。因此,不能选择使用互斥锁或其他类型的锁 - 每个线程必须拥有自己的 someGlobalVariable 副本。

【问题讨论】:

  • 你多久调用一次 - 如果你不拥有 C 代码,你能在 C# 端进行锁定吗?
  • @SteveTownsend:不,大多数多线程都在调用该函数。
  • 必须是全局变量吗?您可以从函数返回状态,并要求在下次调用时将状态传递回函数。 (或指向状态的指针。)
  • 尝试在 SomeFunction 上使用 [MethodImpl(MethodImplOptions.Synchronized)] 属性
  • 如果你不想放弃全局变量,不想同步方法调用,那么唯一的选择就是真正拥有多个进程(一个全局变量每个地址空间存在一次)。例如,您可以多次启动您的 C# 程序并实现一个 TPL TaskFactory,该任务工厂在此进程池上安排任务。

标签: c# c multithreading pinvoke


【解决方案1】:

一个常见的模式是有

  • 为状态分配内存的函数,
  • 一个没有副作用但会改变传入状态的函数,以及
  • 释放状态内存的函数。

C# 端如下所示:

用法:

var state = new ThreadLocal<SomeSafeHandle>(NativeMethods.CreateSomeState);

Parallel.For(0, 100, i =>
{
    var result = NativeMethods.SomeFunction(state.Value, i, 42);

    Console.WriteLine(result);
});

声明:

internal static class NativeMethods
{
    [DllImport("MyDll.dll", CallingConvention = CallingConvention.Cdecl)]
    public static extern SomeSafeHandle CreateSomeState();

    [DllImport("MyDll.dll", CallingConvention = CallingConvention.Cdecl)]
    public static extern int SomeFunction(SomeSafeHandle handle,
                                          int parameter1,
                                          int parameter2);

    [DllImport("MyDll.dll", CallingConvention = CallingConvention.Cdecl)]
    internal static extern int FreeSomeState(IntPtr handle);
}

SafeHandle 魔法:

[SecurityPermission(SecurityAction.InheritanceDemand, UnmanagedCode = true)]
[SecurityPermission(SecurityAction.Demand, UnmanagedCode = true)]
internal class SomeSafeHandle : SafeHandle
{
    [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
    public SomeSafeHandle()
        : base(IntPtr.Zero, true)
    {
    }

    public override bool IsInvalid
    {
        get { return this.handle == IntPtr.Zero; }
    }

    [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
    protected override bool ReleaseHandle()
    {
        return NativeMethods.FreeSomeState(this.handle) == 0;
    }
}

【讨论】:

    【解决方案2】:

    您可以确保在 C# 代码中一次只调用一次 _someFunction,或者更改 C 代码以将对全局变量的访问封装在一个同步原语中,如临界区。

    我建议更改 C# 代码而不是 C 代码,因为 C# 代码是多线程的,而不是 C 代码。

    【讨论】:

      【解决方案3】:

      就个人而言,如果要在其他地方调用 C 代码,我会在那里使用互斥锁。如果这不能让你的船浮起来,你可以很容易地锁定 .Net:

      static object SomeFunctionLock = new Object();
      
      public static int SomeFunction(int parameter1, int parameter2){
        lock ( SomeFunctionLock ){
          return _SomeFunction( parameter1, parameter2 );
        }
      }
      
      [DllImport("MyDll", CallingConvention = CallingConvention.Cdecl)]
      internal static extern int _SomeFunction(int parameter1, int parameter2);
      

      [编辑..]

      正如所指出的,这会序列化对在这种情况下您无法自己执行的功能的访问。您有一些 C/C++ 代码(错误地 IMO)在调用公开函数期间使用全局状态。

      正如您所观察到的,__declspec(thread) 技巧在这里不起作用,那么我会尝试将您的状态/上下文作为不透明指针来回传递,如下所示:-

      extern "C" 
      {
          int _SomeOtherFunction( void* pctx, int p1, int p2 )
          { 
              return stuff;
          }
      
          // publically exposed library function
          int __declspec(dllexport) SomeFunction(int parameter1, int parameter2)
          {
              StateContext ctx;
              return _SomeOtherFunction( &ctx, parameter1, parameter2 );
          }
      
          // another publically exposed library function that takes state
          int __declspec(dllexport) SomeFunctionWithState(StateContext * ctx, int parameter1, int parameter2)
          {
              return _SomeOtherFunction( ctx, parameter1, parameter2 );
          }
      
          // if you wanted to create/preserve/use the state directly
          StateContext * __declspec(dllexport) GetState(void) {
              ctx = (StateContext*) calloc( 1 , sizeof(StateContext) );
              return ctx;
          }
      
          // tidy up
          void __declspec(dllexport) FreeState(StateContext * ctx) {
              free (ctx);
          }
      }
      

      和之前对应的C#包装器:

      [DllImport("MyDll", CallingConvention = CallingConvention.Cdecl)]
      internal static extern int SomeFunction(int parameter1, int parameter2);
      
      [DllImport("MyDll", CallingConvention = CallingConvention.Cdecl)]
      internal static extern int SomeFunctionWithState(IntPtr ctx, int parameter1, int parameter2);
      
      [DllImport("MyDll", CallingConvention = CallingConvention.Cdecl)]
      internal static extern IntPtr GetState();
      
      [DllImport("MyDll", CallingConvention = CallingConvention.Cdecl)]
      internal static extern void FreeState(IntPtr);
      

      【讨论】:

      • 这基本上会序列化我的线程。难道没有办法让全局变量存储在线程本地存储中吗?
      • 不改C代码?不。即使每个 C# 线程都可以存储一个本地副本,但与其他修改 C“主副本”的线程相比,仍然没有防火墙。
      • @SteveTownsend:我不介意更改 C 代码;但是,我不知道该怎么办。正如我所提到的,__declspec(thread) 似乎不起作用。我需要每个线程的变量副本;互斥锁或其他类型的锁不是一种选择。我想我应该提到,SomeFunction() 的结果必须按照基于someGlobalVariable 的规定顺序进行(想想例如一个PRNG,someGlobalVariable 作为种子/内部状态)
      • 那么你应该能够做到这一点,如果当前托管线程没有值,则使用适合首次初始化的逻辑。即便如此,这也很复杂,因为托管线程 (C#) 和本机线程不是 1-1 - 请参阅此处stackoverflow.com/questions/1279950/…
      • @Steve:是的,正如我提到的,它就像 PRNG 的内部状态 - 我不是 天真 :) 我不明白你的建议 - 如何“如果当前托管线程没有值,则为首次初始化使用合适的逻辑” 为每个线程获取一个单独的实例?
      【解决方案4】:

      好消息,您可以创建一个 __declspec(naked) 函数作为 C++(非 CLI)类的成员:

      class A {
          int n;
      public:
          A() { n = 0; }
          void f(int n1, int n2);
      };
      
      __declspec(naked) void A::f(int n1, int n2)
      {
          n++;
      }
      

      坏消息,你需要 COM 才能使用这样的类。没错:asm用C++封装,用COM封装,用RCW封装,用CLR封装……

      【讨论】:

        猜你喜欢
        • 2023-04-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-04-03
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多