[C#] Threads "Classiques" en usages avancés sous Unity. ;)

Cette section est destinée aux scripts partagés par la communauté. Chaque post est destiné à un script. Suivez bien les recommandations.
Avatar de l’utilisateur
ZJP
Messages : 5611
Inscription : 15 Déc 2009 06:00

[C#] Threads "Classiques" en usages avancés sous Unity. ;)

Message par ZJP » 27 Mars 2017 17:23

Salut,

Une des choses les plus frustrantes quand on utilise des Processeurs modernes est l’inexploitation des tous les Cores.
J’ai (comme tout le monde) cherché a implanter diverses solutions de Threading/Parallélisme sous Unity avec plus ou moins de bonheur, sans toutefois trouver une réponse adaptée à mes pré requis (Solution usant de l’API de BASE de Csharp, donc pas de Thread Pool ou librairie plus ou moins évoluée, pas d’usage par exemple des Lambdas – car vieux dev des années 80-90 maîtrisant que 10% du langage-, Solution optimisée pour le Desktop etc etc…), tout en évitant les lacunes et difficultés du multi-thread.

Rappel : L'API d'Unity n'est pas "Thread Safe". Voir ici un "résumé" de ce qui est possible ou pas.


Postulats :

- Lancer des Threads ça craint !!!. Parfois, les démarrer prend plus de ressources que le travail demandé. Donc, les ré-démarrer après les taches accomplies est TOTALEMENT a proscrire.

- Synchroniser des Threads ça craint !!!. Cela y compris avec les API avancées (Mutex, Thread.Join() et Cie).
Certains (comme ici ) ont trouvés une solution (que je préfère) à base de "Variable-Cumulatrice-Des-Threads-Terminés" mais le concept exploité dans l’exemple dysfonctionne avec des CPUs performants exécutants des taches courtes : la fameuse variable étant "partagée", son intégrité n’est plus garantie, plusieurs Threads cherchant simultanément a l’incrémenter. La "relance" des taches s’interrompt car le test de "Fin-Des-Travaux" n’est plus vérifié. Ça.. ::d

- Stopper une application avec des Threads en cours (surtout avec des taches lourdes) cela peut…craindre. Essayez par exemple d’interrompre un "Play" puis de le relancer avec des Threads occupés à des taches qui durent plusieurs secondes/minutes.

- La répartition automatique des Threads vers les CPUs n’est pas si efficace que cela. Lancer par exemple 512 (ou même que 8) Threads "lourds" sous Unity avec mon FX8350 en exploitant les 8 Cores peut aussi être problématique. Ce postulat est le plus important de mon point de vu car :

a) C’est de là que proviennent les freezes/blocages ( en plus du lancement/re-lancement des Threads)
b) Car des solutions, du moins pour le Desktop sous Unity (à ma connaissance car j’ai pas trouvé) ne courent pas les rues.



La solution proposée ici pallie donc aux postulats précités :

- C’est du Code de Base (vraiment) facile a comprendre et a utiliser.

- Les Threads (peut importe leur nombre) sont lancés UNE FOIS, avec un paramétrage complet et détaillé.

- La méthode utilisée pour la synchronisation des Threads est fonctionnelle y compris avec des CPUS performants exécutants des taches MINIMALISTES. De plus, la durées des taches n’a QUASIMENT aucun impact sur la boucle principale d’Unity.

