Suite

Les fonctions principales de QGIS exploiteront-elles le multi-threading ?


Après avoir lu Activer le traitement multithread dans QGIS?, je me demandais si QGIS 2.6 inclurait cela lors de l'exécution de divers processus. J'ai vérifié en ligne et je n'ai trouvé que ça rendu multi-thread a été introduit dans QGIS 2.4 (je suppose qu'il sera à nouveau disponible dans 2.6). Désormais, les cartes peuvent être redessinées très rapidement, en particulier lorsqu'il s'agit de grands ensembles de données.

J'ai lu qu'il est assez difficile d'inclure le multi-threading aux fonctionnalités principales et qu'il est conseillé aux utilisateurs de manipuler le code Python pour y parvenir, comme dans Paralléliser les opérations SIG dans PyQGIS?.

J'ai également vérifié la demande de fonctionnalité mais elle est fermée depuis 9 mois maintenant par Tim Sutton avec le dernier commentaire étant :

"Je ferme ceci - Martin Dobias a une implémentation dans une branche qui sera fusionnée après QGIS 2.2"

Certaines fonctions de QGIS 2.6 exploiteront-elles le multi-threading (ou seront-elles à nouveau axées sur le rendu) et sinon, cela changera-t-il dans QGIS 3.0 ?


Je pense que la réponse pour QGIS 3.0 peut être trouvée dans cette récente conversation publiée dans Nabble :

Le traitement dans QGIS 3 prendra-t-il en charge la parallélisation ?

Citant Nyall Dawson :

