{"id":2529,"date":"2012-07-16T07:00:40","date_gmt":"2012-07-16T06:00:40","guid":{"rendered":"http:\/\/www.blaess.fr\/christophe\/?p=2529"},"modified":"2012-11-30T13:34:13","modified_gmt":"2012-11-30T12:34:13","slug":"incrementation-et-non-atomicite","status":"publish","type":"post","link":"https:\/\/www.blaess.fr\/christophe\/2012\/07\/16\/incrementation-et-non-atomicite\/","title":{"rendered":"Incr\u00e9mentation et (non) atomicit\u00e9"},"content":{"rendered":"<p style=\"text-align: justify;\"><a href=\"http:\/\/www.blaess.fr\/christophe\/2012\/07\/16\/incrementation-et-non-atomicite\/\"><img loading=\"lazy\" decoding=\"async\" class=\"alignright size-full wp-image-2548\" title=\"Incr\u00e9mentation (non) atomique\" src=\"http:\/\/www.blaess.fr\/christophe\/wp-content\/uploads\/2012\/06\/incrementation-pas-atomique-reduit.png\" alt=\"Incr\u00e9mentation (non) atomique\" width=\"150\" height=\"150\" \/><\/a>J&rsquo;ai remarqu\u00e9, au cours de plusieurs <a title=\"Formations\" href=\"http:\/\/www.blaess.fr\/christophe\/formations\/\" target=\"_blank\">sessions de formations<\/a>, que de nombreux d\u00e9veloppeurs pensent que certains op\u00e9rateurs du C (ou de ses descendants)\u00a0 sont naturellement atomiques vis-\u00e0-vis de l&rsquo;ordonnancement. Pour v\u00e9rifier ou r\u00e9futer ceci, j&rsquo;ai fait r\u00e9cemment quelques essais avec les participants d&rsquo;un de mes cours.<\/p>\n<p style=\"text-align: justify;\">\n<!--more-->\n<\/p>\n<h1>Atomicit\u00e9 d&rsquo;une op\u00e9ration<\/h1>\n<p style=\"text-align: justify;\">La notion d&rsquo;atomicit\u00e9 qui nous concerne ici se rapporte \u00e0 l&rsquo;ordonnancement. Une op\u00e9ration sera consid\u00e9r\u00e9e comme atomique vis-\u00e0-vis du <em>scheduler<\/em> si ce dernier nous garantit qu&rsquo;une fois l&rsquo;op\u00e9ration entam\u00e9e, elle s&rsquo;ex\u00e9cutera jusqu&rsquo;\u00e0 son terme sans \u00eatre interrompue par une autre t\u00e2che en attente. Il existe diff\u00e9rent degr\u00e9s d&rsquo;atomicit\u00e9 suivant le niveau d\u00e9sir\u00e9 de protection contre les discontinuit\u00e9 d&rsquo;ex\u00e9cution.<\/p>\n<p style=\"text-align: justify;\">Les instructions assembleurs ex\u00e9cut\u00e9es par le micro-processeur sont g\u00e9n\u00e9ralement atomiques et se d\u00e9roulent en une seule \u00e9tape (mais en plusieurs cycles d&rsquo;horloge) m\u00eame si une demande d&rsquo;interruption mat\u00e9rielle (IRQ &#8211; <em>Interrupt Request<\/em>) survient pendant leur ex\u00e9cution. Ces instructions sont souvent tr\u00e8s simples et la moindre op\u00e9ration un tant soit peu significative pour un programme applicatif regroupe plusieurs &#8211; voire plusieurs dizaines &#8211; d&rsquo;instructions assembleurs. Notons que l&rsquo;atomicit\u00e9 de certaines instructions assembleurs r\u00e9alisant plusieurs actions comme celle nomm\u00e9e <code>BTS<\/code> (<em>Bit Test and Set<\/em>) dans les processeurs x86 (qui fixe un bit d&rsquo;un registre et renvoie sa valeur pr\u00e9c\u00e9dente) est essentielle pour la construction de certaines structures de contr\u00f4le et outils de synchronisation (mutex, spinlock, s\u00e9maphores&#8230;).<\/p>\n<p style=\"text-align: justify;\">Dans le code du noyau, il est possible de rendre une portion de code atomique en coupant les interruptions sur le processeur local &#8211; avec <code>local_irq_disable()<\/code> ou <code>local_irq_save()<\/code> &#8211; ou en indiquant \u00e0 l&rsquo;ordonnanceur qu&rsquo;une t\u00e2che ne doit pas \u00eatre pr\u00e9empt\u00e9e &#8211; en appelant <code>preempt_disable()<\/code>. Ceci n&rsquo;est toutefois valable que dans le code kernel, aucune de ces op\u00e9rations n&rsquo;est accessible \u00e0 l&rsquo;espace utilisateur.<\/p>\n<p style=\"text-align: justify;\">\u00a0Pour les applications, l&rsquo;atomicit\u00e9 d&rsquo;une op\u00e9ration est habituellement rendue n\u00e9cessaire pour se pr\u00e9munir contre l&rsquo;acc\u00e8s simultan\u00e9 \u00e0 des zones de m\u00e9moire partag\u00e9e. Imaginons que nous souhaitions v\u00e9rifier la somme de contr\u00f4le d&rsquo;une trame de donn\u00e9es re\u00e7ue depuis un port s\u00e9rie. Notre t\u00e2che commence \u00e0 additionner les octets de la trame. Soudain, une interruption se produit et l&rsquo;ordonnanceur d\u00e9cide d&rsquo;activer une autre t\u00e2che qui travaille sur la m\u00eame zone de m\u00e9moire (pour y inscrire une nouvelle trame re\u00e7ue par exemple). Lorsque nous revenons sur notre premi\u00e8re t\u00e2che, celle-ci termine son calcul de <em>checksum<\/em> avec une fin de trame qui n&rsquo;est plus en corr\u00e9lation avec les premier octets observ\u00e9s auparavant. Donc une somme de contr\u00f4le invalide. Donc une trame de donn\u00e9es rejet\u00e9e ind\u00fbment. Donc un bug&#8230;<\/p>\n<p style=\"text-align: justify;\">Pour se prot\u00e9ger, on a coutume de verrouiller syst\u00e9matiquement un mutex &#8211; avec <code>pthread_mutex_lock()<\/code> &#8211; avant tout acc\u00e8s \u00e0 des donn\u00e9es partag\u00e9es entre threads du m\u00eame processus puis de le lib\u00e9rer ensuite avec <code>pthread_mutex_unlock()<\/code>. Lorsque les donn\u00e9es sont partag\u00e9es entre diff\u00e9rents processus plut\u00f4t qu&rsquo;entre threads, on emploie un s\u00e9maphore &#8211; de l&rsquo;API Posix avec <code>sem_wait()<\/code> et <code>sem_post()<\/code> ou Syst\u00e8me V avec <code>semop()<\/code>.<\/p>\n<h1>Non atomicit\u00e9 d&rsquo;une op\u00e9ration<\/h1>\n<p style=\"text-align: justify;\">Dans la longue liste des l\u00e9gendes modernes et des croyances erron\u00e9es comme les <code>void main(void)<\/code> ou <code>fflush(stdin)<\/code> en C, ou <code>export PATH=$PATH:...<\/code> dans un script shell, ou encore\u00a0 <code>sync;sync;sync<\/code> avant d&rsquo;arr\u00eater un syst\u00e8me, il en est une qui a la vie dure&nbsp;: \u00ab\u00a0<em>l&rsquo;op\u00e9rateur ++ du C effectue une incr\u00e9mentation atomique<\/em>\u00a0\u00bb parfois nuanc\u00e9e en \u00ab\u00a0<em>l&rsquo;op\u00e9rateur ++ <span style=\"text-decoration: underline;\">sur une variable de type <code>int<\/code><\/span> <\/em> est atomique\u00a0\u00bb.<\/p>\n<p style=\"text-align: center;\">Non, le C ne nous garantit rien de tel&nbsp;!<\/p>\n<p style=\"text-align: left;\">La preuve&nbsp;? Essayons&nbsp;!<\/p>\n<p style=\"text-align: justify;\">Je vais prendre en exemple l&rsquo;incr\u00e9mentation d&rsquo;une variable enti\u00e8re partag\u00e9e entre deux threads.<\/p>\n<p style=\"text-align: justify;\">La variable est initialis\u00e9e \u00e0 z\u00e9ro. Pour mettre en \u00e9vidence la collision durant les acc\u00e8s \u00e0 cette variable, je vais lancer deux threads qui essayeront simultan\u00e9ment de l&rsquo;incr\u00e9menter un milliard de fois. Une fois les deux threads termin\u00e9s, nous afficherons la valeur de cette variable. En toute logique nous attendons \u00ab\u00a0deux milliards\u00a0\u00bb mais ce n&rsquo;est pas ce que nous obtiendrons.<\/p>\n<pre><strong><a title=\"http:\/\/www.blaess.fr\/christophe\/files\/article-2012-07-02\/threads_increments.c\" href=\"http:\/\/www.blaess.fr\/christophe\/files\/article-2012-07-02\/threads_increments.c\">threads_increments.c<\/a>:<\/strong>\n#include &lt;pthread.h&gt;\n#include &lt;stdio.h&gt;\n#include &lt;stdlib.h&gt;\n#include &lt;unistd.h&gt;\n\nint compteur = 0;\n\nvoid * fonction_thread (void * unused)\n{\n    int i;\n    int j;\n    for (i = 0; i &lt; 1000000000; i ++)\n        <strong>compteur ++;<\/strong>\n    return NULL;\n}\n\nint main(void)\n{\n    pthread_t thr1, thr2;\n    pthread_create(&amp; thr1, NULL, fonction_thread, NULL);\n    pthread_create(&amp; thr2, NULL, fonction_thread, NULL);\n    pthread_join(thr1, NULL);\n    pthread_join(thr2, NULL);\n    fprintf(stdout, \"%dn\", compteur);\n    return EXIT_SUCCESS;\n}<\/pre>\n<p style=\"text-align: justify;\">\u00a0Ce genre de code n&rsquo;est bien s\u00fbr pas propre du tout, il faudrait prot\u00e9ger par un mutex l&rsquo;acc\u00e8s \u00e0 cette variable globale. Essayons-le&#8230;<\/p>\n<pre>$ <strong>gcc threads_increments.c -o threads_increments -pthread -O2<\/strong>\n$ <strong>.\/threads_increments<\/strong> \n1003987499\n$ <strong>.\/threads_increments<\/strong> \n1029276863\n$ <strong>.\/threads_increments<\/strong> \n1024894145\n$<\/pre>\n<p style=\"text-align: justify;\">Nous esp\u00e8rions atteindre deux milliards et nous arrivons poussivement jusqu&rsquo;\u00e0 un milliard&#8230; Essayons de contraindre nos deux threads sur le m\u00eame CPU.<\/p>\n<pre>$ <strong>taskset -c 0 .\/threads_increments<\/strong>\n1191969269\n$ <strong>taskset -c 0 .\/threads_increments<\/strong>\n1188035064\n$ <strong>taskset -c 0 .\/threads_increments<\/strong>\n1191212419\n$<\/pre>\n<p style=\"text-align: justify;\">Il y a un peu moins de collisions si les deux t\u00e2ches s&rsquo;ex\u00e9cutent sur un m\u00eame c\u0153ur de processeur (car la concurrence repose alors sur la pr\u00e9emption mutuelle et non plus sur le parall\u00e9lisme), mais le r\u00e9sultat n&rsquo;est toujours pas concluant.<\/p>\n<p style=\"text-align: justify;\">Vous pouvez essayer de reprendre cet exemple avec des variantes comme<\/p>\n<ul>\n<li>d\u00e9clarer la variable globale <code>volatile<\/code>,<\/li>\n<li>utiliser <code>++compteur<\/code> au lieu de <code>compteur++<\/code>,<\/li>\n<li>compiler avec ou sans l&rsquo;option <code>-O2<\/code> (optimisation du code produit)<\/li>\n<li>etc.<\/li>\n<\/ul>\n<p style=\"text-align: justify;\">Rien n&rsquo;y fait, les collisions d&rsquo;acc\u00e8s \u00e0 la variable globale rendent le r\u00e9sultat incorrect. La seule solution serait d&rsquo;encadrer ces acc\u00e8s par des <code>pthread_mutex_lock()<\/code> \/ <code>pthread_mutex_unlock()<\/code>.<\/p>\n<h1>Code assembleur<\/h1>\n<p style=\"text-align: justify;\">Il peut para\u00eetre \u00e9tonnant que l&rsquo;op\u00e9ration <code>i++<\/code> (ou <code>++i<\/code> au choix) ne soit pas atomique. Observons le code assembleur produit par <code>gcc<\/code> en repartant d&rsquo;un exemple plus simple que le pr\u00e9c\u00e9dent.<\/p>\n<pre><a title=\"http:\/\/www.blaess.fr\/christophe\/files\/article-2012-07-02\/incrementation.c\" href=\"http:\/\/www.blaess.fr\/christophe\/files\/article-2012-07-02\/incrementation.c\"><strong>incrementation.c:<\/strong><\/a>\n#include &lt;stdio.h&gt;\n#include &lt;stdlib.h&gt;\n\nint main(int argc, char * argv[])\n{\n    int i;\n\n    if ((argc != 2)\n     || (sscanf(argv[1], \"%d\", &amp; i) != 1)) {\n        fprintf(stderr, \"usage: %s valeurn\", argv[0]);\n        exit(EXIT_FAILURE);\n    }\n    fprintf(stdout, \"Valeur initiale : %dn\", i);\n\n    i ++;\n\n    fprintf(stdout, \"Valeur incrementee : %dn\", i);\n    return EXIT_SUCCESS;\n}<\/pre>\n<p style=\"text-align: justify;\">Compilons-le et examinons le code d\u00e9sassembl\u00e9.<\/p>\n<pre>$ <strong>gcc incrementation.c -o incrementation <\/strong>\n$ <strong>objdump -d incrementation<\/strong>\n[...]\n080484b4 &lt;main&gt;:\n 80484b4:       55                      push   %ebp\n 80484b5:       89 e5                   mov    %esp,%ebp\n 80484b7:       83 e4 f0                and    $0xfffffff0,%esp\n 80484ba:       83 ec 20                sub    $0x20,%esp\n 80484bd:       83 7d 08 02             cmpl   $0x2,0x8(%ebp)\n 80484c1:       75 26                   jne    80484e9 &lt;main+0x35&gt;\n 80484c3:       ba 40 86 04 08          mov    $0x8048640,%edx\n 80484c8:       8b 45 0c                mov    0xc(%ebp),%eax\n 80484cb:       83 c0 04                add    $0x4,%eax\n 80484ce:       8b 00                   mov    (%eax),%eax\n 80484d0:       8d 4c 24 1c             lea    0x1c(%esp),%ecx\n 80484d4:       89 4c 24 08             mov    %ecx,0x8(%esp)\n 80484d8:       89 54 24 04             mov    %edx,0x4(%esp)\n 80484dc:       89 04 24                mov    %eax,(%esp)\n 80484df:       e8 0c ff ff ff          call   80483f0 &lt;__isoc99_sscanf@plt&gt;\n 80484e4:       83 f8 01                cmp    $0x1,%eax\n 80484e7:       74 2b                   je     8048514 \n 80484e9:       8b 45 0c                mov    0xc(%ebp),%eax\n 80484ec:       8b 08                   mov    (%eax),%ecx\n 80484ee:       ba 43 86 04 08          mov    $0x8048643,%edx\n 80484f3:       a1 20 a0 04 08          mov    0x804a020,%eax\n 80484f8:       89 4c 24 08             mov    %ecx,0x8(%esp)\n 80484fc:       89 54 24 04             mov    %edx,0x4(%esp)\n 8048500:       89 04 24                mov    %eax,(%esp)\n 8048503:       e8 d8 fe ff ff          call   80483e0 &lt;fprintf@plt&gt;\n 8048508:       c7 04 24 01 00 00 00    movl   $0x1,(%esp)\n 804850f:       e8 ac fe ff ff          call   80483c0 &lt;exit@plt&gt;\n 8048514:       8b 4c 24 1c             mov    0x1c(%esp),%ecx\n 8048518:       ba 55 86 04 08          mov    $0x8048655,%edx\n 804851d:       a1 40 a0 04 08          mov    0x804a040,%eax\n 8048522:       89 4c 24 08             mov    %ecx,0x8(%esp)\n 8048526:       89 54 24 04             mov    %edx,0x4(%esp)\n 804852a:       89 04 24                mov    %eax,(%esp)\n 804852d:       e8 ae fe ff ff          call   80483e0 &lt;fprintf@plt&gt;\n <strong>8048532: 8b 44 24 1c mov 0x1c(%esp),%eax<\/strong>\n <strong>8048536: 83 c0 01 add $0x1,%eax<\/strong>\n <strong>8048539: 89 44 24 1c mov %eax,0x1c(%esp)<\/strong>\n 804853d:       8b 4c 24 1c             mov    0x1c(%esp),%ecx\n 8048541:       ba 6b 86 04 08          mov    $0x804866b,%edx\n 8048546:       a1 40 a0 04 08          mov    0x804a040,%eax\n 804854b:       89 4c 24 08             mov    %ecx,0x8(%esp)\n 804854f:       89 54 24 04             mov    %edx,0x4(%esp)\n 8048553:       89 04 24                mov    %eax,(%esp)\n 8048556:       e8 85 fe ff ff          call   80483e0 &lt;fprintf@plt&gt;\n 804855b:       b8 00 00 00 00          mov    $0x0,%eax\n 8048560:       c9                      leave  \n 8048561:       c3                      ret    \n[...]\n$<\/pre>\n<p style=\"text-align: justify;\">Inutile d&rsquo;\u00eatre un sp\u00e9cialiste de l&rsquo;assembleur PC pour comprendre ce code. On peut suivre assez facilement sa structure.<\/p>\n<ul>\n<li><code>80484bd cmpl...<\/code> : test correspondant \u00e0 <code>(argc&nbsp;!= 2)<\/code><\/li>\n<li><code>80484df call...<\/code> : invocation de <code>sscanf(argv[1]...<\/code><\/li>\n<li><code>8048503 call...<\/code> : affichage d&rsquo;erreur avec <code>fprintf(stderr, \"usage...\"<\/code><\/li>\n<li><code>804850f call...<\/code> : sortie du programme <code>exit(EXIT_FAILURE)<\/code><\/li>\n<li><code>804852d call...<\/code> : premier affichage <code>fprintf(stdout, \"Valeur initiale...<\/code><\/li>\n<li><code>8048556 call...<\/code> : second affichage avec <code>fprintf(stdout, \"Valeur incrementee...<\/code><\/li>\n<\/ul>\n<p style=\"text-align: justify;\">Mais nous pouvons \u00e9galement apercevoir les lignes suivantes<\/p>\n<pre> 8048532:       8b 44 24 1c             mov    0x1c(%esp),%eax\n 8048536:       83 c0 01                add    $0x1,%eax\n 8048539:       89 44 24 1c             mov    %eax,0x1c(%esp)<\/pre>\n<p style=\"text-align: justify;\">Clairement, il s&rsquo;agit de notre op\u00e9ration d&rsquo;incr\u00e9mentation. Et tout aussi clairement, elle n&rsquo;est pas du tout atomique. Il est tout \u00e0 fait possible que notre t\u00e2che soit pr\u00e9empt\u00e9e entre le premier chargement de <code>eax<\/code> depuis la m\u00e9moire et l&rsquo;addition, ou entre l&rsquo;addition et la re\u00e9\u00e9criture de la valeur incr\u00e9ment\u00e9e.<\/p>\n<p style=\"text-align: justify;\">Pourtant il existe bien un op\u00e9rateur <code>INC<\/code> dans l&rsquo;assembleur 80386 capable d&rsquo;agir sur un mot m\u00e9moire de 1, 2 ou 4 octets. Peut-\u00eatre <code>gcc<\/code> ne l&rsquo;utilise-t&rsquo;il qu&rsquo;en optimisation de code&nbsp;? V\u00e9rifions&#8230;<\/p>\n<pre>$ <strong>gcc incrementation.c -o incrementation -O2<\/strong>\n$ <strong>objdump -d incrementation<\/strong>\n[...]\n08048420 &lt;main&gt;:\n [...]\n 8048473:       e8 98 ff ff ff          call   8048410 &lt;__fprintf_chk@plt&gt;\n <strong>8048478: 8b 44 24 1c mov 0x1c(%esp),%eax<\/strong>\n 804847c:       c7 44 24 08 8b 86 04    movl   $0x804868b,0x8(%esp)\n 8048483:       08 \n 8048484:       c7 44 24 04 01 00 00    movl   $0x1,0x4(%esp)\n 804848b:       00 \n <strong>804848c: 83 c0 01 add $0x1,%eax<\/strong>\n <strong>804848f: 89 44 24 1c mov %eax,0x1c(%esp)<\/strong>\n 8048493:       89 44 24 0c             mov    %eax,0xc(%esp)\n 8048497:       a1 40 a0 04 08          mov    0x804a040,%eax\n 804849c:       89 04 24                mov    %eax,(%esp)\n 804849f:       e8 6c ff ff ff          call   8048410 &lt;__fprintf_chk@plt&gt;\n [...]\n$<\/pre>\n<p style=\"text-align: justify;\">C&rsquo;est encore pire&nbsp;! Cette fois deux instructions sont intercal\u00e9es entre la lecture de la valeur initiale et son incr\u00e9mentation. Encore plus de risque d&rsquo;\u00eatre pr\u00e9empt\u00e9&#8230;<\/p>\n<p style=\"text-align: justify;\">Essayons de pr\u00e9ciser que nous travaillons avec un processeur descendant du 80386.<\/p>\n<pre>$ <strong>gcc incrementation.c -o incrementation -O2 -mtune=i386<\/strong>\n$ <strong>objdump -d incrementation<\/strong>\n[...]\n 8048476:       8b 44 24 1c             mov    0x1c(%esp),%eax\n 804847a:       40                      inc    %eax\n 804847b:       89 44 24 1c             mov    %eax,0x1c(%esp)\n[...]\n$<\/pre>\n<p style=\"text-align: justify;\">Le code produit utilise <code>inc&nbsp;%eax<\/code> au lieu de <code>add $0x1,%eax<\/code> mais il n&rsquo;incr\u00e9mente toujours pas la variable directement en m\u00e9moire.<\/p>\n<h1>Conclusion<\/h1>\n<p style=\"text-align: justify;\">Notre premi\u00e8re exp\u00e9rience a mis en \u00e9vidence des collisions sur une variable partag\u00e9e entre threads. Le code \u00e9tait outrancier puisqu&rsquo;il s&rsquo;agissait d&rsquo;acc\u00e8s intensifs et simultan\u00e9s. Je ne crois pas qu&rsquo;un d\u00e9veloppeur puisse raisonnablement \u00e9crire ce genre de chose. Toutefois, j&rsquo;ai d\u00e9j\u00e0 vu des programmes o\u00f9 des acc\u00e8s (rares) \u00e0 certaines variables enti\u00e8res (compteurs statistiques, flags du type \u00ab\u00a0<code>condition_1_ok<\/code>\u00a0\u00bb etc.) n&rsquo;\u00e9tait pas prot\u00e9g\u00e9s en partant de cette croyance que l&rsquo;incr\u00e9mentation d&rsquo;un variable <code>int<\/code> \u00e9tait atomique. C&rsquo;est un bug. Un bug qui se d\u00e9clenche tr\u00e8s rarement certes, mais c&rsquo;est justement ce qui le rend difficilement d\u00e9tectable. Tout acc\u00e8s \u00e0 des variables partag\u00e9es entre des t\u00e2ches &#8211; threads ou processus &#8211; doit \u00eatre prot\u00e9g\u00e9 par un m\u00e9canisme appropri\u00e9&nbsp;: mutex, spinlock, s\u00e9maphore, etc.<\/p>","protected":false},"excerpt":{"rendered":"<p>J&rsquo;ai remarqu&eacute;, au cours de plusieurs sessions de formations, que de nombreux d&eacute;veloppeurs pensent que certains op&eacute;rateurs du C (ou de ses descendants)&nbsp; sont naturellement atomiques vis-&agrave;-vis de l&rsquo;ordonnancement. Pour v&eacute;rifier ou r&eacute;futer ceci, j&rsquo;ai fait r&eacute;cemment quelques essais avec les participants d&rsquo;un de mes cours.<\/p>","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[8,10],"tags":[],"class_list":["post-2529","post","type-post","status-publish","format-standard","hentry","category-linux-2","category-microprocesseur"],"_links":{"self":[{"href":"https:\/\/www.blaess.fr\/christophe\/wp-json\/wp\/v2\/posts\/2529","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.blaess.fr\/christophe\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.blaess.fr\/christophe\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.blaess.fr\/christophe\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.blaess.fr\/christophe\/wp-json\/wp\/v2\/comments?post=2529"}],"version-history":[{"count":1,"href":"https:\/\/www.blaess.fr\/christophe\/wp-json\/wp\/v2\/posts\/2529\/revisions"}],"predecessor-version":[{"id":3230,"href":"https:\/\/www.blaess.fr\/christophe\/wp-json\/wp\/v2\/posts\/2529\/revisions\/3230"}],"wp:attachment":[{"href":"https:\/\/www.blaess.fr\/christophe\/wp-json\/wp\/v2\/media?parent=2529"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.blaess.fr\/christophe\/wp-json\/wp\/v2\/categories?post=2529"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.blaess.fr\/christophe\/wp-json\/wp\/v2\/tags?post=2529"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}