Потокобезопасность 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, что решило проблему в указанных в начале условиях, но не решило её в общем.



Добавить комментарий: