2017-02-04 1 views
0

Je veux juste mon code aussi simple que possible et thread sécurisé.SPSC thread sûr avec des clôtures

Avec C11 atomics

En ce qui concerne une partie "7.17.4 clôtures" de la norme ISO/IEC 9899/201X projet

X et Y, les deux fonctionnant sur un certain objet M atomique, de telle sorte que A est séquence avant X, X modifie M, Y est séquencé avant B, et Y lit la valeur écrite par X ou une valeur écrite par un effet secondaire dans la séquence de libération hypothétique se dirigerait si elle était une opération de libération . Ce thread de code est-il sûr (avec "w_i" comme "objet M")?


Est-ce que "w_i" et "r_i" doivent être déclarés comme _Atomic?
Si seulement w_i est _Atomic, le thread principal peut-il conserver une ancienne valeur de r_i dans le cache et considérer la file d'attente comme incomplète (lorsqu'elle est pleine) et écrire des données?
Que se passe-t-il si je lis un atome sans atomic_load?

J'ai fait quelques tests mais toutes mes tentatives semblent donner les bons résultats. Cependant, je sais que mes tests ne sont pas vraiment corrects concernant le multithread: je lance mon programme plusieurs fois et regarde le résultat.

Même si ni w_i not r_i n'est déclaré comme _Atomic, mon programme fonctionne, mais seules les clôtures ne sont pas suffisantes en ce qui concerne la norme C11, n'est-ce pas?

typedef int rbuff_data_t; 

struct rbuf { 
    rbuff_data_t * buf; 
    unsigned int bufmask; 

    _Atomic unsigned int w_i; 
    _Atomic unsigned int r_i; 
}; 
typedef struct rbuf rbuf_t; 

static inline int 
thrd_tryenq(struct rbuf * queue, rbuff_data_t val) { 
    size_t next_w_i; 

    next_w_i = (queue->w_i + 1) & queue->bufmask; 

    /* if ring full */ 
    if (atomic_load(&queue->r_i) == next_w_i) { 
     return 1; 
    } 

    queue->buf[queue->w_i] = val; 
    atomic_thread_fence(memory_order_release); 
    atomic_store(&queue->w_i, next_w_i); 

    return 0; 
} 

static inline int 
thrd_trydeq(struct rbuf * queue, rbuff_data_t * val) { 
    size_t next_r_i; 

    /*if ring empty*/ 
    if (queue->r_i == atomic_load(&queue->w_i)) { 
     return 1; 
    } 
    next_r_i = (queue->r_i + 1) & queue->bufmask; 
    atomic_thread_fence(memory_order_acquire); 
    *val = queue->buf[queue->r_i]; 
    atomic_store(&queue->r_i, next_r_i); 
    return 0; 
} 

J'appeler des fonctions de thèses comme suit:
fil conducteur file d'attente le quelques données:

while (thrd_tryenq(thrd_get_queue(&tinfo[tnum]), i)) { 
    usleep(10); 
    continue; 
} 

fils Autres données dequeue:

static void * 
thrd_work(void *arg) { 
    struct thrd_info *tinfo = arg; 
    int elt; 

    atomic_init(&tinfo->alive, true); 

    /* busy waiting when queue empty */ 
    while (atomic_load(&tinfo->alive)) { 
     if (thrd_trydeq(&tinfo->queue, &elt)) { 
      sched_yield(); 
      continue; 
     } 
     printf("Thread %zu deq %d\n", 
       tinfo->thrd_num, elt); 
    } 

    pthread_exit(NULL); 
} 

Avec des clôtures asm

En ce qui concerne un x86 plate-forme spécifique avec lfence et sfence, Si je supprime tout le code C11 et juste remplacer les clôtures par

asm volatile ("sfence" ::: "memory"); 

et

asm volatile ("lfence" ::: "memory"); 

(Ma compréhension de ces macro est: clôture du compilateur pour éviter que la mémoire accès à réorganiser/optimiser + clôture matérielle)

mes variables doivent-elles être déclarées comme volatiles par exemple?

J'ai déjà vu ce code tampon en anneau ci-dessus avec seulement ces clôtures ASM mais sans types atomiques et j'ai été vraiment surpris, je veux savoir si ce code était correct.

Répondre

1

Je viens de répondre en ce qui concerne les atomes C11, les spécificités de la plate-forme sont trop compliquées et devraient être éliminées.

La synchronisation entre threads dans C11 est uniquement garantie par certains appels système (par exemple mtx_t) et atomiques. N'essayez même pas de le faire sans. Cela dit, la synchronisation fonctionne via atomics, c'est à dire la visibilité des effets secondaires est garanti pour se propager via la visibilité des effets sur les atomiques. E.g pour le modèle de cohérence le plus simple, séquentiel, chaque fois que le thread T2 voit une modification que le thread T1 a effectuée sur une variable atomique A, tous les effets secondaires avant cette modification dans le thread T1 sont visibles pour T2.

Toutes vos variables partagées ne doivent donc pas nécessairement être atomiques, vous devez seulement vous assurer que votre état est correctement propagé via un atome. En ce sens, les clôtures ne vous achètent rien lorsque vous utilisez une cohérence séquentielle ou d'acquisition-libération, elles ne font que compliquer l'image.

Remarques générales: Un peu plus

  • Puisque vous semblez utiliser le modèle de cohérence séquentielle, ce qui est la valeur par défaut , l'écriture fonctionnelle des opérations atomiques (par exemple atomic_load) est superflue. Juste l'évaluation de la variable atomique est exactement les mêmes.
  • J'ai l'impression que vous essayez beaucoup trop d'optimisation au début de votre développement. Je pense que vous devriez faire une mise en œuvre pour laquelle vous pouvez prouver l'exactitude, d'abord. Ensuite, si et seulement si vous remarquez un problème de performance, vous devriez commencer à penser à l'optimisation . Il est très improbable qu'une telle structure de données atomique soit un véritable goulot d'étranglement pour votre applcation. Vous auriez besoin d'un très grand nombre de threads qui tous simultanément marteler sur votre pauvre variable atomique , pour voir un goulot d'étranglement mesurable ici.
+0

Merci beaucoup! Si j'évite les clôtures, je dois utiliser atomic_store_explicit avec l'ordre de mémoire voulu? Ou je voudrais juste écrire: queue-> w_i = next_w_i (c'est l'ordre relaxé utilisé par défaut?) Dans quel cas dois-je utiliser des clôtures? – treywelsh

+0

Non, l'ordre par défaut est la cohérence séquentielle, la plus forte possible. –