games / fivem
Threads & performance
Explication
Ce que vous allez apprendre
- Comprendre pourquoi un
CreateThreadsansWait()bloque tout le serveur/client - Ajuster dynamiquement la durée d'un
Wait()selon le contexte (sleep dynamique) - Éviter les allocations de tables/closures répétées dans une boucle chaude
- Utiliser le débounce pour regrouper des déclenchements rapprochés
- Identifier un thread mal écrit avant qu'il n'affecte tous les joueurs
Dans quel contexte ?
Un serveur avec soixante-quatre joueurs connectés commence à ramer visiblement pour tout le monde, sans qu'aucune erreur n'apparaisse dans la console. L'enquête révèle qu'une ressource, parmi la centaine installée, exécute une vérification coûteuse à chaque frame avec un Wait(0) permanent, alors qu'un Wait(1000) aurait suffi. Cette leçon donne les réflexes pour éviter ce genre de ralentissement collectif, et pour le diagnostiquer quand il survient malgré tout.
| Pattern | Coût | Correction |
|---|---|---|
Boucle sans Wait() | Freeze garanti du thread | Toujours au moins un Wait() |
Wait(0) permanent sur logique lourde | ~60 exécutions/seconde inutiles | Sleep dynamique selon le contexte |
| Table recréée à chaque itération | Pression GC constante | Sortir les constantes de la boucle |
Piège dangereux
Un CreateThread contenant une boucle while true do ... end SANS aucun Wait() à l'intérieur bloque complètement le moteur : c'est l'erreur de performance la plus grave et pourtant la plus facile à commettre par simple oubli.
Un serveur, des centaines de scripts qui partagent le même temps
Un serveur FiveM exécute simultanément des centaines, voire des milliers de petits bouts de code appelés "threads" (fils d'exécution), tous issus de dizaines de ressources différentes. Contrairement à ce qu'on pourrait imaginer, ces threads ne tournent pas réellement en parallèle sur des cœurs séparés : ils se partagent un temps de calcul limité. Un thread mal écrit qui accapare trop ce temps ralentit alors TOUS les autres, pour tous les joueurs connectés.
Pourquoi Wait() n'est pas optionnel
Wait() met en pause un thread pendant un nombre précis de millisecondes, rendant la main au reste du serveur pendant ce temps. Une boucle infinie sans aucun Wait() ne rend jamais la main : elle monopolise le temps de calcul en continu, ce qui provoque un blocage complet, perceptible par tous les joueurs. C'est l'erreur la plus grave et pourtant la plus facile à commettre par inattention.
Le sleep dynamique, un compromis intelligent
Entre "vérifier à chaque frame" (très réactif mais coûteux) et "vérifier une fois par seconde" (économe mais peu réactif), le bon compromis dépend souvent du contexte : rester peu réactif tant que rien d'intéressant ne se passe, et devenir très réactif seulement quand c'est réellement utile (par exemple, quand le joueur est tout près d'un point d'interaction). Ajuster dynamiquement la durée du Wait() selon la situation, comme montré dans le code, est un des réflexes de performance les plus rentables.
Le coût caché des allocations répétées
Créer une nouvelle table à chaque itération d'une boucle qui tourne très souvent oblige le "ramasse-miettes" de Lua (le mécanisme qui libère automatiquement la mémoire non utilisée) à travailler beaucoup plus que nécessaire. Sortir les valeurs constantes en dehors de la boucle, pour les réutiliser telles quelles à chaque passage, réduit cette pression invisible mais bien réelle sur les performances.
Le débounce : ne pas répéter ce qui n'a pas besoin de l'être
Quand une action coûteuse peut être déclenchée très souvent en peu de temps (chaque changement d'inventaire, par exemple), le débounce regroupe ces déclenchements rapprochés en une seule exécution différée, plutôt que d'exécuter l'action à chaque fois individuellement.
Commandes & code
Threads & performance
Un serveur FiveM tourne des centaines de threads Lua simultanément. Un thread mal écrit affecte tous les joueurs.
-- MAUVAIS : boucle sans Wait() -> freeze garanti (le thread bloque le moteur)
CreateThread(function()
while true do
-- pas de Wait() ici => crash / freeze du client ou du serveur
end
end)-- MAUVAIS : Wait(0) permanent sur une logique lourde -> tourne à chaque frame, gaspille le CPU
CreateThread(function()
while true do
CheckAllNearbyPeds() -- coûteux, exécuté ~60 fois par seconde inutilement
Wait(0)
end
end)-- BON : adapter dynamiquement le sleep selon la pertinence du contexte
CreateThread(function()
while true do
local sleep = 1000 -- valeur par défaut, thread "au repos"
local ped = PlayerPedId()
local coords = GetEntityCoords(ped)
local nearestShop = GetNearestShop(coords)
if nearestShop and #(coords - nearestShop.coords) < 20.0 then
sleep = 0 -- boucle rapide seulement quand c'est réellement utile
end
Wait(sleep)
end
end)-- Éviter de recréer des tables/valeurs à chaque itération d'une boucle chaude
-- MAUVAIS
CreateThread(function()
while true do
local heavyConfig = { a = 1, b = 2, c = 3 } -- réalloué à chaque tick, coûteux au GC
Wait(0)
end
end)
-- BON : sortir les constantes de la boucle
local heavyConfig = { a = 1, b = 2, c = 3 }
CreateThread(function()
while true do
-- utiliser directement heavyConfig, aucune allocation superflue
Wait(0)
end
end)-- Débounce pour limiter la fréquence d'exécution d'une action coûteuse
local debounceTimer = nil
local function DebouncedSave()
if debounceTimer then return end -- une sauvegarde est déjà programmée, on ignore
debounceTimer = SetTimeout(2000, function()
SavePlayerData(GetPlayerServerId(PlayerId()))
debounceTimer = nil
end)
end
-- Appelé potentiellement très souvent (ex: à chaque changement d'inventaire)
RegisterNetEvent('technologik:inventoryChanged', DebouncedSave)Résumé
- Chaque
CreateThreaddoit toujours contenir unWait(), même de courte durée. - Un
Wait(0)permanent doit être réservé aux logiques réellement critiques en fréquence (input, rendu). - Sortir les constantes/tables des boucles chaudes réduit la pression sur le garbage collector.
- Le debounce évite de ré-exécuter une action coûteuse à chaque petit changement d'état.
Exercices pratiques
Mission : trouver le thread qui ralentit tout le monde
Objectif : Diagnostiquer un Wait(0) permanent sur une logique lourde, le corriger avec un sleep dynamique, et raisonner sur le partage du temps de calcul entre threads.
Contexte
Un serveur de 64 joueurs sous technologik_rp commence à ramer visiblement pour tout le monde. En inspectant les ressources une par une, tu trouves dans technologik_peds un CreateThread qui appelle CheckAllNearbyPeds() (une fonction coûteuse qui scanne tous les PNJ proches) à l'intérieur d'une boucle while true do ... Wait(0) end.