Voici mon petit bout de code qui permet d'exploiter la classe native de Thread Pooling :
Code : Tout sélectionner
using System;
using System.Threading;
using UnityEngine;
public static class ThreadPool
{
private static ManualResetEvent[] doneEvents = new ManualResetEvent[1];
private struct ThreadPoolData
{
public Action action;
public ManualResetEvent doneEvent;
}
// Pool actions in background while main thread is running
public static void Pool(params Action[] actions){
for (int i = 0; i < actions.Length; i++){
ThreadPoolData data = new ThreadPoolData();
data.action = actions[i];
System.Threading.ThreadPool.QueueUserWorkItem(new System.Threading.WaitCallback (PoolNewThread), data);
}
}
private static void PoolNewThread(System.Object stateInfo){
try{
((ThreadPoolData)stateInfo).action.Invoke ();
}catch(Exception e) {
Debug.Log("Error in thread "+e);
}
}
// Pool actions and wait for all of them to finish
public static void PoolAndWait(params Action[] actions){
if(doneEvents.Length < actions.Length){
doneEvents = new ManualResetEvent[actions.Length];
}
for (int i = 0; i < doneEvents.Length; i++){
if(doneEvents[i] == null){
doneEvents[i] = new ManualResetEvent (true);
}else{
doneEvents[i].Set();
}
}
for (int i = 0; i < actions.Length; i++){
doneEvents[i].Reset();
ThreadPoolData data = new ThreadPoolData();
data.action = actions[i];
data.doneEvent = doneEvents[i];
System.Threading.ThreadPool.QueueUserWorkItem (new System.Threading.WaitCallback (WaitForThread), data);
}
WaitHandle.WaitAll (doneEvents);
}
private static void WaitForThread(System.Object stateInfo){
ThreadPoolData data = (ThreadPoolData)stateInfo;
try{
data.action.Invoke ();
}catch(Exception e) {
Debug.Log("Error in thread "+e);
}
data.doneEvent.Set();
}
}
Il est important de noter que comme la plupart des classes unity ne sont pas "thread safe", il ne sera pas possible d'y toucher depuis une fonction exécutée dans un thread, sous risque de plantage, voir de freeze d'unity.
J'ai ajouté un try/catch pour essayer d'éviter ça... à tester pour voir si ça fonctionne bien dans tous les cas.
Si vous avez par exemple un gros tableau de données à traiter, il faut répartir le traitement sur plusieurs threads, en évitant de faire se croiser les accès au données.
Il faut donc assigner à chaque thread une plage de données à traiter. Dans l'exemple ci-dessous, je divise le total selon le nombre de coeurs du cpu disponibles, pour que chacun ne s'occupe que d'une partie du tableau, sans taper dans celle des autres.
Assignez ce script à un gameobject, puis lancez l'execution. Il y a trois tests, un pour chaque bouton de la souris :
- bouton de gauche : exécution asynchrone (donc en arrière plan) de la fonction de test sur chaque coeur
- bouton de droite : exécution synchrone de la fonction de test sur chaque coeur, dans ce cas, le thread principal sera bloqué le temps que tous les threads secondaires se terminent.
Ici on divise donc le temps de traitement par le nombre de coeurs disponibles.
- bouton du milieu : même test que le bouton de droite, mais en doublant le nombre de coeurs à utiliser.
Dans ce cas, mon tableau va être divisé en 8 parts au lieu de 4, et chaque coeur va traiter 2 parts.
On se rend compte que ça va légèrement moins vite que si on utilise que 4 coeurs, du moins dans mon cas.
Ceci n'est valable que lorsqu'on veut exécuter du code synchrone, en répartissant la tâche sur plusieurs coeurs.
Dans le cas d'une exécution asynchrone, les fonctions vont être exécutées dans leur ordre d'ajout, à chaque fois qu'un coeur sera libre pour la traiter.
On ne peut donc pas prédire à l'avance quand les fonctions seront traitées ni quand elles se termineront.
La grosse boucle à la fin, c'est pour allonger la durée du traitement, afin d'avoir des chronométrages compréhensibles
Code : Tout sélectionner
using UnityEngine;
using System;
public class ThreadPool_tests : MonoBehaviour {
Vector3[] buffer = new Vector3[19];
int coresCount = 0;
void Start () {
coresCount = SystemInfo.processorCount;
}
void Update () {
if(Input.GetMouseButtonDown(0)){
StartTest(coresCount);
Invoke("DebugBuffer",1);
}
if(Input.GetMouseButtonDown(1)){
StartTest(coresCount,true);
DebugBuffer();
}
if(Input.GetMouseButtonDown(2)){
StartTest(coresCount * 2,true);
DebugBuffer();
}
}
void DebugBuffer(){
for(int i=0;i<buffer.Length;i++){
Debug.Log ("buffer "+i+" : "+buffer[i]+" updated by thread #"+buffer[i].y);
}
}
void StartTest(int count,bool wait = false){
int i;
Vector3 vector = Vector3.zero;
Debug.Log ("Test loop start");
double now = Time.realtimeSinceStartup;
int total = buffer.Length;
int step = Mathf.CeilToInt((float)buffer.Length / count);
int start = 0;
int end = 0;
// prepare actions
Action[] actions = new Action[count];
for(i = 0;i < count; i++){
vector.y = i;
Vector3 _vector = vector;
int _i = i;
string _string = "label "+i;
int _start = start;
start += step;
int _end = Mathf.Min (start,total);
actions[i] = () => this.Test(_i,_vector,_string,_start,_end);
}
// send all the actions to the ThreadPool
if(wait){
ThreadPool.PoolAndWait(actions);
}else{
ThreadPool.Pool(actions);
}
now = (Time.realtimeSinceStartup - now)*1000;
Debug.Log ("Test loop end "+now+"ms");
}
public void Test(int index,Vector3 vector,string str,int start,int end){
Debug.Log ("START "+index+" "+vector+" "+str);
int now = Environment.TickCount;
for(int i = start;i<end;i++){
vector.x = i;
buffer[i] = vector;
}
int total = (end-start) * 1000000;
for(int i =0;i<total;i++){
float dist = Mathf.Sqrt(Vector3.Distance(Vector3.one,Vector3.one*5.3f ));
}
Debug.Log ("END "+index+" "+vector+" "+str+" : "+(Environment.TickCount - now)+"ms");
}
}