Почему вызов лямбда-выражения Python из С# не является потокобезопасным?
Я определяю безболевое (чистое) лямбда-выражение в IronPython и назначаю его делегату С#. При вызове делегата одновременно из нескольких потоков я получаю исключения типа AccessViolationException, NullReferenceException и FatalEngineExecutionError.
Происшествие ошибки не является детерминированным, и в большинстве случаев требуется несколько миллионов итераций, чтобы спровоцировать его, что говорит о "состоянии гонки" для меня. Как я могу избежать этого?
Исключения возникают только при запуске процесса с x64 (x86 не сбой) и вне отладчика. Система тестирования - Core I7 (8 потоков) для Windows 7,.NET Framework 4.0 и IronPython 2.7.1.
Здесь минимальный код для создания ошибки:
var engine = Python.CreateEngine();
double a = 1.0;
double b = 2.0;
while (true)
{
Func<double, double, double> calculate = engine.Execute("lambda a,b : a+b");
System.Threading.Tasks.Parallel.For(0, 1000, _ =>
{
for (int i = 0; i < 1000; i++) { calculate(a,b); }
});
Console.Write(".");
}
Сообщение об ошибке:
обнаружено FatalExecutionEngineError
Сообщение: среда выполнения столкнулась с фатальной ошибкой. Адрес ошибки был равен 0xf807829e, в потоке 0x3da0. Код ошибки: 0xc0000005. Эта ошибка может быть ошибкой в CLR или в небезопасных или не поддающихся проверке частях кода пользователя. Общие источники этой ошибки включают ошибки маршалинга пользователя для COM-interop или PInvoke, которые могут повредить стек.
Обновление: даже если движок объявлен как локальный поток, он через некоторое время сработает:
var calculate = new ThreadLocal<Func<double, double, double>>(() => Python.CreateEngine().Execute("lambda a,b : a+b"));
Ответы
Ответ 1
Вероятно, это связано с состоянием гонки в ScriptEngine. Обратите внимание, что ScriptEngine.Execute возвращает ссылку на PythonFunction, а не на Func (из-за динамического поведения С#, что вы можете рассматривать результат как Func. Я не специалист по IronPython, но, глядя на источник PythonFunction, есть нет никаких указаний, что это потокобезопасно.
Ответ 2
Вы пытались заменить 64-битный тип данных (double) на 32-битный тип данных (float)? Я знаю, что "Простое чтение или запись в поле из 32 бит или меньше всегда является атомарным" , тогда как поле с 64-разрядным битом может иметь проблемы в зависимости от на процессор и код. Это похоже на длинный выстрел, но стоит попробовать.
Ответ 3
Запуск аналогичного кода (см. ниже) на IronScheme не вызывает таких проблем.
Единственное, о чем я могу думать, это то, что engine.Execute("lambda a,b : a+b")
создает интерпретированную функцию вместо скомпилированной. Объедините это с потенциально небезопасным интерпретатором и kaboom.
double a = 1.0;
double b = 2.0;
while (true)
{
var calculate = "(lambda (a b) (+ a b))".Eval<Callable>();
System.Threading.Tasks.Parallel.For(0, 1000, _ =>
{
for (int i = 0; i < 1000; i++) { calculate.Call(a, b); }
});
Console.Write(".");
}