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..
- 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
// ************************************************************************************************************
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.