Si vous recherchez la parallélisation dans un seul algorithme (par exemple, des fonctionnalités de mise en mémoire tampon à l'aide de plusieurs threads), alors je ne suis au courant d'aucun plan en place pour gérer cela.


Rendu uniquement à l'heure actuelle (QGIS 2.6).

Martin et moi avons parlé d'une sorte d'API de processus threadé générique, mais il ne s'agit que de discussions à l'heure actuelle.


Q : Quand devez-vous utiliser le multithreading ?

R : "Votre question est très large. Il existe peu de systèmes non triviaux où la fonctionnalité peut être satisfaite de manière simple, rapide et fiable avec un seul thread. Par exemple : [choisissez un système typique que la société cible vend et sélectionnez quelques aspects de sa fonction qui seraient mieux définis - CPU lourd, communications, multi-utilisateur - choisissez simplement quelque chose de probable et expliquez].

Q : Le multithreading serait-il bénéfique si les différents threads exécutaient des tâches indépendantes les unes des autres ?

R : "Dépend de ce que vous entendez par « exécuter des tâches ». Le multithreading serait certainement bénéfique si les threads traitaient des données mutuellement indépendantes de manière concurrente - cela réduit les besoins en verrous et la probabilité de blocages augmente de manière super linéaire avec le nombre de verrous. OTOH, il n'y a aucun problème avec les threads exécutant le même code, c'est sûr et très courant."


3 réponses 3

Le système d'exploitation propose tranche de tempss de CPU aux threads éligibles à l'exécution.

S'il n'y a qu'un seul cœur, le système d'exploitation programme le thread le plus éligible pour qu'il s'exécute sur ce cœur pendant une tranche de temps. Une fois qu'une tranche de temps est terminée, ou lorsque le thread en cours d'exécution se bloque sur les E/S, ou lorsque le processeur est interrompu par des événements externes, le système d'exploitation réévalue le thread à exécuter ensuite (et il peut choisir à nouveau le même thread ou un autre).

L'éligibilité à l'exécution consiste en des variations sur l'équité, la priorité et la préparation, et par cette méthode, divers threads obtiennent des tranches de temps, certains plus que d'autres.

S'il y a plusieurs cœurs, N, alors le système d'exploitation planifie les N threads les plus éligibles pour qu'ils s'exécutent sur les cœurs.

Affinité du processeur est une considération d'efficacité. Chaque fois qu'un processeur exécute un thread différent qu'auparavant, il a tendance à ralentir un peu car son cache est chaud pour le thread précédent, mais froid pour le nouveau. Ainsi, l'exécution du même thread sur le même processeur sur de nombreuses tranches de temps est un avantage d'efficacité.

Cependant, le système d'exploitation est libre d'offrir des tranches de temps d'un thread sur différents processeurs, et il pourrait tourner à travers tous les processeurs sur différentes tranches de temps. Cependant, comme le dit @ gnasher729, il ne peut pas exécuter un thread sur plusieurs processeurs simultanément.

L'hyperthreading est une méthode matérielle par laquelle un seul renforcée Le cœur du processeur peut prendre en charge l'exécution de deux ou plus différent fils simultanément. (Un tel processeur peut offrir des threads supplémentaires à moindre coût dans l'immobilier en silicium que des cœurs complets supplémentaires.) Ce cœur de processeur amélioré doit prendre en charge un état supplémentaire pour les autres threads, tels que les valeurs de registre du processeur, et possède également un état de coordination et un comportement qui permet le partage d'unités fonctionnelles au sein de cette CPU sans amalgamer les threads.

L'hyperthreading, bien que techniquement difficile d'un point de vue matériel, du point de vue du programmeur, le modèle d'exécution est simplement celui de cœurs de processeur supplémentaires plutôt que quelque chose de plus complexe. Ainsi, le système d'exploitation voit des cœurs de processeur supplémentaires, bien qu'il y ait de nouveaux problèmes d'affinité du processeur car plusieurs threads hyperthread partagent l'architecture de cache d'un cœur de processeur.

Nous pourrions naïvement penser que deux threads s'exécutant sur un cœur hyperthreadded s'exécutent chacun deux fois moins vite qu'ils le feraient chacun avec leur propre cœur complet. Mais ce n'est pas nécessairement le cas, car l'exécution d'un seul thread est pleine de cycles de relâchement, et une partie d'entre eux peut être utilisée par l'autre thread hyperthread. En outre, même pendant les cycles sans relâchement, un thread peut utiliser des unités fonctionnelles différentes de l'autre, de sorte qu'une exécution simultanée peut se produire. Le processeur amélioré pour l'hyperthreading peut avoir un peu plus de certaines unités fonctionnelles fortement utilisées spécialement pour prendre en charge cela.

Il n'existe pas de thread unique s'exécutant sur plusieurs cœurs simultanément.

Cela ne signifie pas, cependant, que les instructions d'un thread ne peuvent pas être exécutées en parallèle. Il existe des mécanismes appelés pipeline d'instructions et exécution dans le désordre qui le permettent. Chaque cœur a beaucoup de ressources redondantes qui ne sont pas utilisées par des instructions simples, donc plusieurs de ces instructions peuvent être exécutées ensemble (tant que la suivante ne dépend pas du résultat précédent). Cependant, cela se produit toujours à l'intérieur d'un seul noyau.

L'hyper-threading est une sorte de variante extrême de cette idée, dans laquelle un noyau exécute non seulement les instructions d'un thread en parallèle, mais mélange les instructions de deux threads différents pour optimiser encore plus l'utilisation des ressources.

résumé: La recherche et l'exploitation du parallélisme (au niveau des instructions) dans un programme monothread se font uniquement dans le matériel, par le cœur du processeur sur lequel il s'exécute. Et seulement sur une fenêtre de quelques centaines d'instructions, pas de réorganisation à grande échelle.

Les programmes monothread ne bénéficient d'aucun avantage des processeurs multicœurs, sauf que autre les choses peuvent s'exécuter sur les autres cœurs au lieu de prendre du temps sur la tâche à thread unique.

le système d'exploitation organise les instructions de tous les threads de manière à ce qu'ils ne s'attendent pas les uns les autres.

Le système d'exploitation ne regarde PAS à l'intérieur des flux d'instructions des threads. Il ne planifie que les threads vers les cœurs.

En fait, chaque cœur exécute la fonction de planification du système d'exploitation lorsqu'il doit déterminer ce qu'il faut faire ensuite. L'ordonnancement est un algorithme distribué. Pour mieux comprendre les machines multicœurs, pensez à chaque cœur comme exécutant le noyau séparément. Tout comme un programme multithread, le noyau est écrit de sorte que son code sur un cœur puisse interagir en toute sécurité avec son code sur d'autres cœurs pour mettre à jour les structures de données partagées (comme la liste des threads prêts à être exécutés.

Quoi qu'il en soit, le système d'exploitation est impliqué pour aider les processus multi-threads à exploiter parallélisme au niveau des threads qui doit être explicitement exposé en écrivant manuellement un programme multithread. (Ou par un compilateur à parallélisation automatique avec OpenMP ou quelque chose du genre).

Ensuite, le front-end de la CPU organise davantage ces instructions en distribuant un fil à chaque cœur, et distribue des instructions indépendantes de chaque fil parmi tous les cycles ouverts.

Un cœur de processeur n'exécute qu'un seul flux d'instructions, s'il n'est pas arrêté (en veille jusqu'à la prochaine interruption, par exemple une interruption de la minuterie). Il s'agit souvent d'un thread, mais il peut aussi s'agir d'un gestionnaire d'interruption du noyau, ou de divers codes de noyau si le noyau décide de faire autre chose que de simplement revenir au thread précédent après avoir géré et interrompu ou appelé système.

Avec l'HyperThreading ou d'autres conceptions SMT, un cœur de processeur physique agit comme plusieurs cœurs « logiques ». La seule différence du point de vue du système d'exploitation entre un processeur quadricœur avec hyperthreading (4c8t) et une simple machine à 8 cœurs (8c8t) est qu'un système d'exploitation compatible HT essaiera de planifier les threads pour séparer les cœurs physiques afin qu'ils ne ' t rivaliser les uns avec les autres. Un système d'exploitation qui ne connaissait pas l'hyperthreading ne verrait que 8 cœurs (à moins que vous ne désactiviez HT dans le BIOS, il n'en détecterait que 4).

Le terme "front-end" fait référence à la partie d'un cœur de processeur qui récupère le code machine, décode les instructions et les envoie dans la partie hors service du cœur. Chaque cœur a son propre front-end, et il fait partie du cœur dans son ensemble. Instructions qu'il récupère sommes ce que le processeur exécute actuellement.

À l'intérieur de la partie hors service du noyau, les instructions (ou uops) sont envoyées aux ports d'exécution lorsque leurs opérandes d'entrée sont prêts et qu'il existe un port d'exécution libre. Cela ne doit pas nécessairement se produire dans l'ordre du programme, donc c'est ainsi qu'un processeur OOO peut exploiter le parallélisme au niveau des instructions au sein d'un seul thread.

