Thread Safety Bugs in RubyMetaBinderFactory.cs - Exception Thrown on Method's First Invocation
description
In our project, IronRuby intermittently throws an exception whenever a method is invoked for its first time. The behavior occurs when that first invocation is actually multiple parallel invocations. In other words, it occurs when a method has not yet been invoked and then is invoked from multiple threads simultaneously.
The problem seems to be in the IronRuby source file RubyMetaBinderFactory.cs. There are several methods (e.g. InteropReturn) that begin by checking a field for null. If the field is null, then a Dictionary instance is assigned to the field. The object referenced by that field is then used in a subsequent lock() block. This is not thread-safe. When there are multiple parallel invocations, it is possible that multiple Dictionaries will get created, and each thread may then lock on a different Dictionary instance. That allows multiple threads to enter the lock() block at the same time.
One solution is to use Interlocked.CompareExchange() to assign the Dictionary instance (instead of the plain assignment operator).
A workaround is to invoke each method once synchronously before allowing multithreaded invocations.
The attached ZIP file contains a project that reproduces the exception caused by the bug. The project also contains the workaround, commented out. Build the project, then run using the Run.bat batch file. The Run.bat will loop the executable until the problem occurs. The problem may occur on the first few runs, or it may take a few minutes. It also may depend on the number of cores in your machine. My test machine is a quad-core hyperthreaded Core i7-820QM.
Example exceptions follow:
System.ArgumentException: An item with the same key has already been added.
at System.ThrowHelper.ThrowArgumentException(ExceptionResource resource)
at System.Collections.Generic.Dictionary2.Insert(TKey key, TValue value, Boolean add)
at IronRuby.Runtime.Calls.RubyMetaBinderFactory.InteropReturn(CallInfo callInfo)
at IronRuby.Runtime.Calls.InteropBinder.InvokeMember.FallbackInvoke(DynamicMetaObject target, DynamicMetaObject[] args, DynamicMetaObject errorSuggestion)
at System.Dynamic.DynamicObject.MetaDynamic.<>c__DisplayClass12.<BindInvokeMember>b__11(DynamicMetaObject e)
at System.Dynamic.DynamicObject.MetaDynamic.BuildCallMethodWithResult(String methodName, DynamicMetaObjectBinder binder, Expression[] args, DynamicMetaObject fallbackResult, Fallback fallbackInvoke)
at System.Dynamic.DynamicObject.MetaDynamic.BindInvokeMember(InvokeMemberBinder binder, DynamicMetaObject[] args)
at System.Dynamic.InvokeMemberBinder.Bind(DynamicMetaObject target, DynamicMetaObject[] args)
at IronRuby.Runtime.Calls.RubyMetaBinder.InteropBind(MetaObjectBuilder metaBuilder, CallArguments args)
at IronRuby.Runtime.Calls.RubyMetaBinder.Bind(DynamicMetaObject scopeOrContextOrTargetOrArgArray, DynamicMetaObject[] args)
at Microsoft.Scripting.Utils.DynamicUtils.GenericInterpretedBinder1.Bind(DynamicMetaObjectBinder binder, Int32 countDown, Object[] args)
at Microsoft.Scripting.Utils.DynamicUtils.LightBind[T](DynamicMetaObjectBinder binder, Object[] args, Int32 compilationThreshold)
at IronRuby.Runtime.Calls.RubyMetaBinder.BindDelegate[T](CallSite1 site, Object[] args)
at System.Runtime.CompilerServices.CallSiteBinder.BindCore[T](CallSite1 site, Object[] args)
at System.Dynamic.UpdateDelegates.UpdateAndExecute2[T0,T1,TRet](CallSite site, T0 arg0, T1 arg1)
at Microsoft.Scripting.Interpreter.DynamicInstruction3.Run(InterpretedFrame frame)
at Microsoft.Scripting.Interpreter.Interpreter.Run(InterpretedFrame frame)
at Microsoft.Scripting.Interpreter.LightLambda.Run5[T0,T1,T2,T3,T4,TRet](T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4)
at System.Dynamic.UpdateDelegates.UpdateAndExecute4[T0,T1,T2,T3,TRet](CallSite site, T0 arg0, T1 arg1, T2 arg2, T3 arg3)
at lambda_method(Closure , DynamicOperations , CallSiteBinder , Object , Object[] )
at Microsoft.Scripting.Runtime.DynamicOperations.Invoke(Object obj, Object[] parameters)
at Microsoft.Scripting.Hosting.ObjectOperations.Invoke(Object obj, Object[] parameters)
at System.Dynamic.UpdateDelegates.UpdateAndExecute3[T0,T1,T2,TRet](CallSite site, T0 arg0, T1 arg1, T2 arg2)
System.ArgumentException: An item with the same key has already been added.
at System.ThrowHelper.ThrowArgumentException(ExceptionResource resource)
at System.Collections.Generic.Dictionary2.Insert(TKey key, TValue value, Boolean add)
at IronRuby.Runtime.Calls.RubyMetaBinderFactory.InteropTryGetMemberExact(String name)
at IronRuby.Runtime.Calls.RubyScopeMethodMissingInfo.BuildMethodMissingCallNoFlow(MetaObjectBuilder metaBuilder, CallArguments args, String name)
at IronRuby.Runtime.Calls.RubyCallAction.BuildMethodMissingCall(MetaObjectBuilder metaBuilder, CallArguments args, String methodName, RubyMemberInfo methodMissing, RubyMethodVisibility incompatibleVisibility, Boolean isSuperCall, Boolean defaultFallback)
at IronRuby.Runtime.Calls.RubyCallAction.BuildCall(MetaObjectBuilder metaBuilder, String methodName, CallArguments args, Boolean defaultFallback, Boolean callClrMethods)
at IronRuby.Runtime.Calls.RubyCallAction.Build(MetaObjectBuilder metaBuilder, CallArguments args, Boolean defaultFallback)
at IronRuby.Runtime.Calls.RubyMetaBinder.Bind(DynamicMetaObject scopeOrContextOrTargetOrArgArray, DynamicMetaObject[] args)
at Microsoft.Scripting.Utils.DynamicUtils.GenericInterpretedBinder1.Bind(DynamicMetaObjectBinder binder, Int32 countDown, Object[] args)
at Microsoft.Scripting.Utils.DynamicUtils.LightBind[T](DynamicMetaObjectBinder binder, Object[] args, Int32 compilationThreshold)
at IronRuby.Runtime.Calls.RubyMetaBinder.BindDelegate[T](CallSite1 site, Object[] args)
at System.Runtime.CompilerServices.CallSiteBinder.BindCore[T](CallSite1 site, Object[] args)
at System.Dynamic.UpdateDelegates.UpdateAndExecute2[T0,T1,TRet](CallSite site, T0 arg0, T1 arg1)
at Microsoft.Scripting.Interpreter.DynamicInstruction3.Run(InterpretedFrame frame)
at Microsoft.Scripting.Interpreter.Interpreter.Run(InterpretedFrame frame)
at Microsoft.Scripting.Interpreter.LightLambda.Run3[T0,T1,T2,TRet](T0 arg0, T1 arg1, T2 arg2)
at System.Dynamic.UpdateDelegates.UpdateAndExecute2[T0,T1,TRet](CallSite site, T0 arg0, T1 arg1)
at lambda_method(Closure , DynamicOperations , CallSiteBinder , Object , Object[] )
at Microsoft.Scripting.Runtime.DynamicOperations.Invoke(Object obj, Object[] parameters)
at Microsoft.Scripting.Hosting.ObjectOperations.Invoke(Object obj, Object[] parameters)
at System.Dynamic.UpdateDelegates.UpdateAndExecute3[T0,T1,T2,TRet](CallSite site, T0 arg0, T1 arg1, T2 arg2)
at IronRubyBug.Program.<>c__DisplayClass8.<PrepareScopeAndMethod>b__7(Object[] args) in D:\Projects\Oz\IronRubyBug\Program.cs:line 75
at IronRubyBug.Program.<>c__DisplayClass3.<TryReproduceBug>b__1(Int32 i) in D:\Projects\Oz\IronRubyBug\Program.cs:line 56
at System.Threading.Tasks.Parallel.<>c__DisplayClassf`1.<ForWorker>b__c()
at System.Threading.Tasks.Task.InnerInvoke()
at System.Threading.Tasks.Task.InnerInvokeWithArg(Task childTask)
at System.Threading.Tasks.Task.<>c__DisplayClass7.<ExecuteSelfReplicating>b__6(Object )