For Devs - cseelhoff/RimThreaded GitHub Wiki



Multi Threading

Other Sources

RimThreaded Compatibility

Info

  • XML only mods don't need to be changed in any way.

Common Issues

Calling From The Main Thread

Example Error

Function __cdecl SoundHandle::~SoundHandle(void) may only be called from main thread!
(Filename: C:\buildslave\unity\build\Modules/Audio/Public/sound/SoundManager.cpp Line: 35)
  • Sound can only be called from main thread.
    • Restrict sound to the main thread.
  • Some unity functions can only be called from the main thread.
    • Restrict those functions to the main thread.

Thread-Safe Collections

Example Error

System.InvalidOperationException: Collection was modified; enumeration operation may not execute.
  at System.ThrowHelper.ThrowInvalidOperationException (System.ExceptionResource resource) [0x0000b] in <eae584ce26bc40229c1b1aa476bfa589>:0 
  at System.Collections.Generic.List1+Enumerator[T].MoveNextRare () [0x00013] in <eae584ce26bc40229c1b1aa476bfa589>:0 
  at System.Collections.Generic.List1+Enumerator[T].MoveNext () [0x0004a] in <eae584ce26bc40229c1b1aa476bfa589>:0
  • Some collections aren't thread-safe.
    • Use a thread-safe collection instead.
      • You can also use other tools to solve concurrency instead, Like locks.
    • Don't use ConcurrentDictionary.
      • It has poor performance.

Code Without Locks

        public static Dictionary<T, T> A= new Dictionary<T, T>();
        public static void Add(T t)
        {
            A.Add(t); //Without a lock, Multiple threads could be trying to edit "A" at the same time.
        }
        public static void Clear()
        {
            A.Clear();
        }

Code With Locks

        public static Dictionary<T, T> A= new Dictionary<T, T>();
        public static void Add(T t)
        {
            lock (A) //"A" is locked before it's added to, So only one thread is editing it at a time.
            {
                A.Add(t);
            }
        }
        public static void Clear()
        {
            lock (A) //"A" is locked before it's cleared, So only one thread is editing it at a time.
            {
                A.Clear();
            }
        }

Null Handling

Example Error

Exception while generating thing set: System.NullReferenceException: Object reference not set to an instance of an object
  at RimWorld.StockGenerator_Animals.PawnKindAllowed (Verse.PawnKindDef kind, System.Int32 forTile) [0x0000d] in <d243303f714d4dc48680ccaecd2f594e>:0 
  at RimWorld.StockGenerator_Animals+<>c__DisplayClass9_0.<GenerateThings>b__3 (Verse.PawnKindDef k) [0x0000e] in <d243303f714d4dc48680ccaecd2f594e>:0 
  at System.Linq.Enumerable+WhereListIterator`1[TSource].MoveNext () [0x00037] in <351e49e2a5bf4fd6beabb458ce2255f3>:0 
  at Verse.GenCollection.TryRandomElementByWeight[T] (System.Collections.Generic.IEnumerable`1[T] source, System.Func`2[T,TResult] weightSelector, T& result) [0x0021b] in <d243303f714d4dc48680ccaecd2f594e>:0 
  at RimWorld.StockGenerator_Animals+<GenerateThings>d__9.MoveNext () [0x002c4] in <d243303f714d4dc48680ccaecd2f594e>:0 
  at RimWorld.ThingSetMaker_TraderStock.Generate (RimWorld.ThingSetMakerParams parms, System.Collections.Generic.List`1[T] outThings) [0x000e4] in <d243303f714d4dc48680ccaecd2f594e>:0 
  at (wrapper dynamic-method) RimWorld.ThingSetMaker.RimWorld.ThingSetMaker.Generate_Patch0(RimWorld.ThingSetMaker,RimWorld.ThingSetMakerParams)
  • "NullReferenceException" meaning some element that was there before no longer exists.
  • Null handling is particularly important in multi threading.
    • Not having proper null handling will cause errors with multi threading.
      • Add null handling to your code.

Code Without Null Handling

         public override string LabelExtraPart(RitualObligation obligation)
        {
            return ((Corpse)obligation.targetA.Thing).InnerPawn.LabelShort;
        }

Code With Null Handling

         public static bool LabelExtraPart(RitualObligationTargetWorker_GraveWithTarget __instance, ref string __result, RitualObligation obligation)
        {
            __result = string.Empty; // It's important to null check in this order to prevent issues. ("obligation" before "obligation.targetA" etc.)
            if (obligation == null || obligation.targetA == null || ((Corpse)obligation.targetA.Thing) == null || ((Corpse)obligation.targetA.Thing).InnerPawn == null)
            {
                return false;
            }
            __result = ((Corpse)obligation.targetA.Thing).InnerPawn.LabelShort;
            return false;
        }

Thread Safe Temp Variables

  • There is only one instance of a static in memory.
    • Making them a ThreadStatic means that there is one instance per thread.
      • This prevents race conditions.

Non Thread Safe Code

        private static List<string> tmpNames = new List<string>();

        private static HashSet<string> usedNamesTmp = new HashSet<string>();

Thread Safe Code

       [ThreadStatic] private static List<string> tmpNames = new List<string>();

       [ThreadStatic] private static HashSet<string> usedNamesTmp = new HashSet<string>(); 

Hoff Quotes

"I will try to explain some and then summarize an answer for you. So far mod incompatible has been because only one of the following issues:

  1. I was lazy and did a destructive prefix on a method that another mod uses. The fix for this is 100% on me transpiling the conflicting method. A list of currently conflicting methods can be found in RimThreaded's mod settings

  2. The incompatible mod is making a direct call to a unity function that specifically requires the function to come from the main thread only. Since my mod essentially makes every mod multithreaded, this issue occurs. The fixes are normally for me to rewrite these methods to be redirected through the main thread only. A good example of this is the way that GiddyUp calls some graphics functions inside of unity. I have fixed about 4/8 of them so far.

  3. The 3rd kind of incompatibility is a very easy one to fix, but more than likely can only really be done by the author. Its when the mod is using a list or similar data structure that is written to (.Add or .Remove for example). For the lists, just adding a 1 line lock command around the write-access commands and a try-catch on the read accesses commands does the trick and is quite simple. For the most part, these changes are quite trivial, but I don't know of a good way to automatically fix this for the mod authors really. Maybe I will come up with a creative solution some day.

  4. The 4th kind Its when the mod is making calls to either a static variable that is used as a common/shared temporary variable. The static variables used as temp variables just need to be moved to be local variables or sometimes converted to dictionaries. These are still fairly easy to fix and most likely need to be done by the mod author as well.

I think that covers the only issues that I have run into... So let me try to summarize the answer now... When RT is fully working (no more bugs with vanilla and no more methods that need transpiling, and all of the common unity functions have been redirected) then there will still be some mods that will need minor work Normally that will amount to adding less than a dozen lines of code, and the format of the code always looks the same Of course, alternatively to the authors doing it, other contributors (including myself) can always write a "compatibility-patch" that does this for the mod-authors as well. I may end up doing some of these, but probably only for a few really popular mods"

-Major Hoff — 06/11/2020


  1. If you are a using a static variable in a class as a temp variable, just add [ThreadStatic] at the beginning of it

  2. If you are using a collection, use a thread-safe collection instead (i.e. conncurrentStack instead of Stack)

  3. If you are making a call to a unity function that requires it to be called from the main thread only, then call the "threadsafe" equivilent. This one requires adding 5 lines of code. But <1% of mods acutally do this

-Major Hoff — 16/08/2021


⚠️ **GitHub.com Fallback** ⚠️