AccessViolationException in Brep.Split inside MT GH component in Release

I have a GH component that, if I run it on multiple breps with parallel processing enabled in Release mode - but not in Debug -, raises an AccessViolationException after Brep.Split.

Is it possible that this has something to do with the GC.KeepAlive(cutter); call inside Brep.Split?

Is there any obvious workaround for this behavior? At the moment, this exception basically prevents us from being able to produce Release builds. Unfortunately, disabling multithreading for the component is also not an option because the performance impact is too severe.

I can provide a sample file and the memory dump, if necessary.

The component code is the following (minimal example):

protected override void SolveInstance(IGH_DataAccess da)
{
    Brep brep = null;
    if (InPreSolve)
    {
        da.GetData(0, ref brep);
        var task = Task.Run(() => ComputeResult(brep), CancelToken);
        TaskList.Add(task);
        return;
    }

    if (!GetSolveResults(da, out var result))
    {
        da.GetData(0, ref brep);
        result = ComputeResult(brep);
    }

    da.SetData(0, result);
}

private bool ComputeResult(Brep brep)
{
    var box = brep.GetBoundingBox(true);
    var planes = new[]
    {
        new Plane(box.Center, Vector3d.XAxis),
        new Plane(box.Center, Vector3d.YAxis),
        new Plane(box.Center, Vector3d.ZAxis)
    };
    var planeSurfaces = planes.Select(p => PlaneSurface.CreateThroughBox(p, box)).ToList();

    foreach (var plane in planeSurfaces)
    {
        var pieces = brep.Split(plane.ToBrep(), 0.001);
        if (pieces.Length != 2)
        {
            return false;
        };
        // Any call accessing one of the two split Breps causes the exception
        var isValid = pieces[0].IsValid;
    }


    return false;
}

An extract of the memory dump shows the following:

*******************************************************************************
*                                                                             *
*                        Exception Analysis                                   *
*                                                                             *
*******************************************************************************

DEBUG_FLR_EXCEPTION_CODE(80004003) and the ".exr -1" ExceptionCode(e0434f4d) don't match
MethodDesc:   00007fff516c7620
Method Name:  HelloGrasshopper.TestComponent.ComputeResult(Rhino.Geometry.Brep)
Class:        00007fff516b9ab8
MethodTable:  00007fff516c7698
mdToken:      0000000006000012
Module:       00007fff516c62e0
IsJitted:     yes
CodeAddr:     00007fff51c2f040
Transparency: Critical
Source file:  C:\Users\Thomas\source\repos\HelloGrasshopper\TestComponent.cs @ 80

KEY_VALUES_STRING: 1

    Key  : AV.Fault
    Value: Read

    Key  : Analysis.CPU.mSec
    Value: 6405

    Key  : Analysis.DebugAnalysisManager
    Value: Create

    Key  : Analysis.Elapsed.mSec
    Value: 12394

    Key  : Analysis.IO.Other.Mb
    Value: 0

    Key  : Analysis.IO.Read.Mb
    Value: 1

    Key  : Analysis.IO.Write.Mb
    Value: 0

    Key  : Analysis.Init.CPU.mSec
    Value: 343

    Key  : Analysis.Init.Elapsed.mSec
    Value: 7336

    Key  : Analysis.Memory.CommitPeak.Mb
    Value: 253

    Key  : CLR.BuiltBy
    Value: NET481REL1LAST_C

    Key  : CLR.Engine
    Value: CLR

    Key  : CLR.Exception.System.AccessViolationException._message
    Value: Attempted to read or write protected memory. This is often an indication that other memory is corrupt.

    Key  : CLR.Exception.Type
    Value: System.AccessViolationException

    Key  : CLR.Version
    Value: 4.8.9075.0

    Key  : Timeline.OS.Boot.DeltaSec
    Value: 63210

    Key  : Timeline.Process.Start.DeltaSec
    Value: 49

    Key  : WER.OS.Branch
    Value: ni_release

    Key  : WER.OS.Timestamp
    Value: 2022-05-06T12:50:00Z

    Key  : WER.OS.Version
    Value: 10.0.22621.1

    Key  : WER.Process.Version
    Value: 7.23.22282.13001


FILE_IN_CAB:  Rhino.dmp

NTGLOBALFLAG:  0

PROCESS_BAM_CURRENT_THROTTLED: 0

PROCESS_BAM_PREVIOUS_THROTTLED: 0

APPLICATION_VERIFIER_FLAGS:  0

CONTEXT:  000000e0c15fda80 -- (.cxr 0xe0c15fda80)
rax=0000000000000000 rbx=00000278813eb3f0 rcx=0000000000007700
rdx=00000278d1ff9590 rsi=00000278d18719f0 rdi=00000278d140f1a0
rip=00007ffea66c133e rsp=000000e0c15fe230 rbp=000000e0c15fe330
 r8=00000278cbcfa140  r9=0000000000000088 r10=00007fff7e6f0000
r11=000000e0c15fe220 r12=0000000000000229 r13=0000000000000120
r14=0000000000000229 r15=0000000000020670
iopl=0         nv up ei pl nz na po nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010206
opennurbs!ON_Brep::IsValid+0xd0e:
00007ffe`a66c133e 4c63440150      movsxd  r8,dword ptr [rcx+rax+50h] ds:00000000`00007750=????????
Resetting default scope

EXCEPTION_RECORD:  000000e0c15fdf70 -- (.exr 0xe0c15fdf70)
ExceptionAddress: 00007ffea66c133e (opennurbs!ON_Brep::IsValid+0x0000000000000d0e)
   ExceptionCode: c0000005 (Access violation)
  ExceptionFlags: 00000000
NumberParameters: 2
   Parameter[0]: 0000000000000000
   Parameter[1]: 0000000000007750
Attempt to read from address 0000000000007750

PROCESS_NAME:  Rhino.exe

ERROR_CODE: (NTSTATUS) 0xe0434f4d - <Unable to get error code text>

EXCEPTION_CODE_STR:  80004003

READ_ADDRESS:  0000000000007750 

FAULTING_THREAD:  ffffffff

STACK_TEXT:  
000000e0`c15fe630 00000000`00000001 RhinoCommon!UnsafeNativeMethods.ON_Object_IsValid+0x2
000000e0`c15fe6e0 00007fff`51c2f5c8 HelloGrasshopper!HelloGrasshopper.TestComponent.ComputeResult+0x588
000000e0`c15fe980 00007fff`aa34a6e9 mscorlib_ni!System.Threading.Tasks.Task`1[[System.Boolean, mscorlib]].InnerInvoke+0x29
000000e0`c15fe9b0 00007fff`a9a07a48 mscorlib_ni!System.Threading.Tasks.Task.Execute+0x48
000000e0`c15fe9f0 00007fff`a99de9a9 mscorlib_ni!System.Threading.ExecutionContext.RunInternal+0x109
000000e0`c15feac0 00007fff`a99de896 mscorlib_ni!System.Threading.ExecutionContext.Run+0x16
000000e0`c15feaf0 00007fff`a9a08ac2 mscorlib_ni!System.Threading.Tasks.Task.ExecuteWithThreadLocal+0x232
000000e0`c15feba0 00007fff`a9a07b92 mscorlib_ni!System.Threading.Tasks.Task.ExecuteEntry+0xa2
000000e0`c15febe0 00007fff`a9a19c17 mscorlib_ni!System.Threading.ThreadPoolWorkQueue.Dispatch+0x157


STACK_COMMAND:  !C:\ProgramData\Dbg\sym\SOS_AMD64_AMD64_4.8.9075.00.dll\62CCE36B9a4000\SOS_AMD64_AMD64_4.8.9075.00.dll.pe 0x278e90ddc28 ; ** Pseudo Context ** ManagedPseudo ** Value: ffffffff ** ; kb

SYMBOL_NAME:  RhinoCommon!UnsafeNativeMethods.ON_Object_IsValid+2

MODULE_NAME: RhinoCommon

IMAGE_NAME:  RhinoCommon.dll

FAILURE_BUCKET_ID:  CLR_EXCEPTION_System.AccessViolationException_80004003_RhinoCommon.dll!UnsafeNativeMethods.ON_Object_IsValid

OS_VERSION:  10.0.22621.1

BUILDLAB_STR:  ni_release

OSPLATFORM_TYPE:  x64

OSNAME:  Windows 10

IMAGE_VERSION:  7.23.22282.13001

FAILURE_ID_HASH:  {916043c9-e986-3c9b-fa86-4b33e114c865}

Followup:     MachineOwner
---------

Duplicate breps first. Ensure each thread has its own brep copy. Brep.Split may not be thread-safe.

I tried that the following way. Didn’t work either…

if (InPreSolve)
{
    da.GetData(0, ref brep);
    var task = Task.Run(() => this.ComputeResult(brep.DuplicateBrep()));
    TaskList.Add(task);
    return;
}

After some more digging, it looks like there are more places where other Brep methods can fail in a multithreaded setup like the above.
What might work (pending some more tests), is passing the id of the brep to the task and retrieve the brep from there:

if (InPreSolve)
{
    GH_Brep brep;
    da.GetData(0, ref brep);
    var task = Task.Run(() => this.ComputeResult(brep.ReferenceID));
    TaskList.Add(task);
    return;
}

And then:

private bool ComputeResult(Guid brepId)
{
    var brep = RhinoDoc.ActiveDoc.Objects.Find(guid).Geometry as Brep;
    ...
}

btw: is there any way to get the current doc from Grasshopper directly (and not using the active doc)?

Hello,

what I find strange about your code is that there is no point where you await your tasks to be completed.

If your BReps are living on the UI Thread, and they run out of scope, then as a consequence they get garbage collected. Depending on your performance, this may happen before all task have completed. Why not just block the UI thread (like any other GH component) as long as the computation is running. This would also simplify your code drastically. All you need to do is to call Task.WaitAll(tasks)… For any none thread-safe call, you could invoke that part on the UI thread using the Dispatcher. Also note: many collections are not thread-safe, such as a ‘List’. Use an array, and never read & write the same item from multiple threads.

Maybe I miss something of what you are saying, but I assumed that the GH_TaskCapableComponent takes care of handling the tasks. From what I could see in the decompiled source, it retrieves the result of all collected tasks, thereby blocking already.

I also basically followed the implementation in the examples given by McNeel, e.g. here: rhino-developer-samples/SampleGhTaskVolumeComponent.cs at 7 · mcneel/rhino-developer-samples · GitHub