Dans ce POC (Proof of Concept), nous explorerons comment le langage Rust traite les conditions de course, en le comparant avec le C , un langage largement utilisé, mais avec moins de garanties de sécurité pour la concurrence.
Sécurité des threads : course aux données de C à Rust
En informatique, les threads sont utilisés pour diviser les tâches logicielles en sous-tâches pouvant être exécutées simultanément. En utilisant des threads, nous gagnons du temps de traitement et utilisons mieux les ressources de la machine, mais cette concurrence apporte des défis, tels que les conditions de concurrence, qui peuvent générer de graves incohérences dans les données.
Threads sont des unités d'exécution qui vous permettent de traiter des tâches simultanément. Nous pouvons considérer les threads comme des flux d'exécution indépendants au sein d'un programme, illustrés dans l'image ci-dessous :
Bien que les threads apportent des avantages en termes de performances, ils introduisent des risques, en particulier lors de l'accès à des ressources partagées.
De plus, les threads peuvent être utilisés pour implémenter le parallélisme, dans lequel plusieurs tâches sont exécutées simultanément sur différents cœurs de processeur. Cela permet au programme de mieux utiliser le matériel disponible, accélérant ainsi l'exécution de tâches indépendantes.
Créons un système simple en C :
int saldo = 1000; void creditar(int valor) { int tmp_saldo = saldo; sleep(1); // Delay simulado saldo += tmp_saldo + valor; } void debitar(int valor) { int temp = saldo; sleep(1); // Delay simulado if (temp >= valor) { saldo = temp - valor; } } void* processar_transacao(void* arg) { int valor = *(int*)arg; if (valor > 0) { creditar(valor); } else { debitar(abs(valor)); } return NULL; } int main() { int transactions[] = {100, -50, 200, -150, 300, -200, 150, -100, 50, -50}; int num_transactions = sizeof(transactions) / sizeof(transactions[0]); pthread_t threads[num_transactions]; for (int i = 0; i < num_transactions; i++) { pthread_create(&threads[i], NULL, processar_transacao, &transactions[i]); // Cria uma thread para cada transação } for (int i = 0; i < num_transactions; i++) { pthread_join(threads[i], NULL); // Aguarda todas as threads terminarem } printf("Saldo final da conta: %d\n", saldo); return 0; }
Lorsque nous optons pour un environnement avec traitement multithreading ce que nous appelons des conditions de concurrence peuvent se produire, lorsque 2 threads accèdent et modifient la même valeur, nous avons une condition de concurrence. Ce problème se produit car la synchronisation de la valeur accédée dans chaque thread n'est pas garantie en raison de la concurrence entre les appels.
Lors de l'exécution de ce code plusieurs fois, le solde final varie, à mesure que les threads accèdent et modifient le solde simultanément.
int saldo = 1000; void creditar(int valor) { int tmp_saldo = saldo; sleep(1); // Delay simulado saldo += tmp_saldo + valor; } void debitar(int valor) { int temp = saldo; sleep(1); // Delay simulado if (temp >= valor) { saldo = temp - valor; } } void* processar_transacao(void* arg) { int valor = *(int*)arg; if (valor > 0) { creditar(valor); } else { debitar(abs(valor)); } return NULL; } int main() { int transactions[] = {100, -50, 200, -150, 300, -200, 150, -100, 50, -50}; int num_transactions = sizeof(transactions) / sizeof(transactions[0]); pthread_t threads[num_transactions]; for (int i = 0; i < num_transactions; i++) { pthread_create(&threads[i], NULL, processar_transacao, &transactions[i]); // Cria uma thread para cada transação } for (int i = 0; i < num_transactions; i++) { pthread_join(threads[i], NULL); // Aguarda todas as threads terminarem } printf("Saldo final da conta: %d\n", saldo); return 0; }
Mutex est une primitive de synchronisation qui garantit qu'un seul thread à la fois a accès à une ressource partagée. L'acronyme mutex vient du terme anglais mutual exclusion, qui signifie « exclusion mutuelle ».
Lorsqu'un thread acquiert un mutex, tout autre thread tentant d'acquérir le même mutex est suspendu jusqu'à ce que le premier thread libère le mutex. Cela empêche deux ou plusieurs processus (threads) d’avoir un accès simultané à la ressource partagée.
int saldo = 1000; pthread_mutex_t saldo_mutex; // Mutex para proteger o saldo void creditar(int valor) { pthread_mutex_lock(&saldo_mutex); // Bloqueia o mutex int tmp_saldo = saldo; sleep(1); // Delay simulado saldo = tmp_saldo + valor; pthread_mutex_unlock(&saldo_mutex); // Libera o mutex } void debitar(int valor) { pthread_mutex_lock(&saldo_mutex); // Bloqueia o mutex int tmp_saldo = saldo; sleep(1); // Delay simulado if (tmp_saldo >= valor) { saldo = tmp_saldo - valor; } pthread_mutex_unlock(&saldo_mutex); // Libera o mutex }
Penser Rust comme un langage absent de la course aux données n'est pas productif, mais nous pouvons comprendre comment les structs et son compilateur contribuent en apportant d'excellentes fonctionnalités pour la mémoire et la sécurité des threads.
Rust traite les conditions de concurrence avec des garanties au moment de la compilation, en utilisant des fonctionnalités telles que la propriété, l'emprunt et des structures sécurisées pour la concurrence :
Sans utilisation de structures Arc et Mutex
Rust’s rich type system and ownership model guarantee memory-safety and thread-safety — enabling you to eliminate many classes of bugs at compile-time.
Rust ne permet pas l'accès direct aux données mutables (solde) à partir de plusieurs threads sans protection.
Le compilateur générera une erreur car le solde est déplacé vers plusieurs threads (handle1 et handle2) sans mécanisme sécurisé.
Le message d'erreur qui s'affichera est :
fn main() { let mut saldo = 1000; // saldo mutável, mas sem proteção let handle1 = thread::spawn(move || { saldo += 100; // erro: `saldo` é movido para esta thread sem proteção }); let handle2 = thread::spawn(move || { saldo -= 50; // erro: `saldo` é movido para esta thread sem proteção }); handle1.join().unwrap(); handle2.join().unwrap(); }
Grâce à Mutex et Arc, nous avons pu compiler et exécuter notre code, en résolvant les problèmes de condition de concurrence.
error[E0382]: use of moved value: `saldo`
Mutex et RwLock sont utilisés pour gérer les conditions de course, chacune avec des avantages spécifiques :
Mutex : garantit un accès exclusif à une ressource pour un thread, bloquant l'accès aux autres jusqu'à sa libération. C'est simple et efficace, mais même les lectures bloquent la ressource, la rendant moins efficace dans les scénarios de lecture lourde.
RwLock : autorise plusieurs lectures simultanées avec .read() et restreint l'écriture exclusive avec .write(). Il est Idéal pour les scénarios avec une prédominance de lectures, car il améliore les performances en permettant le parallélisme dans les opérations de lecture.
La comparaison entre C et Rust met en évidence différentes approches pour résoudre les conditions de course. Alors que C nécessite une attention particulière pour éviter les erreurs de condition de concurrence, Rust réduit ces risques au moment de la compilation, grâce à des outils tels que Mutex, RwLock et Arc en plus du modèle de propriété. Cela rend non seulement le code plus sécurisé, mais également réduit la charge mentale du programmeur en évitant les bugs silencieux.
En résumé, Rust se positionne comme un excellent choix pour développer des systèmes concurrents, offrant sécurité et fiabilité.
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!