Si vous remplacez « core » par « unité d'exécution » dans votre idée, vous êtes presque correct. Oui, le CPU distribue des instructions/uops indépendants aux unités d'exécution en parallèle. (Mais il y a un mélange de terminologie, puisque vous avez dit "front-end" alors qu'en réalité c'est le planificateur d'instructions du processeur alias Station de réservation qui sélectionne les instructions prêtes à être exécutées).

L'exécution dans le désordre ne peut trouver ILP qu'à un niveau très local, seulement jusqu'à quelques centaines d'instructions, pas entre deux boucles indépendantes (à moins qu'elles ne soient courtes).

Par exemple, l'équivalent asm de ce

fonctionnera à peu près aussi vite que la même boucle en incrémentant seulement un compteur sur Intel Haswell. i++ ne dépend que de la valeur précédente de i , tandis que j++ ne dépend que de la valeur précédente de j , donc les deux chaînes de dépendance peut fonctionner en parallèle sans briser l'illusion que tout s'exécute dans l'ordre du programme.

Sur x86, la boucle ressemblerait à ceci :

Haswell dispose de 4 ports d'exécution d'entiers, et tous ont des unités d'addition, il peut donc supporter un débit allant jusqu'à 4 instructions inc par horloge si elles sont toutes indépendantes. (Avec latence=1, vous n'avez donc besoin que de 4 registres pour maximiser le débit en gardant 4 instructions inc en vol. Comparez cela avec vector-FP MUL ou FMA : latence=5 débit=0,5 a besoin de 10 accumulateurs vectoriels pour garder 10 FMA dans vol pour maximiser le débit. Et chaque vecteur peut être 256b, contenant 8 flotteurs simple précision).

La prise de branche est également un goulot d'étranglement : une boucle prend toujours au moins une horloge entière par itération, car le débit de prise de branche est limité à 1 par horloge. Je pourrais mettre une instruction supplémentaire dans la boucle sans réduire les performances, à moins qu'elle ne lise/écrive également eax ou edx, auquel cas cela allongerait cette chaîne de dépendance. Mettre 2 instructions supplémentaires dans la boucle (ou une instruction multi-uops complexe) créerait un goulot d'étranglement sur le front-end, car il ne peut émettre que 4 uops par horloge dans le noyau en panne. (Voir ce SO Q&A pour quelques détails sur ce qui se passe pour les boucles qui ne sont pas un multiple de 4 uops : le tampon de boucle et le cache uop rendent les choses intéressantes.)

Dans les cas plus complexes, trouver le parallélisme nécessite de regarder une plus grande fenêtre d'instructions. (par exemple, il y a peut-être une séquence de 10 instructions qui dépendent toutes les unes des autres, puis certaines indépendantes).

La capacité du tampon de réapprovisionnement est l'un des facteurs qui limite la taille de la fenêtre de non-ordre. Sur Intel Haswell, c'est 192 uops. (Et vous pouvez même le mesurer expérimentalement, ainsi que la capacité de renommage des registres (taille du fichier de registre).) Les cœurs de processeur à faible consommation comme ARM ont des tailles de ROB beaucoup plus petites, s'ils effectuent une exécution dans le désordre.

Notez également que les processeurs doivent être mis en pipeline, ainsi que dans le désordre. Il doit donc récupérer et décoder les instructions bien avant celles en cours d'exécution, de préférence avec un débit suffisant pour recharger les tampons après avoir raté des cycles de récupération. Les branches sont délicates, car nous ne savons même pas où chercher si nous ne savons pas dans quel sens une branche est allée. C'est pourquoi la prédiction de branche est si importante. (Et pourquoi les processeurs modernes utilisent l'exécution spéculative : ils devinent dans quelle direction une branche ira et commencent à récupérer/décoder/exécuter ce flux d'instructions. Lorsqu'une erreur de prédiction est détectée, ils reviennent au dernier état connu et s'exécutent à partir de là.)

Si vous souhaitez en savoir plus sur les composants internes du processeur, il existe des liens dans le wiki des balises Stackoverflow x86, notamment le guide microarch d'Agner Fog et les articles détaillés de David Kanter avec des diagrammes des processeurs Intel et AMD. D'après son article sur la microarchitecture Intel Haswell, il s'agit du schéma final de l'ensemble du pipeline d'un cœur Haswell (pas de la puce entière).

Il s'agit d'un schéma fonctionnel d'un Célibataire noyau CPU. Un processeur quadricœur en a 4 sur une puce, chacun avec ses propres caches L1/L2 (partageant un cache L3, des contrôleurs de mémoire et des connexions PCIe avec les périphériques système).

Je sais que c'est extrêmement compliqué. L'article de Kanter en montre également des parties pour parler du frontend séparément des unités d'exécution ou des caches, par exemple.


GO - Goroutine et Concurrence

pthreads suivre l'ordonnancement préemptif, alors que Fibres C++ suivre l'ordonnancement coopératif.

Avec Pthreads: le chemin d'exécution actuel peut être interrompu ou préempté à tout moment Cela signifie que pour les threads, l'intégrité des données est un gros problème car un thread peut être arrêté au milieu de la mise à jour d'un bloc de données, laissant l'intégrité des données dans un mauvais ou état incomplet. Cela signifie également que le système d'exploitation peut tirer parti de plusieurs processeurs et cœurs de processeur en exécutant plusieurs threads en même temps et en laissant au développeur le soin de protéger l'accès aux données.

En utilisant C,

Utiliser des fils, l'application peut avoir la simultanéité,

Propriétés de la concurrence :

1) Plusieurs acteurs

2) Ressource partagée

3) Règles d'accès (synchronisation atomique/conditionnelle)

