Как сделать код C (P/invoke), вызванный из С# "Thread-safe"

У меня есть простой C-код, который использует одну глобальную переменную. Очевидно, что это не потокобезопасно, поэтому, когда я называю это из нескольких потоков в С#, используя P/invoke, все испортится.

Как я могу либо импортировать эту функцию отдельно для каждого потока, либо сделать его потокобезопасным?

Я попытался объявить переменную __declspec(thread), но это вызвало сбой программы. Я также попытался создать класс С++/CLI, но он не позволяет функциям-членам __declspec(naked), которые мне нужны (я использую встроенную сборку). Я не очень опытен, написав многопоточный код на С++, поэтому может быть что-то, что мне не хватает.


Вот пример кода:

С#

[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.

Ответ 1

Общим шаблоном является

  • функция, которая выделяет память для состояния,
  • функция, которая не имеет побочных эффектов, но мутирует состояние переданного состояния и
  • функция, которая освобождает память для состояния.

Сторона С# будет выглядеть так:

Использование:

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 должен был вызываться в другом месте, я бы использовал мьютекс. Если вы не плаваете на своей лодке, вы можете легко заблокировать .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);

[Edit..]

Как указано, это сериализует доступ к функции, которую вы не можете сделать сами в этом случае. У вас есть код 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);
    }
}

И соответствующая оболочка С# как раньше:

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

Ответ 3

Вы можете либо удостовериться, что вы только вызываете _someFunction один раз за раз в коде С#, либо изменяете код C, чтобы обернуть доступ к глобальной переменной в примитиве синхронизации, как в критическом разделе.

Я бы рекомендовал изменить код С#, а не код C, поскольку код С# многопоточен, а не код C.

Ответ 4

Хорошие новости, вы можете создать функцию __declspec(naked) как член класса С++ (не 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, завернутый в С++, завернутый в COM, завернутый в RCW, завернутый в CLR...