Как сделать код 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...