Avec des fibres C++: le chemin d'exécution actuel n'est interrompu que lorsque la fibre cède l'exécution. Cela signifie que les fibres démarrent et s'arrêtent toujours à des endroits bien définis, de sorte que l'intégrité des données est beaucoup moins problématique. De plus, comme les fibres sont souvent gérées dans l'espace utilisateur, il n'est pas nécessaire d'effectuer des changements de contexte coûteux et des changements d'état du processeur, ce qui rend le passage d'une fibre à l'autre extrêmement efficace. D'un autre côté, étant donné que deux fibres ne peuvent pas fonctionner exactement en même temps, l'utilisation de fibres seules ne tirera pas parti de plusieurs processeurs ou de plusieurs cœurs de processeur.

Dans Win32, un fibre est une sorte de thread géré par l'utilisateur. Une fibre a sa propre pile et son propre pointeur d'instruction etc., mais les fibres ne sont pas programmées par l'OS : vous devez appeler SwitchToFiber explicitement. Les threads, en revanche, sont planifiés de manière préventive par le système d'exploitation.

Donc grosso modo, une fibre est un thread qui est géré au niveau de l'application/runtime plutôt que d'être un véritable thread du système d'exploitation.

En utilisant C,

Pourquoi des fibres C++ ?

Système d'exploitation fils donnez-nous tout ce que nous voulons, mais pour une forte pénalité de performances : basculer entre les threads implique de passer du mode utilisateur au mode noyau, peut-être même au-delà des limites de l'espace d'adressage. Ce sont des opérations coûteuses en partie parce qu'elles TLB rince, cache manque et Des ravages dans le pipeline du processeur: c'est aussi pourquoi les traps et les syscalls peuvent être des ordres de grandeur plus lents que les appels de procédure normaux.

De plus, le noyau planifie les threads (c'est-à-dire attribue leur continuation à un cœur de processeur) à l'aide d'un algorithme de planification à usage général, qui peut prendre en compte toutes sortes de threads, de ceux servant une seule transaction à ceux qui lisent une vidéo entière.

Fibres, car ils sont planifiés au niveau de la couche application, peuvent utiliser un planificateur plus approprié à leur cas d'utilisation. Comme la plupart des fibres sont utilisées pour servir les transactions, ils sont généralement actifs pendant de très courtes périodes et bloquent très souvent. Leur comportement doit souvent être réveillé par IO ou une autre fibre, exécuter un cycle de traitement court, puis transférer le contrôle à une autre fibre (à l'aide d'une file d'attente ou d'un autre mécanisme de synchronisation). Un tel comportement est mieux servi par un ordonnanceur utilisant un algorithme appelé « voler du travail »Lorsque les fibres se comportent de cette façon, vol de travail assure manque de cache minime lorsque commutation entre les fibres.

La fibre n'exploite pas la puissance de plusieurs cœurs, car ce que le système d'exploitation connaît, c'est un processus à thread unique.

Dans GO, nous invoquons des goroutines en utilisant le mot-clé go

1) La routine GO( f ) est-elle une fibre qui n'est pas planifié de manière préventive par l'exécution de GO, dans l'espace utilisateur ?


10 réponses 10

Tout ce qu'un thread fait est d'entrelacer des opérations de sorte que certaines parties du processus semblent se chevaucher dans le temps. Une machine monocœur avec plusieurs threads saute simplement : elle exécute de petits morceaux de code à partir d'un thread, puis passe à un autre thread. Un simple planificateur décide quel thread a la priorité la plus élevée et est réellement exécuté dans le noyau.

Sur un ordinateur monocœur, rien se passe réellement "en même temps". C'est juste une exécution entrelacée.

Il existe de très nombreuses façons de réaliser l'entrelacement. Beaucoup.

Supposons que vous ayez un processus simple à deux threads qui utilise un verrou simple pour que les deux threads puissent écrire dans une variable commune. Vous avez six blocs de code.

  • T1-avant verrouillage
  • T1-avec serrure
  • T1-après serrure
  • T2-avant serrure
  • T2-avec serrure
  • T2-après serrure

[Cela peut être dans une boucle ou avoir plus de verrous ou autre. Tout ce qu'il fait, c'est devenir plus long, pas plus complexe.]

Les étapes de T1 doivent s'exécuter dans l'ordre (T1-avant, T1-avec, T1-après) et les étapes de T2 doivent s'exécuter dans l'ordre (T2-avant, T2-avec, T2-après).

En dehors de la contrainte "dans l'ordre", ceux-ci peuvent être entrelacés de n'importe quelle manière. En tous cas. Ils pourraient être exécutés comme indiqué ci-dessus. Un autre ordre valide est (T1-avant, T2-avant, T2-verrouillage, T1-verrouillage, T2-après, T1-après). Il y a beaucoup de commandes valides.

C'est juste une machine à états avec six états.

C'est un automate à états finis non déterministe. L'ordre des états T1-xxx avec les états T2-xxx est indéterminé et n'a pas d'importance. Il y a donc des endroits où "l'état suivant" est un tirage au sort.

Par exemple, lorsque le FSM démarre, T1-avant ou T2-avant sont tous deux des premiers états légitimes. Un tirage au sort.

Disons que c'est arrivé T1 avant. Faites ça. Lorsque cela est fait, vous avez le choix entre T1-with et T2-before. Un tirage au sort.

À chaque étape du FSM, il y aura deux choix (deux fils -- deux choix) et un tirage au sort peut déterminer quel état spécifique est suivi.

L'écriture de fonctions de blocage est destinée aux personnes qui ne peuvent pas créer de machines à états)

Les threads sont utiles si vous ne pouvez pas contourner le blocage. Aucune activité informatique fondamentale n'est vraiment bloquante, c'est juste que beaucoup d'entre elles sont implémentées de cette façon pour une facilité d'utilisation. Au lieu de renvoyer un caractère ou un "échec de lecture", une fonction de lecture se bloque jusqu'à ce que tout le tampon soit lu. Au lieu de rechercher un message de retour dans une file d'attente et de le renvoyer si aucun n'est trouvé, une fonction de connexion attend la réponse.

Vous ne pouvez pas utiliser de fonctions de blocage dans une machine d'état (au moins une qui ne peut pas être autorisée à "geler").

Et oui, l'utilisation de la machine à états est une alternative viable. Dans les systèmes Temps Réel, c'est la seule option, le système fournissant un cadre à la machine. L'utilisation de threads et de fonctions de blocage n'est que "la solution de facilité", car généralement un appel à une fonction de blocage remplace environ 3-4 états dans la machine d'état.

Comment obtenir une fonctionnalité multi-thread dans un langage de haut niveau, tel que Java, en utilisant un seul thread et une seule machine à états ? Par exemple, que se passe-t-il s'il y a 2 activités à effectuer (faire des calculs et faire des E/S) et qu'une activité peut bloquer ?

Ce que tu décris s'appelle multitâche coopératif, où les tâches reçoivent le CPU et sont censées l'abandonner volontairement après un certain temps ou une activité autodéterminée. Une tâche qui ne coopère pas en continuant à utiliser le processeur ou en bloquant tout le travail et à moins d'avoir une minuterie de surveillance matérielle, le code supervisant les tâches ne peut rien y faire.

Ce que vous voyez dans les systèmes modernes s'appelle multitâche préemptif, c'est là que les tâches n'ont pas à abandonner le CPU car le superviseur le fait pour elles lorsqu'une interruption générée par le matériel arrive. La routine de service d'interruption dans le superviseur enregistre l'état de la CPU et le restaure la prochaine fois que la tâche est considérée comme méritant une tranche de temps, puis restaure l'état de la tâche à exécuter ensuite et y revient comme si de rien n'était . Cette action est appelée un changement de contexte et peut coûter cher.

L'utilisation de la "machine à états uniquement" est-elle une alternative viable au multi-threading dans les langages de haut niveau ?

Viable? Sûr. Sain? Quelquefois. Que vous utilisiez des threads ou une forme de multitâche coopératif maison (par exemple, des machines à états) dépend des compromis que vous êtes prêt à faire.

Les threads simplifient la conception des tâches au point où vous pouvez traiter chacun comme son propre programme qui partage l'espace de données avec d'autres. Cela vous donne la liberté de vous concentrer sur le travail à accomplir et non sur toute la gestion et l'entretien ménager nécessaires pour le faire fonctionner une itération à la fois. Mais comme aucune bonne action ne reste impunie, vous payez pour toute cette commodité dans les changements de contexte. Avoir de nombreux threads qui cèdent le CPU après avoir effectué un travail minimal (volontairement ou en faisant quelque chose qui bloquerait, comme les E/S) peut consommer beaucoup de temps processeur pour la commutation de contexte. Cela est particulièrement vrai si vos opérations de blocage bloquent rarement très longtemps.

Il y a des situations où la voie coopérative a plus de sens. Une fois, j'ai dû écrire un logiciel utilisateur pour un matériel qui diffusait de nombreux canaux de données via une interface mappée en mémoire qui nécessitait une interrogation. Chaque canal était un objet construit de telle manière que je pouvais soit le laisser fonctionner en tant que thread, soit exécuter à plusieurs reprises un seul cycle d'interrogation.

Les performances de la version multithread n'étaient pas bonnes du tout pour exactement la raison que j'ai décrite ci-dessus : chaque thread effectuait un travail minimal et cédait ensuite le CPU afin que les autres canaux puissent avoir du temps, provoquant de nombreux changements de contexte. Laisser les threads s'exécuter librement jusqu'à ce qu'ils soient préemptés a contribué au débit, mais a entraîné le non-entretien de certains canaux avant que le matériel ne subisse un dépassement de mémoire tampon, car ils n'ont pas obtenu de tranche de temps assez tôt.

La version à thread unique, qui effectuait des itérations égales de chaque canal, fonctionnait comme un singe échaudé et la charge sur le système tombait comme un roc. La pénalité que j'ai payée pour la performance supplémentaire était de devoir jongler moi-même avec les tâches. Dans ce cas, le code pour le faire était suffisamment simple pour que le coût de développement et de maintenance valait bien l'amélioration des performances. Je suppose que c'est vraiment l'essentiel. Si mes discussions avaient été celles qui attendaient le retour d'un appel système, l'exercice n'aurait probablement pas valu la peine.


[Udemy 100 % de réduction] -Qgis 101

Obtenez un code de réduction Udemy 100% gratuit (code promotionnel gratuit UDEMY), vous pourrez vous inscrire à ce cours “Qgis 101” totalement GRATUIT pour un accès à vie. Dépêchez-vous ou vous devrez payer $ $

Exigences

Durée du cours: 2 heures

Instructeur: Géologue minier

Évaluations en anglais: 4.3

La description

L'objectif du cours est d'initier l'étudiant à QGIS, un libre et Open source logiciel de système d'information géographique de bureau, avec un environnement de travail complexe qui permet aux utilisateurs d'analyser et de modifier des informations spatiales. QGIS prend en charge plusieurs types de données (formats vectoriels et raster), des services Web, fournissant en même temps une variété de commandes et d'utilitaires utiles pour le géotraitement en raison de son intégration avec GDAL et OGR bibliothèques.

Le cours est structuré en unités ayant différents niveaux de difficulté qui viennent en aide à l'étudiant qui apprendra les concepts généraux d'un système d'information géographique, travaillant avec des données vectorielles, alphanumériques et matricielles, manipulant les outils de géo-traitement et la création de cartes.

Les étudiants inscrits à ce cours en ligne apprendront à la fin de ce cours comment utiliser efficacement Qgis. Le cours ne couvrira que les bases au début et une section de niveau intermédiaire, et une section avancée sera ajoutée plus tard.


2 réponses 2

(Ce n'est pas une réponse complète, mais cela semble trop long pour tenir dans un commentaire.)

De nombreux facteurs affectent le taux d'adoption de la « conception pour le parallélisme » dans l'industrie du logiciel. Certains d'entre eux n'avaient rien à voir avec les avantages. Par exemple, les ensembles de compétences et les niveaux de connaissances des développeurs, etc.

Une de mes observations est que le type d'application détermine son taux d'adoption du paradigme parallèle. Chaque ligne de produit (niveau) ou composant logiciel a un ou plusieurs "domaines/paradigmes naturels", c'est-à-dire que le logiciel serait beaucoup plus facile à développer et à maintenir s'il était implémenté dans un paradigme particulier.

Si un changement de paradigme est nécessaire pour paralléliser une certaine application, il est probable que les éditeurs de logiciels ne trouveront pas cela rentable à justifier. Si ce paradigme particulier est facilement parallélisable, alors ce que vous voyez, c'est que ces logiciels auraient un taux d'adoption plus élevé pour la programmation parallèle.

Concernant la liste des paradigmes, je voudrais ajouter Dataflow. Toutes les tâches sont déclarées à l'avance. Chaque tâche déclare ses entrées et sorties avant exécution. Une tâche est lancée dès que toutes ses données d'entrée sont disponibles.

Exemples de paradigme Dataflow :

Avoir un paradigme très réussi ne suffit pas. Pour augmenter le taux d'adoption de la programmation parallèle, le parallélisme doit également être introduit dans d'autres paradigmes (y compris les "obsolètes").

J'en ai vu d'autres qui ont réussi à implémenter le parallélisme dans un programme d'interface graphique Windows, en créant une boîte de dialogue Windows (chaque boîte de dialogue Windows est hébergée dans un thread) par tâche de calcul et en échangeant des données à l'aide de messages Windows.

Cela fait écho à mon observation ci-dessus : si l'introduction du parallélisme dans une application nécessite que cette application soit réécrite dans un paradigme non naturel, alors la complexité du développement et de la maintenance sera augmentée.

Pour les tâches purement informatiques, le gain de performances correspond généralement très étroitement à la prédiction de la loi d'Amdahl, à condition que tous les calculs soient effectués localement sur un ordinateur (c'est-à-dire non soumis au trafic d'E/S réseau beaucoup plus lent.)

Cela dit, vous découvrirez rapidement qu'il existe des goulots d'étranglement non parallélisables dans vos applications. Parfois, ces goulots d'étranglement sont théoriquement non parallélisables, ce qui signifie qu'il n'y a aucun espoir de trouver un meilleur algorithme.

Une histoire personnelle. J'ai écrit un programme parallélisé simple qui décode un fichier JPEG, le redimensionne, puis l'enregistre dans un format de fichier image personnalisé. Lors des tests, je constate que le programme prend 1,6 seconde pour se terminer, lorsqu'il est testé en utilisant 3 threads ou 4 threads. Il s'avère que l'étape de décodage JPEG prend plus de 25 % du temps, ce qui en fait l'étape non parallélisable la plus lente.

En d'autres termes, la loi d'Amdahl prend effet avec seulement 3-4 cœurs de processeur pour mon petit programme.

Parfois, ces goulots d'étranglement peuvent être supprimés si vous êtes autorisé à modifier les exigences logicielles (par exemple, si vous pouviez demander à vos clients de ne pas utiliser un format d'image particulier), mais la plupart du temps, les exigences sont figées.

Vous ne pouvez pas faire en sorte que du code non écrit pour la concurrence fonctionne correctement simultanément (et inclut comme par magie l'utilisation de compilateurs).

Plusieurs fois, les conditions ont changé et le code a dû être fait différemment. Dans les temps très anciens, la mémoire était une prime, donc le code auto-modifiable était courant. De nos jours, les avantages du code en mémoire en lecture seule sont si importants que tous les systèmes d'exploitation modernes veulent l'appliquer.

Pensez également à ce qui s'est passé lorsque Windows est devenu populaire. Tous les programmeurs DOS ont dû repenser leurs méthodes de programmation. Vous ne pouviez pas avoir une seule boucle d'interrogation de clavier invoquant la fonctionnalité complète de l'application dans un seul programme à threads - vous deviez avoir des gestionnaires d'événements - ce qui à son tour changeait la façon dont le code de l'application était conçu.

Des mantras comme « utilisez des objets immuables ! » sont des expériences apprises à la dure. Les raisons sont souvent perdues en chemin mais peuvent être reconstituées. L'"utilisation d'objets immuables" est un moyen simple d'autoriser la mise en cache des données à plusieurs endroits sans que votre programme ne soit interrompu par la mise à jour d'un de ces endroits.

Le changement nécessaire pour être simultané n'a pas besoin d'être une réécriture complète dans un langage fonctionnel, mais peut être résolu avec des bibliothèques - ce qui permet de conserver les bibliothèques de code existantes - mais vous devez toujours écrire votre programme pour l'utiliser. L'approche OpenCL (Grand Central sous OS X) est un moyen très intéressant d'utiliser à la fois le CPU et le GPU pour exécuter du code, mais encore une fois, vous devez créer votre programme en conséquence.

En Java, beaucoup d'efforts ont été consacrés à la fourniture de bons blocs de construction pour une mise à l'échelle transparente de l'exécution de petits extraits de code, mais vous ne pouvez pas l'utiliser si vous n'écrivez pas pour cela.


4 réponses 4

La spécification des fonctions ctime et asctime remonte à C89, et les choses se faisaient un peu différemment à l'époque, principalement parce que les systèmes multiprocesseurs n'étaient pas très courants et que l'utilisation d'un tampon statique ne poserait donc pas de gros problème.

Très probablement, ils n'ont pas renvoyé de mémoire allouée dynamiquement car cela prenait plus de temps, et à cette époque les cycles CPU étaient plus difficiles à trouver.

Si vous êtes sur un système POSIX comme Linux, vous disposez de deux autres fonctions qui correspondent essentiellement à ce que vous avez décrit comme alternative :

Ces fonctions prennent un pointeur vers un tampon qui peut recevoir la sortie (et elles renvoient un pointeur vers ce même tampon). Le suffixe _r signifie "réentrant", ce qui signifie qu'il peut être appelé en toute sécurité dans un programme multithread ou plus d'une fois sans point de séquence entre les deux.


Cela semble correct si un peu bâclé avec un formatage différent pour certains appels print x vs print(x) (ce dernier étant vraiment préférable), vous devriez probablement également utiliser des classes de nouveau style, c'est-à-dire la classe Loader(object): .

En dehors de cela, la principale préoccupation que j'aurais est que le sémaphore doit être protégé contre les exceptions. C'est surtout une préoccupation pour les gros scripts, mais c'est quand même une bonne habitude. Ainsi, la méthode de libération doit être appelée indépendamment de savoir si une exception a été déclenchée par quelque chose d'autre dans le thread - sinon le programme pourrait simplement y rester bloqué, ce qui est probablement bien pour un script unique.

Vous devriez probablement également vérifier si le threading améliore réellement le débit, compte tenu du verrouillage de l'interpréteur global Python.


Déboguer la corruption de la mémoire

Tout d'abord, je me rends compte que ce n'est pas une question de style Q&A parfaite avec une réponse absolue, mais je ne peux penser à aucune formulation pour que cela fonctionne mieux. Je ne pense pas qu'il y ait une solution absolue à cela et c'est l'une des raisons pour lesquelles je le poste ici au lieu de Stack Overflow.

Au cours du dernier mois, j'ai réécrit un morceau de code serveur assez ancien (mmorpg) pour qu'il soit plus moderne et plus facile à étendre/modifier. J'ai commencé par la partie réseau et implémenté une bibliothèque tierce (libevent) pour gérer les choses pour moi. Avec toutes les refactorisations et les changements de code, j'ai introduit une corruption de mémoire quelque part et j'ai eu du mal à trouver où cela se produit.

Je n'arrive pas à le reproduire de manière fiable sur mon environnement de développement/test, même lors de l'implémentation de bots primitifs pour simuler une charge, je n'ai plus de plantage (j'ai corrigé un problème de libevent qui a causé des problèmes)

Valgrinding the hell out of it - No invalid writes until the thing crashes (which may take 1+ day in production.. or just an hour) which is really baffling me, surely at some point it would access invalid memory and not overwrite stuff by chance? (Is there a way to "spread out" the address range?)

