Changing document properties in async thread crashes Mac, but not Windows

I’ve created a context manager that I use in the plugin I work on, that looks like this:

class pause_redrawing:
  def _on_enter(self):
    self.previous_redraw = rs.EnableRedraw(False)
  def _on_exit(self):
    rs.EnableRedraw(self.previous_redraw)

  def __enter__(self):
    Rhino.RhinoApp.InvokeOnUiThread(System.Action(self._on_enter))
  def __exit__(self, exc_type, exc_inst, exc_tb):
    Rhino.RhinoApp.InvokeOnUiThread(System.Action(self._on_exit))

This can then be used like:

# async: true
with pause_redrawing():
  # delete some stuff
  # add some stuff
  # etc.
# do more stuff

The InvokeOnUiThread makes this work both in synchronous and asynchronous commands. If I remove this and call either _on_enter or _on_exit directly, this crashes Rhino on Macs when run through an asynchronous command. This does not crash Rhino on Windows machines. The advantage of being able to do this without the invoke is that I can guarantee the background thread won’t add/remove geometry before redrawing is paused or after it’s unpaused. To get this behavior without crashing Macs, I could use Rhino.RhinoApp.InvokeAndWait, but this adds an artificial delay which slows things down a lot. My understanding is that the delay is necessary because there’s a flag that both the UI thread needs to set to indicate that it’s done, and the background thread wants to check the value of this flag, but because of resource sharing between threads and whatnot, checking constantly would slow both threads down significantly, so it’s better to just add a delay in the background thread’s while (!done) loop because it’s going to be waiting anyways. I’m not too familiar with multithreading and shared resources in C#, and I haven’t tried recreating that while (!done) loop on the Python side using something like threading.Event.

Some other things that I’ve also seen crash Rhino only on Macs:

  • Adding/removing layers from an async thread (guaranteed in at least SR 8.9.24194.18122, but might still be a problem in SR 8.11 because changing the document’s redraw parameter still breaks in the latest SR)

    • Regardless of whether or not redrawing is on
  • Changing the active layer from an async thread (guaranteed in at least SR 8.9.24194.18122, same as above)

At this time, I only have a few stacktraces, all from SR 8.9.24194, but I know the set_RedrawEnabled stacktrace (or one very similar to it) is still being seen in the latest SR. I only have Windows machines handy to test on, so these reports have to be sent to me from users with a Mac.

set_RedrawEnabled crash from async thread
[ERROR] FATAL UNHANDLED EXCEPTION: ObjectiveCException.NSInternalInconsistencyException: Modifications to the layout engine must not be performed from a background thread after it has been accessed from the main thread.
   at ObjCRuntime.Messaging.void_objc_msgSendSuper(IntPtr receiver, IntPtr selector)
   at ObjCRuntime.Messaging.void_objc_msgSendSuper(IntPtr receiver, IntPtr selector)
   at AppKit.NSTableView.BeginUpdates()
   at Eto.Mac.Forms.Controls.TreeGridViewHandler.set_DataStore(ITreeGridStore`1 value)
   at Eto.Forms.TreeGridView.set_DataStore(ITreeGridStore`1 value)
   at InvokeStub_TreeGridView.set_DataStore(Object, Object, IntPtr*)
   at System.Reflection.MethodInvoker.Invoke(Object obj, IntPtr* args, BindingFlags invokeAttr)
   at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
   at System.ComponentModel.ReflectPropertyDescriptor.SetValue(Object component, Object value)
   at Eto.Forms.PropertyBinding`1.InternalSetValue(Object dataItem, T value)
   at Eto.Forms.IndirectBinding`1.SetValue(Object dataItem, T value)
   at Eto.Forms.ObjectBinding`2.set_DataValue(TValue value)
   at Eto.Forms.DualBinding`1.SetDestination()
   at Eto.Forms.DualBinding`1.HandleSourceChanged(Object sender, EventArgs e)
   at Eto.Forms.DirectBinding`1.OnDataValueChanged(EventArgs e)
   at Eto.Forms.ObjectBinding`2.HandleChangedEvent(Object sender, EventArgs e)
   at Rhino.UI.ViewModel.RaisePropertyChanged(String propertyName)
   at Rhino.UI.DialogPanels.LayersPanelViewModel.FillCollection()
   at Rhino.UI.DialogPanels.LayersPanelViewModel.OnLayerTableEvent(Object sender, LayerTableEventArgs args)
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
   at System.Reflection.MethodInvoker.Invoke(Object obj, IntPtr* args, BindingFlags invokeAttr)
   at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
   at System.Delegate.DynamicInvokeImpl(Object[] args)
   at Rhino.Runtime.HostUtils.SafeInvoke[TEventArgs](EventHandler`1 handler, Object sender, TEventArgs args)
   at Rhino.RhinoDoc.OnLayerTableEvent(UInt32 docSerialNumber, Int32 eventType, Int32 index, IntPtr pConstOldSettings)
   at UnsafeNativeMethods.CRhinoDoc_GetSetBool(UInt32 docSerialNumber, DocumentStatusBool which, Boolean set, Boolean setValue)
   at UnsafeNativeMethods.CRhinoDoc_GetSetBool(UInt32 docSerialNumber, DocumentStatusBool which, Boolean set, Boolean setValue)
   at Rhino.DocObjects.Tables.ViewTable.set_RedrawEnabled(Boolean value)
   at InvokeStub_ViewTable.set_RedrawEnabled(Object, Object, IntPtr*)
   at System.Reflection.MethodInvoker.Invoke(Object obj, IntPtr* args, BindingFlags invokeAttr)
   at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
   at Python.Runtime.PropertyObject.tp_descr_set(BorrowedReference ds, BorrowedReference ob, BorrowedReference val)
   at Python.Runtime.Runtime.PyEval_EvalCode(BorrowedReference co, BorrowedReference globals, BorrowedReference locals)
   at Python.Runtime.PyModule.Execute(PyObject script, PyDict locals)
 at Python.Runtime.RhinoCodePythonEngine.RunScope(Object scope, Object code, String pythonFile, String beforeScript, String afterScript)
   at Rhino.Runtime.Code.Languages.PythonNet.CPythonCode.Execute(RunContext context)
   at Rhino.Runtime.Code.Code.ExecTry(RunContext context, IPlatformDocument& doc, Object& docState)
   at Rhino.Runtime.Code.Code.Run(RunContext context)
   at Rhino.Runtime.Code.Code.Run(String runId)
   at __RhinoCodeScript__.<>c__DisplayClass0_0.<__RunScript__>b__0()
   at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(Thread threadPoolThread, ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task& currentTaskSlot, Thread threadPoolThread)
   at System.Threading.ThreadPoolWorkQueue.Dispatch()
   at System.Threading.PortableThreadPool.WorkerThread.WorkerThreadStart()
   at System.Threading.Thread.StartCallback()
