[HELP] What are the guidelines for multi-threading in C# (server side plugins)?

On playerConnect I’d like to do certain stuff which involves some complicated multi-threading, and I’m wondering if it would be wise to use locks in certain scenarios.

To give you an idea please see the following code below.

private async void OnPlayerCConnecting(params object[] args)
{
	// mandatory wait
	await Delay(0);

	// start new threads which update one variable with lock(..) {} statement in them
	Task.Run(..);
	Task.Run(..)

	// wait for that variable which is being updated in other threads to get a certain value
	while (true)
	{
		await Delay(1);

		// i guess here we are on the main thread, so is it wise to do this here?
		lock (mylock)
		{
			if (myvariable == 2)
			{
				break;
			}
		}
	}
	
	...
}

So what do I have to watch out for when doing things like this in server side plugins? What are the limitations if there is any?

Thanks in advance for any advice.

2 Likes

I’m actually interested in this too, there’s no real guide or doc on how to use the threading part of C# modding.

Here’s some of the questions I have in my head :

  • Is there another way of executing a function on every tick other than doing Tick += YourOnTickFunction(); or is that the right way of doing that?

  • When to use the await keyword, how to correctly use the BaseScript.Delay() function? Is it any different than the C# native await Task.Delay() ?

  • What is/when to use await Task.FromResult(0);

  • There’s so much more C# native functions like Task.Run() Task.Start() or Task.Factory.StartNew(). Are there of any use in the context of FiveM modding?

  • In general, what is the correct way/workflow of using these sync and async capabilites.

I would be really grateful if there was a good example project using these threaded features. :anguished:

There’s [Tick] on a public member function of a BaseScript.

On the client, you should not ever use platform task APIs. On the server, you’ll use BaseScript.Delay when you want to ensure you’ll marshal back into the game loop.

Never, this looks extremely redundant.

Not if you’re intending to call game functionality in the started task.

  • Don’t use framework task factories unless you actually have to in order to integrate with third-party libraries.

  • Marshal back to the main thread if you’ve left the main thread context by running an external task. The BCL sync context stuff doesn’t seem to work here.

  • If you want to run an ad-hoc script task, you could make a custom scheduler such as the following (taken from a random project I’ve had on my disk, I don’t necessarily approve of the code conventions or workflow used here, it seems based on some 2009 project of someone’s):

    using CitizenFX.Core;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace GTAD
    {
        class DeferredScript : BaseScript
        {
            Func<Task> _action;
            public string Source { get; set; }
    
            public DeferredScript()
            {
                Tick += DeferredScript_OnTick;
            }
    
            public void Assign(Func<Task> action)
            {
                _action = action;
            }
    
            async Task DeferredScript_OnTick()
            {
                if (_action == null)
                {
                    return;
                }
    
                try
                {
                    await _action();
                }
                catch (Exception ex)
                {
                    CitizenFX.Core.UI.Screen.ShowNotification("A deferred script caused an exception.");
    
                    Debug.WriteLine("Exception caused in deferred script: " + ex.ToString());
                }
    
                _action = null;
            }
    
            public bool Busy
            {
                get
                {
                    return (_action == null) ? false : true;
                }
            }
        }
    
        public class Defer : BaseScript
        {
            static List<DeferredScript> workers = new List<DeferredScript>();
    
            public Defer()
            {
                Tick += Defer_OnTick;
    
                for (int i = 0; i < 5; i++)
                {
                    NewWorker();
                }
            }
    
            private static DeferredScript NewWorker()
            {
                var ds = new DeferredScript();
                workers.Add(ds);
    
                BaseScript.RegisterScript(ds);
    
                return ds;
            }
    
            public static void DeferScript(Func<Task> action)
            {
                bool assigned = false;
    
                foreach (var worker in workers)
                {
                    if (worker.Busy)
                    {
                        continue;
                    }
    
                    var trace = new System.Diagnostics.StackTrace();
                    foreach (var frame in trace.GetFrames())
                    {
                        var method = frame.GetMethod();
                        if (method.Name.Equals("DeferScript")) continue;
                        worker.Source = string.Format("{0}::{1}",
                            method.ReflectedType != null ? method.ReflectedType.Name : string.Empty,
                            method.Name);
    
                        break;
                    }
                    worker.Assign(action);
    
                    assigned = true;
                    break;
                }
    
                if (!assigned)
                {
                    var ds = NewWorker();
                    ds.Assign(action);
                }
            }
    
            public static List<string> WorkerData = new List<string>();
    
            Task Defer_OnTick()
            {
                WorkerData.Clear();
    
                int i = 0;
    
                foreach (var worker in workers)
                {
                    string d = "Worker " + i.ToString();
    
                    if (worker.Busy)
                    {
                        d += " - assigned: " + worker.Source;
                    }
                    else
                    {
                        d += " - free";
                    }
    
                    i++;
    
                    WorkerData.Add(d);
                }
    
                return Task.FromResult(0);
            }
        }
    }
    

    Usage would be as such:

    Defer.DeferScript(async () =>
    {
        await World.CreateVehicle(...);
    });
    
3 Likes

Thanks for that, @deterministic_bubble!
I assume some of the variables/values coming back from the Cfx API is somehow tied to the main thread, or is there some sort of copying from the managed memory to the game loop’s memory?
E.g. does the CitizenFX.Core.UI.Screen.ShowNotification function call marshal the .net managed string to C string? And does the same happen in reverse for functions which return a string for example?

If there’s a copy I guess one just have to take care to marshal the thread back to the main loop, and don’t have to worry about values bound to threads.

Worth noting here that there are sufficient examples in the docs for marshaling the thread back to the main thread. (just for future readers)

Thanks for the very informative answer, and props for showing me the existence of the [Tick] attribute and the CitizenFX.Core.UI.Screen.ShowNotification(). (I’m still very new to FiveM modding :grin: )

I still don’t know what “marshalling back to the main thread” or BCL means but then again I don’t have much experience with threads.

Ok so no Task.Delay() but BaseScript.Delay() instead, right?
If I want to slow down a script’s execution (or a particular func in a script) for exemple.

But then it’s used in the DefferedScript exemple you gave us, only difference I see is that it returns it instead of awaiting :

I guess this is a little too advanced for me for now, I’ll stick to using [Tick] or Tick += myFunc(); and only use await BaseScript.Delay() if I’m running a non critical script