Code-Analysis tools, namely coverity and cppcheck. While they did point out some.. nastiness and edge cases in the code there was nothing serious.

Recording the process until it crashes with gdb (via undodb) and then working my way backwards. This /sounds/ like it should be doable, but I either end up crashing gdb by using the auto-complete feature or I end up in some internal libevent structure where I get lost since there's too many possible branches (one corruption causing another and so on). I guess it would be nice if I could see what a pointer originally belongs to/where it was allocated, that would eliminate most of the branching-issues. I cant run valgrind with undodb though, and I the normal gdb record is unusably slow (if that even works in combination with valgrind).

Code review! By myself (thoroughly) and having some friends look over my code, though I doubt it was thorough enough. I was thinking about maybe hiring a dev to do some code review/debugging with me, but I cant afford to put too much money in it and I wouldn't know where to look for someone who'd be willing to work for little-to-no money if he doesn't find the issue or anyone qualified at all.

I should also note: I usually get consistent backtraces. There are a few places where the crash happens, mostly related to the socket class becoming corrupted somehow. Be it an invalid pointer pointing to something which isn't a socket or the socket class itself becoming overwritten (partially?) with gibberish. Although I suspect it's crashing there the most since that's one of the mostly used parts, so it's the first corrupted memory which gets used.

All in all this issue has had me busy for nearly 2 month (on and off, more of a hobby project) and is really frustrating me to the point where I become grumpy IRL and think about just giving up. I just can't think about what else I am supposed to do to find the issue.

Are there any useful techniques I missed? How do you deal with that? (It can't be that common since there isn't much information about this.. or I'm just really blind?)

Some specs in case it matters:

Using c++(11) via gcc 4.7 (version supplied by debian wheezy)

The codebase is around 150k lines

Edit in response to david.pfx post: (sorry for the slow response)

Are you keeping careful records of crashes, to look for patterns?

Yes, I still have dumps of the recent crashes lying around

Are the few places really similar? In what way?

Well, in the most recent version (they seem to change whenever I add/remove code or change related structures) it would always get caught in an item timer method. Basically an item has a specific time after which it expires and it sends updated info to the client. The invalid socket pointer would be in the (still valid as far as I can tell) Player class, mostly related to that. I am also experiencing loads of crashes in the cleanup phase, after the normal shutdown where it's destroying all the static classes that haven't been explicitly destroyed ( __run_exit_handlers in the backtrace). Mostly involving std::map of one class, guessing that's just the first thing that comes up though.

What does the corrupt data look like? Zeros? Ascii? Patterns?

I haven't found any patterns yet, seems somewhat random to me. It's hard to tell since I don't know where the corruption started.

It's entirely heap-related (I enabled gcc's stack guard and that didn't catch anything).

Does the corruption happen after a free() ?

You're going to have to elaborate a bit on that one. Do you mean having pointers of already free'd objects lying around? I'm setting every reference to null once the object gets destroyed, so unless I missed something somewhere, no. That should show up in valgrind though which it didn't.

Is there something distinctive about the network traffic (buffer size, recovery cycle)?

The network traffic consists of raw data. So char arrays, (u)intX_t or packed (to remove padding) structs for more complex things, each packet has a header consisting of an id and the packet size itself which is validated against the expected size. They are around 10-60bytes with the biggest (internal 'bootup' packet, fired once at startup) having a size of a few Mb.

Lots and lots of production asserts. Crash early and predictably before the damage propagates.

I once had a crash related to std::map corruption, each entity has a map of it's "view", each entity that can see it and vice versa is in that. I added a 200byte buffer in front and after, filled it with 0x33 and checked it before each access. The corruption just magically vanished, I must've moved something around which made it corrupt something else.

Strategic logging, so you know accurately what was happening just before. Add to the logging as you get closer to an answer.

In desperation, can you save state and auto-restart? I can think of a few pieces of production software that do that.

I somewhat do that. The software consists of a main "cache" process and some other worker ones which all access the cache to get and save stuff. So per crash I don't lose much progress, it still disconnects all the users and so on, it's definitely not a solution.

Concurrency: threading, race conditions, etc

There's a mysql thread to do "async" queries, that's all untouched though and only shares information to the database class via functions with all lock.

There's an interrupt timer to prevent it from locking up that just aborts if it didn't complete a cycle for 30 seconds, that code should be safe though:

tics is volatile int tics = 0 which is increased each time a cycle is completed. Old code too.

events/callbacks/exceptions: corrupting state or the stack unpredictably

Lots of callbacks are being used (async network I/O, timers), but they shouldn't do anything bad.

Unusual data: unusual input data/timing/state

I've had a few edge cases related to that. Disconnecting a socket while packets are still being processed resulted in accessing a nullptr and such, but those have been easy to spot so far since every reference gets cleaned up right after telling the class itself it's done. (Destruction itself is handled by a loop deleting all the destroyed objects each cycle)

Dependency on an asynchronous external process.

Care to elaborate? This is somewhat the case, the cache process mentioned above. Only thing I could imagine off the top of my head would be it not finishing quick enough and using garbage data, but that's not the case since that's using network too. Same packet model.


Voir la vidéo: Les fonctions grammaticales (Octobre 2021).