[END ERROR]
SetCurrentLayerIndex crash from async thread
[ERROR] FATAL UNHANDLED EXCEPTION: ObjectiveCException.NSInternalInconsistencyException: Modifications to the layout engine must not be performed from a background thread after it has been accessed from the main thread.
   at ObjCRuntime.Messaging.void_objc_msgSendSuper(IntPtr receiver, IntPtr selector)
   at ObjCRuntime.Messaging.void_objc_msgSendSuper(IntPtr receiver, IntPtr selector)
   at AppKit.NSTableView.BeginUpdates()
   at Eto.Mac.Forms.Controls.TreeGridViewHandler.ReloadData()
   at Eto.Forms.TreeGridView.ReloadData()
   at Rhino.UI.DialogPanels.LayerTreeGridView.OnReloadDataRequired(ReloadDataEventArgs args)
   at Rhino.UI.DialogPanels.LayersPanelViewModel.ReloadLayerData()
   at Rhino.UI.DialogPanels.LayersPanelViewModel.OnLayerTableEvent(Object sender, LayerTableEventArgs args)
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
   at System.Reflection.MethodInvoker.Invoke(Object obj, IntPtr* args, BindingFlags invokeAttr)
   at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
   at System.Delegate.DynamicInvokeImpl(Object[] args)
   at Rhino.Runtime.HostUtils.SafeInvoke[TEventArgs](EventHandler`1 handler, Object sender, TEventArgs args)
   at Rhino.RhinoDoc.OnLayerTableEvent(UInt32 docSerialNumber, Int32 eventType, Int32 index, IntPtr pConstOldSettings)
   at UnsafeNativeMethods.CRhinoLayerTable_SetCurrentLayerIndex(UInt32 docSerialNumber, Int32 layerIndex, Boolean quiet)
   at UnsafeNativeMethods.CRhinoLayerTable_SetCurrentLayerIndex(UInt32 docSerialNumber, Int32 layerIndex, Boolean quiet)
   at Rhino.DocObjects.Tables.LayerTable.SetCurrentLayerIndex(Int32 layerIndex, Boolean quiet)
   at InvokeStub_LayerTable.SetCurrentLayerIndex(Object, Object, IntPtr*)
   at System.Reflection.MethodInvoker.Invoke(Object obj, IntPtr* args, BindingFlags invokeAttr)
   at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
   at Python.Runtime.MethodBinder.Invoke(BorrowedReference inst, BorrowedReference args, BorrowedReference kwargs, MethodBase[] methods)
   at Python.Runtime.MethodObject.InvokeMethod(BorrowedReference target, BorrowedReference args, BorrowedReference kw, MethodBase info)
   at Python.Runtime.MethodBinding.tp_call(BorrowedReference ob, BorrowedReference args, BorrowedReference kw)
   at Python.Runtime.Runtime.PyEval_EvalCode(BorrowedReference co, BorrowedReference globals, BorrowedReference locals)
   at Python.Runtime.PyModule.Execute(PyObject script, PyDict locals)
   at Python.Runtime.RhinoCodePythonEngine.RunScope(Object scope, Object code, String pythonFile, String beforeScript, String afterScript)
   at Rhino.Runtime.Code.Languages.PythonNet.CPythonCode.Execute(RunContext context)
   at Rhino.Runtime.Code.Code.ExecTry(RunContext context, IPlatformDocument& doc, Object& docState)
   at Rhino.Runtime.Code.Code.Run(RunContext context)
   at Rhino.Runtime.Code.Code.Run(String runId)
   at __RhinoCodeScript__.<>c__DisplayClass0_0.<__RunScript__>b__0()
   at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(Thread threadPoolThread, ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task& currentTaskSlot, Thread threadPoolThread)
   at System.Threading.ThreadPoolWorkQueue.Dispatch()
   at System.Threading.PortableThreadPool.WorkerThread.WorkerThreadStart()
   at System.Threading.Thread.StartCallback()
[END ERROR]

I’m not surprised that Rhino crashes here (after all, the stacktraces make it pretty clear that modifications to the layout engine from a background thread are a no-no), but what’s weird is that these specific crashes only happen on Mac, not on Windows.

Whatever you do:

Do not modify the Rhino document in any way from a thread other than the main thread.

Access to the document is not thread-safe.