Потокобезопасность DLR.
Ноябрь 15, 2009 – 1:20 дпПоследние 3 месяца были потрачены на одну из частей распределенной системы, выполняющей некоторые бизнес-задачи. В связи с тем, что проект начат недавно, было принято решение об использовании DLR+IronPython в полный рост. Эта компонента не стала исключением. Итого 1 месяц потрачено на реализацию, 2 месяца – на функциональное, интеграционное, нагрузочное тестирование и доводку. Рабочая среда представляет собой сервер с двумя процессорами Opteron. И вот тут начались проблемы – посыпались ошибки типа
IronPython.Runtime.Exceptions.ImportException: Cannot import name Struct в IronPython.Runtime.Importer.ImportFrom(CodeContext context, Object from, String name) в Microsoft.Scripting.Utils.InvokeHelper`4.Invoke(Object arg0, Object arg1, Object arg2) в Microsoft.Scripting.Interpreter.CallInstruction.Run(InterpretedFrame frame) в Microsoft.Scripting.Interpreter.Interpreter.RunInstructions(InterpretedFrame frame) в Microsoft.Scripting.Interpreter.Interpreter.Run(InterpretedFrame frame) в Microsoft.Scripting.Interpreter.LightLambda.Run2[T0,T1,TRet](T0 arg0, T1 arg1) в IronPython.Compiler.PythonScriptCode.Run(Scope scope) в IronPython.Compiler.RuntimeScriptCode.InvokeTarget(LambdaExpression code, Scope scope) в Microsoft.Scripting.Hosting.ScriptSource.Execute(ScriptScope scope) ..... System.Exception: Can't pickle IronPython.Runtime.Types.BuiltinFunction: it's not the same object as copy_reg._reconstructor в Microsoft.Scripting.Actions.Calls.MethodCandidate.Caller.Call(Object[] args, Boolean& shouldOptimize) в IronPython.Runtime.Types.BuiltinFunction.BuiltinFunctionCaller`2.Call1(CallSite site, CodeContext context, TFuncType func, T0 arg0) в Microsoft.Scripting.UpdateDelegates.UpdateAndExecute3[T0,T1,T2,TRet](CallSite site, T0 arg0, T1 arg1, T2 arg2) в serialize$67(Closure , PythonFunction , Object ) в IronPython.Compiler.PythonCallTargets.OriginalCallTarget1(PythonFunction function, Object arg0) в CallSite.Target(Closure , CallSite , Object , Object ) в Microsoft.Scripting.UpdateDelegates.UpdateAndExecute2[T0,T1,TRet](CallSite site, T0 arg0, T1 arg1) в _Scripting_(Object[] , Object ) .....
Стек исключения явно указывает на то, что ошибки где-то в самом ядре DLR или IronPython и они явно происходят из-за многопроцессорности системы. В результате был написан минимальный код, на котором воспроизводятся подобные ошибки на многопроцессорных конфигурациях и Core2 Quad.
Когда только была обнаружена данная проблема, в список рассылки IronPython было написано письмо с описанием (тестовый код ещё не был подготовлен). Ответ пришел от Dino Viehland, который заключался в следующем:
… Executing Struct’s definition multiple times might cause the exceptions you’re seeing but I don’t know why it would get executed multiple times. …
По мимо этого он просил подготовить тестовый код, где можно было бы проанализировать сколько раз происходит определение класса Struct.
Тестовый код представляет собой консольное приложение, в котором имеется класс IronPythonHelper.
public static class IronPythonHelper
{
public static ScriptEngine CreateScriptEngine(IEnumerable<string> paths)
{
Console.WriteLine("Call IronPythonHelper.CreateScriptEngine");
var langSetup = ScriptRuntimeSetup.ReadConfiguration();
var scriptEngine = new ScriptRuntime(langSetup).GetEngine("IronPython");
var path = scriptEngine.GetSearchPaths();
path.Extend(paths);
scriptEngine.SetSearchPaths(path);
return scriptEngine;
}
public static byte[] CPickleSerialize(ScriptEngine engine, object obj)
{
var scope = engine.CreateScope();
engine.Execute("import cPickle", scope);
engine.Execute("def serialize( obj ) : return cPickle.dumps( obj )", scope);
var serialize = scope.GetVariable<Func<object, string>>("serialize");
return Encoding.Unicode.GetBytes(serialize(obj));
}
public static object PackParams(ScriptEngine engine, IDictionary<string, object> param)
{
var scope = engine.CreateScope();
engine.Execute("from utils import Struct", scope);
engine.Execute("def pack( params ) : return Struct(**dict(params))", scope);
var pack = scope.GetVariable<Func<IDictionary<string, object>, object>>("pack");
return pack(param);
}
}
Метод IronPythonHelper.CreateScriptEngine говорит сам за себя. Единственно, хочу заметить, что в нем расширяется список путей поиска модулей по средством добавления новых (Extend является extension-методом, который просто добавляет в коллекцию новые элементы). Метод CPickleSerialize используется для сериализации объектов в формат cPickle. PackParams необходим для построения python-класса Struct. Модуль utils.py:
class Struct ( object ) :
def __init__ ( self, **kwds ) :
for name, val in kwds.iteritems() :
setattr( self, name, val )
print ' load Struct! '
В основном потоке тестового кода создавалось большое количество задач и помещались в System.Threading.ThreadPool. Исполняемый метод задачи имел вид:
public void Execute(object obj)
{
InvokeOnStart();
try
{
new Info(1).ToString(engine);
}
catch (Exception e)
{
Console.WriteLine(e);
}
finally
{
InvokeOnFinish();
}
}
Класс Info:
public class Info
{
public Info(int x)
{
X = x;
}
public int X { get; private set; }
public byte[] ToString(ScriptEngine engine)
{
var param = new Dictionary<string, object>
{
{"x", X}
};
return IronPythonHelper.CPickleSerialize(engine, IronPythonHelper.PackParams(engine, param));
}
}
В результате экспериментов было выяснено, что при запуске 50 задач на одноядерных (AMD Athlon 3000+) и двухядерных (Core2 Duo) процессорах проблема не проявляется, хотя загрузка модуля utils.py происходит 2 раза. На машинах с процессором Core2 Quad проблема начинает проявлятся – utils.py загружается 4 раза. На двухпроцессорных машинах (Opteron или Xeon) проблема остается – utils.py загружается 6-7 раз.
В ходе дальнейших экспериментов были предприняты попытки реализовать декоратор над ScriptEngine и ScriptScope классами, который блокировал обращения к ним через монитор, однако это не привело к желанному результату – ошибки остались, однако начали вылезать в другом месте: в момент вызова делегатов, полученных с помощью метода ScriptScope.GetMember().
Таким образом, налицо проблема с многопоточностью в DLR или IronPython. На данный момент эта проблема решена очень просто – введен пул объектов ScriptEngine, что решило проблему в указанных в начале условиях, но не решило её в общем.