How to make C (P/invoke) code called from C# “Thread-safe”

后端 未结 4 711
长情又很酷
长情又很酷 2021-02-08 06:43

I have some simple C-code which uses a single global-variable. Obviously this is not thread-safe, so when I call it from multiple threads in C# using P/invoke, things screw up.

相关标签:
4条回答
  • 2021-02-08 06:49

    A common pattern is to have

    • a function that allocates memory for the state,
    • a function that has no side-effects but mutating the passed-in state, and
    • a function that releases the memoy for the state.

    The C# side would look like this:

    Usage:

    var state = new ThreadLocal<SomeSafeHandle>(NativeMethods.CreateSomeState);
    
    Parallel.For(0, 100, i =>
    {
        var result = NativeMethods.SomeFunction(state.Value, i, 42);
    
        Console.WriteLine(result);
    });
    

    Declarations:

    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 magic:

    [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;
        }
    }
    
    0 讨论(0)
  • 2021-02-08 06:52

    The good news, you can create a __declspec(naked) function as a member of C++ (non-CLI) class:

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

    The bad news, you will need COM to be able to use such class. That's right: asm wrapped in C++, wrapped in COM, wrapped in RCW, wrapped in CLR...

    0 讨论(0)
  • 2021-02-08 06:58

    You can either make sure what you only call _someFunction once at a time in your C# code or change the C code to wrap the access to the global variable in a synchronization primitive like a critical section.

    I would recommend changing the C# code rather than the C code, as the C# code is multi-threaded, not the C code.

    0 讨论(0)
  • 2021-02-08 07:03

    Personally if the C code was to be called elsewhere I would use a mutex there. If that doesn't float your boat you can lock in .Net quite easily:

    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);
    

    [Edit..]

    As pointed out, this serializes access to the function which you can't do yourself in this case. You have some C/C++ code that (wrongly IMO) uses a global for state during the call to the exposed function.

    As you have observed that the __declspec(thread) trick doesn't work here then I would try to pass your state/context back and forth as an opaque pointer like so:-

    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);
        }
    }
    

    And the corresponding C# wrapper as before:

    [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);
    
    0 讨论(0)
提交回复
热议问题