- Un "disjoncteur" permet (s’il est correctement placé) l’arrêt immédiat et SYSTEMATIQUE (je ne l’ai pas pris en défaut jusqu'à présent) des Threads quelque soit la lourdeur (ou pas) de la tache a accomplir.

- Il est possible "d’épargner" des CPUs/COREs ( Le premier et/ou le deuxième) qui ne seront JAMAIS sollicités par les Threads et cela quelque qu’en soit leur nombre : c’est la garantie que les process principaux d’Unity continueront de s’exécuter sans souffrir des taches en "background". Usage de l’API Windows 32/64 ( GetCurrentThread, SetThreadAffinityMask et GetCurrentProcessorNumber). Donc "Desktop Only" pour cette partie.


Bien que l’exemple montre ici un parallélisme de taches, il est bien sur possible d’utiliser ces "concepts" pour des Threads isolés.

Le Projet en pièce jointe est sommaire. Notez l’emploi d’un "Manager d’Update" comme évoqué ici pour l’imiter le nombre…d’Update(). Juste un test… Rien a voir (ou pas) avec le sujet qui nous préoccupe ici.

Code : Tout sélectionner

// ************************************************************************************************************
//  (c) ZJP                                                                               Parallele Thread Test
// ************************************************************************************************************

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Threading;
using System.Runtime.InteropServices; // DLL !!!

public class testTHREAD_JOB_NEW : MonoBehaviour {

	public int threadsToLaunch = 512;
	[Range(0,2)] public int CoreToPreserv = 1; // Number of not used firts Cores
	[Range(0.05f, 0.5f)] public float chekAllJobs = 0.1f; // 20fps to 0.5fps (for example ^^ )
	private bool[] abortTH; // 
	private int[] jobEndTH; // 
	private int[] cpuMask;  // Binary Mask of used Cores
	private int[] cpuID;    // Ralationnal array Thread => used Cores
	private int MaxCoreToUse = 0;     // Number of used Cores
	private int countThreadEnded = 0; // Count of terminated Threads

	// ****************************************************************************************** Windows 32/64
	[DllImport("kernel32.dll")] static extern int GetCurrentThread();
	[DllImport("kernel32.dll")] static extern int SetThreadAffinityMask(int hThread, int dwThreadAffinityMask);
	[DllImport("kernel32.dll")] static extern int GetCurrentProcessorNumber();
	// ****************************************************************************************** Windows 32/64

	private void Start() {
		Application.runInBackground = true;

		// ************************************************************************************** Windows 32/64
		// *********************************************************************** Build used Cores Binary Mask
		MaxCoreToUse = System.Environment.ProcessorCount - CoreToPreserv;
		Debug.Log("Cores available : " + MaxCoreToUse);
		cpuMask = new int[MaxCoreToUse];
		cpuID   = new int[threadsToLaunch];
		for (int i = 0; i < MaxCoreToUse; i++) cpuMask[i] =  1 << (i + CoreToPreserv);
		// ******************************************************************* Build array Thread => used Cores
		int j = 0;
		for (int i = 0; i < threadsToLaunch; i++) {
			cpuID[i] = cpuMask[j];
			j++;
			if (j > MaxCoreToUse - 1) j = 0;
		}
		// ************************************************************************************** Windows 32/64

		abortTH  = new bool[threadsToLaunch];
		jobEndTH = new int[threadsToLaunch];

		for (int i = 0; i < threadsToLaunch; i++) {           
			ParameterizedThreadStart pts = new ParameterizedThreadStart(ThreadJobFunction);
			Thread jobThread       = new System.Threading.Thread(pts);
			// According with the Official documentation : https://docs.unity3d.com/Manual/BestPracticeUnderstandingPerformanceInUnity8.html
			// ' The priority for the main thread and graphics thread are both ThreadPriority.Normal.
			//   Any threads with higher priority preempt the main/graphics threads and cause framerate hiccups, 
			//   whereas threads with lower priority do not. If threads have an equivalent priority to the main thread, the CPU attempts to give equal time to the threads, 
			//   which generally results in framerate stuttering if multiple background threads are performing heavy operations, such as AssetBundle decompression.'
			// So, better use a lowest Thread Priority
			jobThread.Priority     = System.Threading.ThreadPriority.BelowNormal; // Lowest, BelowNormal, Normal, AboveNormal, Highest
			jobThread.IsBackground = true;
			abortTH[i]  = false; // "Disjonctor" End of Thread
			jobEndTH[i] = 0; 
			jobThread.Start(i); // Thread Number
		}
		
        StartCoroutine(CheckEndOfJobs()); // (Why not !!! ^^ )

	}

	// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
	// According with :                                                                                            +
	// https://forum.unity3d.com/threads/best-method-to-check-something-often-without-dragging-performance.444079/ +
	// seem better than Update or FixedUpdate Loop                                                                 +
	// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    IEnumerator CheckEndOfJobs()
    {
        var wait = new WaitForSeconds(chekAllJobs);
        while (true)
        {
            yield return wait;
			// ALL Threads done ? **********************************************************
			countThreadEnded = 0;
			for (int i = 0; i < threadsToLaunch; i++) countThreadEnded += jobEndTH[i];
			if (countThreadEnded == threadsToLaunch) // YES !!!
			{
				Debug.Log("All thread Done...");
				// USING OF THE JOBS TASK ***********************
				//
				// USING OF THE JOBS TASK ***********************
			
				// Go Again *************************************
				for (int i = 0; i < threadsToLaunch; i++) {
					jobEndTH[i] = 0; // ReStart all Job/Thread
				}
			}
			// ALL Threads done ? **********************************************************
        }
    }
	// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
	// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++


/*
	private void FixedUpdate() {
		// ALL Threads done ? **********************************************************
		countThreadEnded = 0;
		for (int i = 0; i < threadsToLaunch; i++) countThreadEnded += jobEndTH[i];
		if (countThreadEnded == threadsToLaunch) // YES !!!
		{
			Debug.Log("All thread Done...");
			// USING OF THE JOBS TASK ***********************
			//
			// USING OF THE JOBS TASK ***********************
			
			// Go Again *************************************
			for (int i = 0; i < threadsToLaunch; i++) {
				jobEndTH[i] = 0; // ReStart all Job/Thread
			}
		}
		// ALL Threads done ? **********************************************************
	}
*/
	private void OnDisable() {
		// Stop ALL Threads
		for (int i = 0; i < threadsToLaunch; i++) {
			abortTH[i] = true;
		}
	}

	private void ThreadJobFunction(object threadParams) {
		int threadNumber = (int)threadParams; 
		Debug.Log("Enter Thread Number " + threadNumber );

		// ************************************************************* Windows 32/64
		SetThreadAffinityMask(GetCurrentThread(), cpuID[threadNumber]); // thread=>cpu
		Debug.Log("Cpu used > " + GetCurrentProcessorNumber()); // Just Checking ^^
		// ************************************************************* Windows 32/64

		// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
		// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
		again:
		// Waiting for the launch
		while (jobEndTH[threadNumber] == 1) {
			if (abortTH[threadNumber]) return; // end of Thread
			Thread.Sleep(1); // Maybe another value is better?.
		}
		//Debug.Log("Thread Number " + threadNumber + " Started/ReStarted !!!");

		// ******************************************************************* JOB !!!
		// ******************************************************************* JOB !!!
//		int a = 2000000000; // Big job ^^
		int a = 2; // Works very well even on VERY SMALL jobs...
		while (a > 1) {
			if (abortTH[threadNumber]) return; // end of Thread
			a--;
		}
		// ******************************************************************* JOB !!!
		// ******************************************************************* JOB !!!

		jobEndTH[threadNumber] = 1; // Job done
		//Debug.Log("Thread Number " + threadNumber + " Completed ");

		goto again; // ^^ yes, Goto...
		// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
		// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
	}

}
// ************************************************************************************************************
//  (c) ZJP                                                                               Parallele Thread Test
// ************************************************************************************************************

Image

Image


PS :
Pour un exemple d’usage, voir ce sujet D’Alesk.

PS2 :
Le script du post est plus à jour que celui du projet en pièce jointe.
Pièces jointes
thread test.zip
(33.36 Kio) Téléchargé 18 fois
Dernière édition par ZJP le 13 Avr 2017 18:10, édité 10 fois.
Pour triompher, le mal n’a besoin que de l’inaction des gens de bien.Edmund Burke (1729-1797)

zugsoft
Messages : 355
Inscription : 26 Juin 2014 23:43
Localisation : Swiss
Contact :

Re: [C#] Threads "Classiques" en usages avancés sous Unity. ;)

Message par zugsoft » 27 Mars 2017 20:33

C'est bien et simple a comprendre en effet.
Ma methode est assez proche, mais a l'avantage de fonctionner aussi sur Mobile, et de plus dans mon cas les Threads se tuent si tu sors du jeu sans passer par OnApplicationQuit(Alt F4, kill de process, ou bouton Home sur Mobile)

Rajoute un petit attribut lastUpdate=System.Datetime.now dans ta class, que tu mets a jour dans ton FixedUpdate, et tu compares ensuite dans tes Threads, si la difference est superieur a xxx sec, tu t'autokill :hehe:
Menfou devient zugsoft

Avatar de l’utilisateur
ZJP
Messages : 5611
Inscription : 15 Déc 2009 06:00

Re: [C#] Threads "Classiques" en usages avancés sous Unity. ;)

Message par ZJP » 27 Mars 2017 23:22

zugsoft a écrit :Ma methode est assez proche, mais a l'avantage de fonctionner aussi sur Mobile..
C'est aussi le cas ici sans l'API Windows de "répartition" des Threads vers un CPU quelconque. On reste après tout sur du 100% Mono/Csharp.
A ce propos, pas de souci non plus avec les monocores (testé avec succès sur un Acer eMachines E430 Cpu Sempron -entres autres- vieux de 6 ans). La méthode de "relance" des Threads conserve ses avantages.
En revanche, il faut éviter de trop surcharger l'unique CPU. :langue2:

Ce qu'il faut surtout comprendre ( et apprécier :hehe: ) avec la "ventilation" des Threads et les Cores "épargnés" c'est que l'on peut "bourrer" autant de Threads que l'on veut (dans la limite du système bien sur) avec des taches TRÈS lourdes (ou TRÈS courtes finalement) : cela n'a pas vraiment d'importance pour le Thread principal (ou Threads principaux/initiaux)
zugsoft a écrit : et de plus dans mon cas les Threads se tuent si tu sors du jeu sans passer par OnApplicationQuit(Alt F4, kill de process, ou bouton Home sur Mobile):
Idem ici. C'est la particularité du "jobThread.IsBackground = true;" de .NET.
Le "disjoncteur" a (surtout) pour but d’arrêter TOUT les threads sous L'IDE sans attendre la fin de ceux ci, car un "Alt-F4" dans ce cas et bye bye les modifications de scène ou autre... :mrgreen:
zugsoft a écrit :Rajoute un petit attribut lastUpdate=System.Datetime.now dans ta class, que tu mets a jour dans ton FixedUpdate, et tu compares ensuite dans tes Threads, si la difference est superieur a xxx sec, tu t'autokill :hehe:
Oui, par exemple. ;-)



PS :

Les tableaux de Matrix4x4 étant accessible dans les Threads cela ouvre de bonne perspective avec Graphics.DrawMeshInstanced

PS2:

Source mis à jour :

- Anglicisme exagéré en prévision d'un sujet sur le forum officiel :mrgreen: Edit : Fait...
- Ajout d'une fonction de Coroutine qui d'après un sujet du forum officiel serait plus efficace qu'une boucle "Update()". De toute façon cela permet de configurer un cycle de vérification des threads terminés.
Pour triompher, le mal n’a besoin que de l’inaction des gens de bien.Edmund Burke (1729-1797)

Répondre

Revenir vers « Scripts »