Αρρωστημένη κλωστή. Δημιουργία και αναμονή για την εκτέλεση νημάτων. Συστήματα απλού και πολλαπλού επεξεργαστή

Τι κοινό έχουν ένα μπλουζάκι και ένα πρόγραμμα υπολογιστή; Αποτελούνται και τα δύο από πολλά νήματα! Ενώ οι κλωστές σε ένα μπλουζάκι συγκρατούν το ύφασμα μαζί, το νήμα C (κυριολεκτικά, «κλωστές» ή «κλωστές») του λειτουργικού συστήματος συνδέει όλα τα προγράμματα προκειμένου να εκτελούνται διαδοχικές ή παράλληλες ενέργειες ταυτόχρονα. Κάθε νήμα σε ένα πρόγραμμα προσδιορίζει μια διεργασία που εκτελείται όταν το σύστημα (σύστημα νήμα C) το ζητήσει. Αυτό βελτιστοποιεί τη λειτουργία μιας τόσο περίπλοκης συσκευής όπως ένας προσωπικός υπολογιστής και έχει θετική επίδραση στην ταχύτητα και την απόδοσή της.

Ορισμός

Στην επιστήμη των υπολογιστών, το C, ένα νήμα ή νήμα εκτέλεσης, είναι η μικρότερη ακολουθία εντολών που ελέγχονται από έναν ανεξάρτητο προγραμματιστή, ο οποίος είναι συνήθως μέρος του λειτουργικού συστήματος.

Τα νήματα έχουν συνήθως συγκεκριμένη προτεραιότητα, πράγμα που σημαίνει ότι ορισμένα νήματα έχουν προτεραιότητα έναντι άλλων. Μόλις ο επεξεργαστής ολοκληρώσει την επεξεργασία ενός νήματος, μπορεί να ξεκινήσει το επόμενο που περιμένει στην ουρά. Συνήθως, η αναμονή δεν υπερβαίνει τα λίγα χιλιοστά του δευτερολέπτου. Τα προγράμματα υπολογιστών που εφαρμόζουν «πολλαπλών νημάτων» μπορούν να εκτελέσουν πολλαπλά νήματα ταυτόχρονα. Τα περισσότερα σύγχρονα λειτουργικά συστήματα υποστηρίζουν το C Thread σε επίπεδο συστήματος. Αυτό σημαίνει ότι όταν ένα πρόγραμμα προσπαθεί να καταλάβει όλους τους πόρους της CPU, το σύστημα θα μεταβεί αναγκαστικά σε άλλα προγράμματα και θα αναγκάσει το πρόγραμμα υποστήριξης της CPU να μοιραστεί ομοιόμορφα τους πόρους.

Ο όρος "νήμα" (C Thread) μπορεί επίσης να αναφέρεται σε μια σειρά σχετικών αναρτήσεων σε μια διαδικτυακή συζήτηση. Οι πίνακες μηνυμάτων Ιστού αποτελούνται από πολλά θέματα ή νήματα. Οι απαντήσεις που δημοσιεύτηκαν ως απάντηση στην αρχική δημοσίευση αποτελούν μέρος του ίδιου νήματος. Στο ηλεκτρονικό ταχυδρομείο, ένα νήμα μπορεί να αναφέρεται σε μια σειρά από απαντήσεις με τη μορφή εντολών εμπρός και πίσω που σχετίζονται με ένα συγκεκριμένο μήνυμα και να δομεί το δέντρο συνομιλίας.

C Multithreading στα Windows

Στον προγραμματισμό υπολογιστών, το single threading είναι η επεξεργασία μιας εντολής κάθε φορά. Το αντίθετο του single threading είναι το multithreading. Και οι δύο όροι χρησιμοποιούνται ευρέως στην κοινότητα λειτουργικού προγραμματισμού.

Το Multithreading είναι παρόμοιο με το multitasking, αλλά σας επιτρέπει να επεξεργάζεστε πολλά νήματα ταυτόχρονα, αλλά όχι πολλές διεργασίες. Επειδή τα νήματα είναι μικρότερα και ελέγχονται από απλούστερες οδηγίες, η πολυνηματική μπορεί επίσης να προκύψει εντός των διεργασιών.

Παραδείγματα του εργαλείου εργασιών C Thread

Ένα λειτουργικό σύστημα πολλαπλών νημάτων μπορεί να εκτελεί ταυτόχρονα πολλές εργασίες στο παρασκήνιο, όπως καταγραφή αλλαγών αρχείων, δημιουργία ευρετηρίου δεδομένων και διαχείριση παραθύρων. Τα προγράμματα περιήγησης Ιστού που υποστηρίζουν πολλαπλές νήματα μπορούν να ανοίξουν πολλά παράθυρα με JavaScript και Flash να εκτελούνται ταυτόχρονα. Εάν το πρόγραμμα είναι πλήρως πολλαπλών νημάτων, οι διαφορετικές διεργασίες δεν θα πρέπει να επηρεάζουν η μία την άλλη, εφόσον ο επεξεργαστής έχει αρκετή ισχύ για να τις χειριστεί.

Όπως το multitasking, το multithreading βελτιώνει επίσης τη σταθερότητα του προγράμματος. Το Multithreading μπορεί να αποτρέψει τη συντριβή ενός προγράμματος και να αποτρέψει τη διακοπή λειτουργίας του υπολογιστή σας. Δεδομένου ότι κάθε νήμα επεξεργάζεται ξεχωριστά, ένα σφάλμα σε ένα από αυτά δεν μπορεί να διαταράξει τη λειτουργία του υπολογιστή. Έτσι, το multithreading μπορεί να οδηγήσει σε λιγότερα σφάλματα στο λειτουργικό σύστημα στο σύνολό του.

Multitasking

Το Multitasking επεξεργάζεται πολλές εργασίες παράλληλα και χαρακτηρίζει επίσης τις αρχές λειτουργίας του υπολογιστή. Ο επεξεργαστής μπορεί να χειριστεί πολλές διεργασίες ταυτόχρονα με απόλυτη ακρίβεια. Ωστόσο, επεξεργάζεται μόνο οδηγίες που του αποστέλλονται από το λογισμικό. Επομένως, για να εκμεταλλευτεί πλήρως τις δυνατότητες της CPU, το λογισμικό πρέπει να μπορεί να χειρίζεται περισσότερες από μία εργασίες ταυτόχρονα και επίσης να μπορεί να κάνει πολλαπλές εργασίες.

Ιστορική αναδρομή

Τα πρώιμα λειτουργικά συστήματα μπορούσαν να εκτελούν πολλαπλά προγράμματα ταυτόχρονα, αλλά δεν υποστήριζαν πλήρως το multitasking. Ένα πρόγραμμα θα μπορούσε να καταναλώσει όλους τους πόρους της CPU κατά την εκτέλεση μιας συγκεκριμένης λειτουργίας. Οι βασικές εργασίες του λειτουργικού συστήματος, όπως η αντιγραφή αρχείων, εμπόδισαν τον χρήστη να εκτελέσει άλλες εργασίες (π.χ. άνοιγμα ή κλείσιμο παραθύρων).

Τα σύγχρονα λειτουργικά συστήματα περιλαμβάνουν πλήρη υποστήριξη για πολλαπλές εργασίες — πολλές λύσεις λογισμικού μπορούν να λειτουργούν ταυτόχρονα χωρίς να παρεμβαίνουν η μία στη λειτουργικότητα της άλλης.

Το multitasking βελτιώνει επίσης τη σταθερότητα του υπολογιστή. Για παράδειγμα, εάν μια από τις διεργασίες διακοπεί, δεν θα επηρεάσει άλλα προγράμματα που εκτελούνται, επειδή ο υπολογιστής χειρίζεται κάθε διαδικασία ξεχωριστά. Αυτό μπορεί να συγκριθεί με τη διαδικασία σύνταξης μιας επιστολής: εάν βρίσκεστε στη μέση ενός φύλλου χαρτιού και έχετε ήδη γράψει μέρος του κειμένου, αλλά το πρόγραμμα περιήγησής σας σταματά απροσδόκητα, τότε δεν θα χάσετε την εργασία που έχετε ήδη ολοκληρώσει .

Συστήματα ενός και πολλαπλών επεξεργαστών

Η εφαρμογή των τεχνολογιών νήματος και επεξεργαστή διαφέρει ανάλογα με το λειτουργικό σύστημα, αλλά τις περισσότερες φορές ένα νήμα είναι συστατικό μιας διαδικασίας. Πολλά νήματα μπορούν να υπάρχουν ταυτόχρονα σε μια ενιαία διαδικασία, εκτελώντας και μοιράζοντας πόρους. Συγκεκριμένα, τα νήματα διαδικασίας C Thread μοιράζονται τον εκτελέσιμο κώδικα και τις μεταβλητές τιμές ανά πάσα στιγμή.

Τα συστήματα ενός επεξεργαστή εφαρμόζουν πολυνηματική βασισμένη στον χρόνο: η κεντρική μονάδα επεξεργασίας (CPU) αλλάζει μεταξύ διαφορετικών νημάτων λογισμικού. Σε ένα σύστημα πολλαπλών επεξεργαστών, καθώς και σε ένα σύστημα πολλαπλών πυρήνων, ένας αριθμός νημάτων εκτελούνται παράλληλα, με κάθε επεξεργαστή ή πυρήνα να εκτελεί ένα ξεχωριστό νήμα ταυτόχρονα.

Τύποι ρεμάτων

Οι προγραμματιστές διεργασιών των περισσότερων σύγχρονων λειτουργικών συστημάτων υποστηρίζουν άμεσα τόσο τη μεταβατική όσο και την πολυεπεξεργαστή, ενώ ο πυρήνας του λειτουργικού συστήματος επιτρέπει στους προγραμματιστές να ελέγχουν τα νήματα παρέχοντας τις επιθυμητές λειτουργίες μέσω μιας διεπαφής κλήσης συστήματος. Ορισμένες υλοποιήσεις νημάτων ονομάζονται νήματα πυρήνα, ενώ οι διεργασίες ελαφρού βάρους (LWP) είναι ένας τύπος νήματος που μοιράζονται την ίδια κατάσταση πληροφοριών. Επίσης, οι λύσεις λογισμικού μπορούν να έχουν νήματα χώρου χρήστη όταν χρησιμοποιούνται με χρονόμετρα (Thread timer C), σήματα ή άλλες μεθόδους για να διακόπτουν τη δική τους εκτέλεση, εκτελώντας ένα είδος ad hoc χρονισμού.

Νήματα και διαδικασίες: διαφορές

Τα νήματα διαφέρουν από τις κλασικές διεργασίες λειτουργικού συστήματος πολλαπλών εργασιών στα ακόλουθα χαρακτηριστικά:

    Οι διεργασίες είναι συνήθως ανεξάρτητες, ενώ τα νήματα υπάρχουν ως υποσύνολα μιας διεργασίας.

    Οι διαδικασίες μεταφέρουν πολύ περισσότερες πληροφορίες από τα νήματα.

    Οι διαδικασίες έχουν αποκλειστικούς χώρους διευθύνσεων.

    Οι διαδικασίες αλληλεπιδρούν μόνο μέσω μηχανισμών επικοινωνίας του συστήματος.

    η εναλλαγή περιβάλλοντος μεταξύ νημάτων σε μια διεργασία πραγματοποιείται πιο γρήγορα από την εναλλαγή περιβάλλοντος μεταξύ διεργασιών.

Προληπτικός και συνεργατικός σχεδιασμός

Στα λειτουργικά συστήματα πολλών χρηστών, η προληπτική πολυνηματική προσέγγιση είναι μια πιο ευρέως χρησιμοποιούμενη προσέγγιση για τον έλεγχο του χρόνου εκτέλεσης μέσω της εναλλαγής περιβάλλοντος. Ωστόσο, ο προληπτικός σχεδιασμός μπορεί να οδηγήσει σε ανεξέλεγκτη ιεράρχηση προτεραιοτήτων και αποτυχίες. Αντίθετα, η συνεργασία πολλαπλών νημάτων βασίζεται σε νήματα για να εγκαταλείψει τον έλεγχο εκτέλεσης. Αυτό μπορεί να δημιουργήσει προβλήματα εάν ένα κοινό νήμα πολλαπλών εργασιών αποκλειστεί αναμένοντας έναν πόρο.

ΕΞΕΛΙΞΗ ΤΗΣ ΤΕΧΝΟΛΟΓΙΑΣ

Μέχρι τις αρχές της δεκαετίας του 2000. Οι περισσότεροι επιτραπέζιοι υπολογιστές είχαν μόνο έναν επεξεργαστή μονού πυρήνα, ο οποίος δεν υποστήριζε νήματα υλικού. Το 2002, η Intel εισήγαγε την υποστήριξη για ταυτόχρονη multithreading στον επεξεργαστή Pentium 4, η οποία ονομάζεται Hyper-Threading. Το 2005 παρουσιάστηκε το διπύρηνο και διπύρηνο AMD Athlon 64 X2.

Οι επεξεργαστές σε ολοκληρωμένα συστήματα με υψηλότερες απαιτήσεις σε πραγματικό χρόνο έχουν τη δυνατότητα πολλαπλών νημάτων, μειώνοντας τον χρόνο μεταγωγής νημάτων και χρησιμοποιούν ένα αποκλειστικό αρχείο μητρώου για κάθε νήμα.

Μοντέλα

Ας απαριθμήσουμε τα κύρια μοντέλα υλοποίησης.

1:1 (νήμα σε επίπεδο πυρήνα) - Τα νήματα που δημιουργούνται από τον χρήστη στον πυρήνα είναι η απλούστερη δυνατή υλοποίηση νημάτων. Το OS/2 και το Win32 χρησιμοποιούν αυτήν την προσέγγιση εγγενώς, ενώ το Linux Thread join υλοποιεί αυτήν την προσέγγιση μέσω NPTL ή παλαιότερων LinuxThreads. Αυτή η προσέγγιση χρησιμοποιείται επίσης από τα Solaris, NetBSD, FreeBSD, macOS και iOS.

N: 1 (νήμα χρήστη) - Αυτό το μοντέλο απαιτεί όλα τα νήματα σε επίπεδο εφαρμογής να αντιστοιχίζονται σε ένα μεμονωμένο προγραμματισμένο αντικείμενο σε επίπεδο πυρήνα. Με αυτήν την προσέγγιση, η εναλλαγή περιβάλλοντος μπορεί να γίνει πολύ γρήγορα, και επιπλέον, μπορεί να εφαρμοστεί ακόμη και σε πυρήνες που δεν υποστηρίζουν threading. Ωστόσο, ένα από τα κύρια μειονεκτήματα είναι ότι δεν επωφελείται από την επιτάχυνση υλικού σε επεξεργαστές πολλαπλών νημάτων ή υπολογιστές. Για παράδειγμα: εάν ένα από τα νήματα πρέπει να εκτελεστεί όταν γίνεται ένα αίτημα εισόδου/εξόδου, ολόκληρη η διαδικασία μπλοκάρεται και δεν μπορεί να χρησιμοποιηθεί το threading. Στο GNU Portable C, η εξαίρεση νήματος χρησιμοποιείται ως νήμα σε επίπεδο χρήστη.

M:N (υβριδική υλοποίηση) - το μοντέλο αντιστοιχίζει έναν αριθμό νημάτων εφαρμογής σε κάποιο αριθμό N κελιών πυρήνα ή «εικονικούς επεξεργαστές». Αυτός είναι ένας συμβιβασμός μεταξύ των νημάτων σε επίπεδο πυρήνα ("1:1") και σε επίπεδο χρήστη ("N:1"). Τα συστήματα ροής M:N είναι πιο περίπλοκα, απαιτώντας αλλαγές τόσο στον πυρήνα όσο και στον κώδικα χρήστη. Σε μια υλοποίηση M:N, η βιβλιοθήκη επεξεργασίας νημάτων είναι υπεύθυνη για τον προγραμματισμό νημάτων σε διαθέσιμες προγραμματιζόμενες οντότητες. Αυτό καθιστά το περιβάλλον βέλτιστο επειδή αποφεύγει τις κλήσεις συστήματος. Ωστόσο, αυτό αυξάνει την πολυπλοκότητα και την πιθανότητα αντιστροφών, καθώς και τον μη βέλτιστο προγραμματισμό χωρίς εκτεταμένο (και ακριβό) συντονισμό μεταξύ του προγραμματιστή περιβάλλοντος χρήστη και του προγραμματιστή πυρήνα.

Παραδείγματα υβριδικής υλοποίησης είναι η ενεργοποίηση χρονοπρογραμματιστή που χρησιμοποιείται από την εγγενή υλοποίηση βιβλιοθήκης POSIX του NetBSD (για το μοντέλο M:N, σε αντίθεση με το μοντέλο υλοποίησης πυρήνα 1:1 ή το μοντέλο χώρου χρήστη).

Ελαφριές διεργασίες που χρησιμοποιούνται από παλαιότερες εκδόσεις του λειτουργικού συστήματος Solaris (εργαλειοθήκη Std Thread C).

Υποστήριξη γλώσσας προγραμματισμού

Πολλά επίσημα συστήματα υποστηρίζουν τη λειτουργία νήματος. Οι υλοποιήσεις C και C++ εφαρμόζουν αυτήν την τεχνολογία και παρέχουν πρόσβαση σε εγγενή API για το λειτουργικό σύστημα. Ορισμένες γλώσσες προγραμματισμού υψηλότερου επιπέδου, όπως η Java, η Python και το .NET Framework, εκθέτουν τα νήματα στους προγραμματιστές αφαιρώντας συγκεκριμένες διαφορές στην υλοποίηση των νημάτων στο χρόνο εκτέλεσης. Άλλες επεκτάσεις γλώσσας προσπαθούν επίσης να αφαιρέσουν την έννοια της ταυτόχρονης χρήσης και του νήματος από τον προγραμματιστή. Ορισμένες γλώσσες έχουν σχεδιαστεί για διαδοχικό παραλληλισμό χρησιμοποιώντας GPU.

Ορισμένες γλώσσες διερμηνείας έχουν υλοποιήσεις που υποστηρίζουν τη νηματοποίηση και την παράλληλη επεξεργασία, αλλά όχι την παράλληλη εκτέλεση νημάτων λόγω του καθολικού κλειδώματος διερμηνέα (GIL). Το GIL είναι ένα κλείδωμα mutex που εκτελείται από τον διερμηνέα και μπορεί να αποτρέψει την ταυτόχρονη ερμηνεία του κώδικα εφαρμογής σε δύο ή περισσότερα νήματα ταυτόχρονα, περιορίζοντας την ταυτόχρονη λειτουργία σε συστήματα πολλαπλών πυρήνων.

Άλλες υλοποιήσεις προγραμματισμού, όπως το Tcl, χρησιμοποιούν την επέκταση ύπνου Thread C Αυτό αποφεύγει το μέγιστο όριο GIL χρησιμοποιώντας ένα μοντέλο όπου το περιεχόμενο και ο κώδικας πρέπει να "μοιράζονται" ρητά μεταξύ των νημάτων.

Οι γλώσσες προγραμματισμού εφαρμογών που βασίζονται σε συμβάντα, όπως η Verilog και η επέκταση ύπνου Thread C έχουν διαφορετικό μοντέλο νήματος που υποστηρίζει τον μέγιστο αριθμό νημάτων για προσομοίωση υλικού.

Πρακτική πολυνηματική

Οι βιβλιοθήκες πολλαπλών νημάτων ξεκινούν μια κλήση συνάρτησης για να δημιουργήσουν ένα νέο νήμα, το οποίο παίρνει την τιμή της συνάρτησης ως παράμετρο. Στη συνέχεια δημιουργείται ένα νέο παράλληλο νήμα και η συνάρτηση που εκτελείται υποβάλλεται σε επεξεργασία και στη συνέχεια επιστρέφεται. Οι γλώσσες προγραμματισμού περιέχουν βιβλιοθήκες νημάτων που περιλαμβάνουν συναρτήσεις καθολικού συγχρονισμού που σας επιτρέπουν να δημιουργήσετε και να εφαρμόσετε επιτυχώς πολλαπλές νήματα χωρίς σφάλματα χρησιμοποιώντας mutexes, μεταβλητές συνθήκης, κρίσιμες ενότητες, οθόνες και άλλους τύπους συγχρονισμού.

Ετικέτες: pthread, pthread_create, pthread_join, EINVAL, ESRCH, EDEADLK, EDEADLOCK, EAGAIN, EPERM, PTHREAD_THREADS_MAX, μεταβίβαση ορισμάτων σε ένα νήμα, επιστροφή ορισμάτων από ένα νήμα, pthread_create arrors, pthread_inthread διαβάζει, αναγνωριστικό νήματος, threads παράδειγμα.

Δημιουργία και αναμονή για ένα νήμα

Ας δούμε ένα απλό παράδειγμα

#περιλαμβάνω #περιλαμβάνω #περιλαμβάνω #περιλαμβάνω #define ERROR_CREATE_THREAD -11 #define ERROR_JOIN_THREAD -12 #define SUCCESS 0 void* helloWorld(void *args) ( printf("Hello from thread!\n"); return SUCCESS; ) int main() ( pthread;t;tthread; int status_addr; status = pthread_create(&thread, NULL, helloWorld, NULL if (status != 0) ("κύριο σφάλμα: δεν μπορώ να δημιουργήσω νήμα, κατάσταση = %d\n", έξοδος (ERROR_CREATE_THREAD); ); = %d\n", status); exit(ERROR_JOIN_THREAD); ) printf("joined with address %d\n", status_addr); _getch(); return 0; )

Σε αυτό το παράδειγμα, μέσα στο κύριο νήμα στο οποίο εκτελείται η κύρια συνάρτηση, δημιουργείται ένα νέο νήμα, εντός του οποίου καλείται η συνάρτηση helloWorld. Η λειτουργία helloWorld εμφανίζει έναν χαιρετισμό. Ένας χαιρετισμός τυπώνεται επίσης μέσα στο κύριο νήμα. Στη συνέχεια, τα ρέματα συνδυάζονται.

Ένα νέο νήμα δημιουργείται χρησιμοποιώντας τη συνάρτηση pthread_create

Int pthread_create(*ptherad_t, const pthread_attr_t *attr, void* (*start_routine)(void*), void *arg);

Η συνάρτηση λαμβάνει ως ορίσματα έναν δείκτη σε ένα νήμα, μια μεταβλητή τύπου pthread_t, στην οποία, εάν ολοκληρωθεί με επιτυχία, αποθηκεύει το αναγνωριστικό νήματος. pthread_attr_t – χαρακτηριστικά νήματος. Εάν χρησιμοποιούνται προεπιλεγμένα χαρακτηριστικά, μπορείτε να μεταβιβάσετε το NULL. start_routin είναι η συνάρτηση που θα εκτελεστεί στο νέο νήμα. arg είναι τα ορίσματα που θα περάσουν στη συνάρτηση.

Ένα νήμα μπορεί να κάνει πολλά διαφορετικά πράγματα και να λάβει πολλά διαφορετικά επιχειρήματα. Για να γίνει αυτό, η συνάρτηση που θα ξεκινήσει σε ένα νέο νήμα παίρνει ένα όρισμα τύπου void*. Λόγω αυτού, μπορείτε να τυλίξετε όλα τα επιχειρήματα που έχουν περάσει σε μια δομή. Μπορείτε επίσης να επιστρέψετε μια τιμή μέσω ενός περασμένου ορίσματος.

Εάν είναι επιτυχής, η συνάρτηση επιστρέφει 0. Εάν παρουσιαστούν σφάλματα, ενδέχεται να επιστραφούν οι ακόλουθες τιμές

  • ΠΑΛΙ– το σύστημα δεν έχει τους πόρους για να δημιουργήσει ένα νέο νήμα ή το σύστημα δεν μπορεί να δημιουργήσει άλλα νήματα επειδή ο αριθμός των νημάτων έχει υπερβεί την τιμή PTHREAD_THREADS_MAX (για παράδειγμα, σε ένα από τα μηχανήματα που χρησιμοποιούνται για δοκιμή, αυτός ο μαγικός αριθμός είναι 2019 )
  • EINVAL– λανθασμένα χαρακτηριστικά ροής (μεταβιβάζονται ως όρισμα attr)
  • EPERM– Το νήμα κλήσης δεν έχει τα κατάλληλα δικαιώματα για να ορίσει τις επιθυμητές παραμέτρους ή πολιτικές προγραμματιστή.

Ας περάσουμε από το πρόγραμμα

#define ERROR_CREATE_THREAD -11 #define ERROR_JOIN_THREAD -12 #define SUCCESS 0

Εδώ καθορίζουμε το σύνολο των τιμών που είναι απαραίτητες για τον χειρισμό πιθανών σφαλμάτων.

Void* helloWorld(void *args) ( printf("Hello from the thread!\n"); return SUCCESS; )

Αυτή είναι μια συνάρτηση που θα εκτελείται σε ξεχωριστό νήμα. Δεν θα λάβει επιχειρήματα. Σύμφωνα με το πρότυπο, θεωρείται ότι μια ρητή έξοδος από μια συνάρτηση καλεί τη συνάρτηση pthread_exit και η τιμή επιστροφής θα μεταβιβαστεί όταν καλείται η συνάρτηση pthread_join ως κατάσταση.

Status = pthread_create(&thread, NULL, helloWorld, NULL); if (κατάσταση != 0) ( printf("κύριο σφάλμα: δεν μπορώ να δημιουργήσω νήμα, κατάσταση = %d\n", κατάσταση); έξοδος(ERROR_CREATE_THREAD); )

Εδώ δημιουργείται ένα νέο νήμα και εκτελείται αμέσως. Η ροή δεν λαμβάνει χαρακτηριστικά ή ορίσματα. Μετά τη δημιουργία του νήματος, εμφανίζεται ένας έλεγχος σφάλματος.

Status = pthread_join(thread, (void**)&status_addr); if (κατάσταση != ΕΠΙΤΥΧΙΑ) ( printf("κύριο σφάλμα: δεν μπορώ να συμμετάσχω στο νήμα, κατάσταση = %d\n", κατάσταση); έξοδος(ERROR_JOIN_THREAD); )

Αναγκάζει το κύριο νήμα να περιμένει να ολοκληρωθεί το θυγατρικό νήμα. Λειτουργία

Int pthread_join(pthread_t νήμα, void **value_ptr);

Καθυστερεί την εκτέλεση του νήματος που καλεί (αυτή τη συνάρτηση) μέχρι να εκτελεστεί το νήμα. Όταν το pthread_join είναι επιτυχές, επιστρέφει 0. Εάν το νήμα επέστρεψε ρητά μια τιμή (αυτή είναι η ίδια τιμή SUCCESS από τη συνάρτησή μας), τότε θα τοποθετηθεί στη μεταβλητή value_ptr. Πιθανά σφάλματα που επιστράφηκαν από το pthread_join

  • EINVAL– το νήμα οδηγεί σε ένα νήμα που δεν συγχωνεύεται
  • ΕΣΡΧ– δεν υπάρχει νήμα με το ίδιο αναγνωριστικό αποθηκευμένο από τη μεταβλητή νήματος
  • ΕΔΕΑΔΛΚ– εντοπίστηκε αδιέξοδο (αμοιβαίος αποκλεισμός) ή το ίδιο το νήμα κλήσης καθορίστηκε ως το συγχωνευμένο νήμα.

Ένα παράδειγμα δημιουργίας νημάτων μεταβιβάζοντας ορίσματα σε αυτά

Ας υποθέσουμε ότι θέλουμε να περάσουμε δεδομένα σε μια ροή και να επιστρέψουμε κάτι πίσω. Ας πούμε ότι θα περάσουμε μια χορδή σε ένα ρεύμα και θα επιστρέψουμε το μήκος αυτής της χορδής από τη ροή.

Δεδομένου ότι μια συνάρτηση μπορεί να λάβει μόνο έναν δείκτη τύπου void, όλα τα ορίσματα θα πρέπει να συσκευάζονται σε μια δομή. Ας ορίσουμε έναν νέο τύπο δομής:

Typedef struct someArgs_tag ( int id; const char *msg; int out; ) someArgs_t;

Εδώ το id είναι το αναγνωριστικό νήματος (γενικά, δεν χρειάζεται στο παράδειγμά μας), το δεύτερο πεδίο είναι μια συμβολοσειρά και το τρίτο είναι το μήκος της συμβολοσειράς που θα επιστρέψουμε.

Μέσα στη συνάρτηση, μετατρέπουμε το όρισμα στον επιθυμητό τύπο, εκτυπώνουμε τη συμβολοσειρά και εισάγουμε το υπολογισμένο μήκος της συμβολοσειράς πίσω στη δομή.

Void* helloWorld(void *args) ( someArgs_t *arg = (someArgs_t*) args; int len; if (arg->msg == NULL) ( return BAD_MESSAGE; ) len = strlen(arg->msg); printf(" %s\n", arg->msg); arg->out = len; επιστροφή SUCCESS; )

Αν όλα πήγαν καλά, τότε επιστρέφουμε την τιμή SUCCESS ως κατάσταση και αν έγινε κάποιο σφάλμα (στην περίπτωσή μας, αν περάσαμε μηδενική συμβολοσειρά), τότε βγαίνουμε με την κατάσταση BAD_MESSAGE.

Σε αυτό το παράδειγμα θα δημιουργήσουμε 4 νήματα. Για 4 νήματα, θα χρειαστούμε έναν πίνακα τύπου pthread_t μήκους 4, έναν πίνακα από περασμένα ορίσματα και 4 συμβολοσειρές, που θα περάσουμε.

Pthread_t νήματα; int status? int i? int status_addr; someArgs_t args; const char *messages = ( "First", NULL, "Third Message", "Fourth Message" );

Πρώτα απ 'όλα, συμπληρώνουμε τις τιμές των ορισμάτων.

Για (i = 0; i< NUM_THREADS; i++) { args[i].id = i; args[i].msg = messages[i]; }

Για (i = 0; i< NUM_THREADS; i++) { status = pthread_create(&threads[i], NULL, helloWorld, (void*) &args[i]); if (status != 0) { printf("main error: can"t create thread, status = %d\n", status); exit(ERROR_CREATE_THREAD); } }

Στη συνέχεια περιμένουμε την ολοκλήρωση

Για (i = 0; i< NUM_THREADS; i++) { status = pthread_join(threads[i], (void**)&status_addr); if (status != SUCCESS) { printf("main error: can"t join thread, status = %d\n", status); exit(ERROR_JOIN_THREAD); } printf("joined with address %d\n", status_addr); }

Τέλος, εξάγουμε επίσης τα ορίσματα, τα οποία τώρα αποθηκεύουν τις επιστρεφόμενες τιμές. Παρατηρήστε ότι ένα από τα ορίσματα είναι "κακό" (η συμβολοσειρά είναι NULL). Εδώ είναι ο πλήρης κώδικας

#περιλαμβάνω #περιλαμβάνω #περιλαμβάνω #περιλαμβάνω #περιλαμβάνω #define ERROR_CREATE_THREAD -11 #define ERROR_JOIN_THREAD -12 #define BAD_MESSAGE -13 #define SUCCESS 0 typedef struct someArgs_tag ( int id; const char *msg; int out; ) someArgs_t; void* helloWorld(void *args) ( someArgs_t *arg = (someArgs_t*) args; int len; if (arg->msg == NULL) ( return BAD_MESSAGE; ) len = strlen(arg->msg); printf(" %s\n", arg->msg); arg->out = len; return SUCCESS; ) #define NUM_THREADS 4 int main() ( νήματα pthread_t; κατάσταση int; int i; int status_addr; someArgs_t args; const char * μηνύματα = ("First", NULL, "Third Message", "Fourth Message" για (i = 0; i< NUM_THREADS; i++) { args[i].id = i; args[i].msg = messages[i]; } for (i = 0; i < NUM_THREADS; i++) { status = pthread_create(&threads[i], NULL, helloWorld, (void*) &args[i]); if (status != 0) { printf("main error: can"t create thread, status = %d\n", status); exit(ERROR_CREATE_THREAD); } } printf("Main Message\n"); for (i = 0; i < NUM_THREADS; i++) { status = pthread_join(threads[i], (void**)&status_addr); if (status != SUCCESS) { printf("main error: can"t join thread, status = %d\n", status); exit(ERROR_JOIN_THREAD); } printf("joined with address %d\n", status_addr); } for (i = 0; i < NUM_THREADS; i++) { printf("thread %d arg.out = %d\n", i, args[i].out); } _getch(); return 0; }

Κάντε το αρκετές φορές. Σημειώστε ότι η σειρά με την οποία εκτελούνται τα νήματα δεν είναι ντετερμινιστική. Εκτελώντας το πρόγραμμα, μπορείτε να λαμβάνετε διαφορετική εντολή εκτέλεσης κάθε φορά.


Λεπτομερής περιγραφή

Τα Protothread είναι ένας τύπος ελαφρών νημάτων χωρίς στοίβα που έχουν σχεδιαστεί για συστήματα χαμηλής μνήμης, όπως ενσωματωμένα συστήματα μικροελεγκτών ή δικτυωμένοι κόμβοι αισθητήρων.

Τα Protothread παρέχουν γραμμική εκτέλεση κώδικα για συστήματα που βασίζονται σε συμβάντα και υλοποιούνται στο C. Τα Protothreads μπορούν να χρησιμοποιηθούν με ή χωρίς RTOS.

Τα Protothread παρέχουν περιβάλλον κλειδωμένο σε αναμονή πάνω από ένα σύστημα που βασίζεται σε συμβάντα χωρίς την επιβάρυνση στοίβας ενός μεμονωμένου νήματος. Ο σκοπός του threading είναι η υλοποίηση διαδοχικής εκτέλεσης κώδικα χωρίς τη χρήση πολύπλοκων μηχανών κατάστασης ή multithreading. Τα Protothread παρέχουν αποκλεισμό υπό όρους για την εκτέλεση κώδικα εντός του σώματος των συναρτήσεων C.

Το πλεονέκτημα των protothreads είναι ότι επιτυγχάνουν έναν καθαρό μηχανισμό συμβάντων, στον οποίο η γραμμική εκτέλεση του κώδικα συνάρτησης μπορεί να αποκλειστεί σύμφωνα με την επιθυμητή συνθήκη. Σε συστήματα που βασίζονται αποκλειστικά σε συμβάντα, ο αποκλεισμός πρέπει να υλοποιείται χειροκίνητα διαιρώντας τη συνάρτηση σε 2 μέρη - ένα μέρος για τον κωδικό πριν από την κλήση αποκλεισμού και ένα δεύτερο μέρος για τον κωδικό μετά την κλήση αποκλεισμού. Αυτό καθιστά δύσκολη τη χρήση δομών ελέγχου όπως η υπό όρους δήλωση if() και βρόχοι while().

Το πλεονέκτημα των protothreads σε σύγκριση με τα κανονικά νήματα είναι ότι ένα protothread δεν απαιτεί ξεχωριστή στοίβα. Σε συστήματα όπου η μνήμη είναι σπάνιος πόρος, η εκχώρηση πολλαπλών στοίβων μπορεί να οδηγήσει σε υπερβολική κατανάλωση μνήμης. Σε αντίθεση με ένα κανονικό ρεύμα, ένα protostream απαιτεί 2 έως 12 byte για να αποθηκεύσει την κατάσταση, ανάλογα με την αρχιτεκτονική που χρησιμοποιείται.

Σημειώσεις:Επειδή τα protothread δεν αποθηκεύουν το περιβάλλον στη στοίβα μεταξύ των κλήσεων αποκλεισμού, Οι τοπικές μεταβλητές δεν θα διατηρηθούν όταν ένα protothread είναι αποκλεισμένο. Αυτό σημαίνει ότι οι τοπικές μεταβλητές πρέπει να χρησιμοποιούνται με προσοχή - Εάν έχετε οποιεσδήποτε αμφιβολίες, μην χρησιμοποιείτε τοπικές μεταβλητές σε ένα protothread!

Κύρια χαρακτηριστικά:

  • Δεν υπάρχει κώδικας συνδεδεμένος με το assembler - η βιβλιοθήκη του protothread είναι γραμμένη σε καθαρό C.
  • Συναρτήσεις επιρρεπείς σε σφάλματα όπως η longjmp() δεν χρησιμοποιούνται.
  • Πολύ χαμηλή κατανάλωση μνήμης RAM - μόνο 2 byte ανά πρωτονήμα.
  • Μπορεί να χρησιμοποιηθεί τόσο από το λειτουργικό σύστημα (όπου υπάρχει multithreading) όσο και χωρίς αυτό.
  • Παρέχει αναμονή αποκλεισμού χωρίς συμμετοχή πολλαπλών νημάτων ή στοίβας.

Παραδείγματα εφαρμογών που μπορείτε να χρησιμοποιήσετε:

  • Συστήματα με περιορισμούς μνήμης.
  • Στοίβες πρωτοκόλλων που βασίζονται σε συμβάντα.
  • Πολύ μικρά ενσωματωμένα συστήματα.
  • Κόμβοι δικτύου για αισθητήρες.

Το protostreams API αποτελείται από 4 βασικές λειτουργίες. Αυτά είναι η προετοιμασία PT_INIT() , η εκτέλεση PT_BEGIN() , ο αποκλεισμός υπό τον όρο PT_WAIT_UNTIL() και η έξοδος PT_END() . Επιπλέον, για διευκόλυνση, υπάρχουν 2 ακόμη λειτουργίες: αποκλεισμός με αντίστροφη συνθήκη PT_WAIT_WHILE() και αποκλεισμός σε πρωτονήμα PT_WAIT_THREAD() .

Δείτε επίσης:Τεκμηρίωση API Protothread

Συγγραφείς

Η βιβλιοθήκη Protothread γράφτηκε από τον Adam Dunkels με την υποστήριξη του Oliver Schmidt .

Πρωτοκλωστές

Τα Protothread είναι εξαιρετικά ελαφριά, χωρίς στοίβα thread που παρέχουν ένα πλαίσιο κλειδώματος πάνω από ένα σύστημα που βασίζεται σε συμβάντα χωρίς την επιβάρυνση στοίβας κάθε νήματος. Ο σκοπός των πρωτονημάτων είναι να εφαρμόσουν μια διαδοχική ροή εκτέλεσης χωρίς μηχανές πολύπλοκων καταστάσεων ή πλήρη πολυνηματική λειτουργία. Τα Protothread παρέχουν αποκλεισμό υπό όρους για την εκτέλεση κώδικα εντός του σώματος των συναρτήσεων C.

Σε συστήματα με περιορισμένη μνήμη (όπως τα συστήματα μικροελεγκτών), η παραδοσιακή πολλαπλή νήμα οδηγεί σε υπερβολική κατανάλωση μνήμης. Στην παραδοσιακή multithreading, κάθε νήμα απαιτεί μια ξεχωριστή στοίβα, η οποία μπορεί να καταλαμβάνει μεγάλη ποσότητα μνήμης.

Το κύριο πλεονέκτημα των protothreads σε σύγκριση με τα κανονικά νήματα είναι ότι ένα protothread είναι πολύ ελαφρύ και δεν απαιτεί ξεχωριστή στοίβα. Αντίθετα, όλα τα protothread χρησιμοποιούν την ίδια στοίβα συστήματος και η εναλλαγή περιβάλλοντος γίνεται μέσω της επαναφοράς στοίβας. Αυτό είναι ένα πλεονέκτημα για συστήματα όπου η μνήμη είναι σπάνιος πόρος, επειδή η εκχώρηση πολλαπλών στοίβων σε νήματα μπορεί να οδηγήσει σε υπερβολικό κόστος μνήμης. Ένα protostream απαιτεί μόνο 2 byte ανά protostream. Επιπλέον, τα protothread υλοποιούνται σε καθαρό C και δεν απαιτούν κώδικα συναρμολόγησης συγκεκριμένης αρχιτεκτονικής.

Ένα protothread λειτουργεί μέσα σε μια μεμονωμένη συνάρτηση C και δεν μπορεί να καλύπτει άλλες συναρτήσεις. Ένα protothread μπορεί να καλέσει κανονικές συναρτήσεις C, αλλά ο αποκλεισμός δεν είναι δυνατός εντός της καλούμενης συνάρτησης. Αντί να αποκλειστεί μέσα στην καλούμενη συνάρτηση, δημιουργείται ένα ξεχωριστό πρωτονήμα για κάθε δυνητικά αποκλειστική συνάρτηση. Το πλεονέκτημα αυτής της προσέγγισης είναι ο ρητός αποκλεισμός: ο προγραμματιστής γνωρίζει ακριβώς ποιες λειτουργίες μπλοκάρουν την εκτέλεση και ποιες όχι.

Τα πρωτονήματα είναι παρόμοια με τις ασύμμετρες κορουτίνες. Η κύρια διαφορά από τις κορουτίνες είναι ότι οι κορουτίνες χρησιμοποιούν μια στοίβα για κάθε κορουτίνα, ενώ τα πρωτονήματα δεν χρησιμοποιούν ξεχωριστή στοίβα για τον εαυτό τους. Ο πιο παρόμοιος μηχανισμός με τα protothreads βρίσκεται στις γεννήτριες Python. Έχουν επίσης σχεδιασμό χωρίς στοίβα, απλώς για διαφορετικό σκοπό. Τα Protothread παρέχουν κλειδώματα περιβάλλοντος σε μια συνάρτηση C και οι γεννήτριες Python παρέχουν πολλαπλά σημεία εξόδου από μια συνάρτηση γεννήτριας.

Τοπικές μεταβλητές

Σημειώσεις:Επειδή τα protothread δεν αποθηκεύουν το περιβάλλον στη στοίβα μεταξύ των κλήσεων αποκλεισμού, οι τοπικές μεταβλητές δεν θα αποθηκευτούν όταν μπλοκάρει το protothread. Αυτό σημαίνει ότι οι τοπικές μεταβλητές πρέπει να χρησιμοποιούνται με προσοχή - εάν έχετε αμφιβολίες, μην χρησιμοποιείτε τοπικές μεταβλητές σε ένα πρωτόγραμμο!

Προγραμματισμός (προγραμματιστής εργασιών) πρωτονημάτων

Ένα protothread ελέγχεται από επαναλαμβανόμενες κλήσεις στη συνάρτηση στην οποία εκτελείται το protothread. Κάθε φορά που καλείται μια συνάρτηση, το protothread θα τρέχει μέχρι να μπλοκάρει ή να βγει. Έτσι, ο προγραμματισμός εκτελείται από μια εφαρμογή που χρησιμοποιεί protothreads.

Εκτέλεση

Τα Protothreads υλοποιούνται χρησιμοποιώντας τοπικές συνέχειες. Μια τοπική συνέχεια αντιπροσωπεύει την τρέχουσα κατάσταση εκτέλεσης σε μια συγκεκριμένη θέση του προγράμματος, αλλά δεν παρέχει ιστορικό κλήσεων ή τοπικές μεταβλητές. Μια τοπική συνέχεια μπορεί να οριστεί σε ξεχωριστή συνάρτηση για να καταγράψει την κατάσταση της συνάρτησης. Μόλις δημιουργηθεί μια τοπική συνέχεια, μπορεί να συνεχιστεί επαναφέροντας την κατάσταση της συνάρτησης στο σημείο όπου δημιουργήθηκε η τοπική συνέχεια. Σημείωση μεταφραστής: σίγουρα ακούγεται σαν ανοησία, αλλά κάτι θα γίνει σαφές αν κοιτάξετε τον κώδικα των μακροεντολών protothread και πώς χρησιμοποιούνται - για παράδειγμα, στην εφαρμογή δικτύου hello-world, η οποία είναι χτισμένη σε protothread.

Η τοπική συνέχιση μπορεί να υλοποιηθεί με διάφορους τρόπους:

  1. χρησιμοποιώντας κώδικα assembler για συγκεκριμένη αρχιτεκτονική,
  2. χρησιμοποιώντας τυπικές κατασκευές C, ή
  3. χρησιμοποιώντας επεκτάσεις μεταγλωττιστή.

Η πρώτη μέθοδος λειτουργεί με αποθήκευση και επαναφορά της κατάστασης του επεξεργαστή, εξαιρουμένων των δεικτών στοίβας και απαιτεί 16 έως 32 byte ανά πρωτονήμα. Η ακριβής ποσότητα μνήμης εξαρτάται από την αρχιτεκτονική του επεξεργαστή που χρησιμοποιείται.

Η τυπική υλοποίηση C απαιτεί μόνο 2 byte ανά protostream για την αποθήκευση της κατάστασης και χρησιμοποιεί τη δήλωση C switch() με μη προφανή τρόπο. Αυτή η υλοποίηση, ωστόσο, εισάγει έναν μικρό περιορισμό για τον κώδικα που χρησιμοποιεί πρωτονήματα - ο ίδιος ο κώδικας δεν μπορεί να χρησιμοποιήσει εντολές switch().

Ορισμένοι μεταγλωττιστές έχουν επεκτάσεις C που μπορούν να χρησιμοποιηθούν για την υλοποίηση πρωτονημάτων. Το GCC υποστηρίζει δείκτες ετικετών που μπορούν να χρησιμοποιηθούν για αυτόν τον σκοπό. Με αυτήν την υλοποίηση, τα protothread θα απαιτούσαν 4 byte μνήμης RAM ανά protothread.

Μακροεντολές

Παραδείγματα: dhcpc.c.

Παραδείγματα: dhcpc.c.

Δείτε επίσης: PT_SPAWN() Παραδείγματα: dhcpc.c.

Δείτε τον ορισμό στο αρχείο

Μονάδα μέτρησης σπείρωμαπαρουσιάστηκε για πρώτη φορά στην Python 1.5.2 ως συνέχεια της ενότητας νήματος χαμηλού επιπέδου. Μονάδα νήματοςαπλοποιεί σημαντικά την εργασία με νήματα και σας επιτρέπει να προγραμματίσετε την εκκίνηση πολλές λειτουργίες ταυτόχρονα. Σημειώστε ότι τα νήματα στην Python λειτουργούν καλύτερα με λειτουργίες εισόδου/εξόδου, όπως η λήψη πόρων από το Διαδίκτυο ή η ανάγνωση αρχείων και φακέλων στον υπολογιστή σας.

Εάν πρέπει να κάνετε κάτι που χρειάζεται εντατική CPU, τότε ίσως θέλετε να δείτε τη μονάδα πολυεπεξεργασία, αντί σπείρωμα. Ο λόγος είναι ότι η Python περιέχει ένα Global Interpreter Lock (GIL) που εκτελεί όλα τα νήματα μέσα στο κύριο νήμα. Για το λόγο αυτό, όταν χρειαστεί να εκτελέσετε κάποιες λειτουργίες έντασης νήματος, θα παρατηρήσετε ότι όλα είναι αρκετά αργά. Έτσι, θα εστιάσουμε σε ποια νήματα είναι καλύτερα: Λειτουργίες εισόδου/εξόδου.

Μια μικρή εισαγωγή

Ένα νήμα σάς επιτρέπει να εκτελέσετε ένα κομμάτι μεγάλου κώδικα σαν να ήταν ένα ξεχωριστό πρόγραμμα. Αυτό μοιάζει με την κλήση μιας κληρονομημένης διαδικασίας, εκτός από το ότι καλείτε μια συνάρτηση ή μια κλάση αντί για ένα ξεχωριστό πρόγραμμα. Πάντα έβρισκα συγκεκριμένα παραδείγματα εξαιρετικά χρήσιμα. Ας δούμε κάτι πολύ απλό:

Εισαγωγή νήματος def doubler(number): """ Μια συνάρτηση που μπορεί να χρησιμοποιηθεί από ένα νήμα """ print(threading.currentThread().getName() + "\n") print(number * 2) print() αν __name__ == "__main__": για το i στην περιοχή (5): my_thread = threading.Thread(target=doubler, args=(i,)) my_thread.start()

Εδώ εισάγουμε μονάδα σπειρώματοςκαι δημιουργήστε μια κανονική συνάρτηση που ονομάζεται διπλασιαστής. Η συνάρτησή μας παίρνει μια τιμή και τη διπλασιάζει. Εκτυπώνει επίσης το όνομα του νήματος που καλεί τη συνάρτηση και εκτυπώνει μια κενή γραμμή στο τέλος. Στη συνέχεια, στο τελευταίο μπλοκ κώδικα, δημιουργούμε πέντε νήματα και εκτελούμε το καθένα από αυτά με τη σειρά.

Χρησιμοποιώντας το multithreading μπορείτε να λύσετε πολλά προβλήματα ρουτίνας. Για παράδειγμα, ανέβασμα βίντεο ή άλλου υλικού σε κοινωνικά δίκτυα όπως το Youtube ή το Facebook. Για να αναπτύξετε το κανάλι σας στο Youtube, μπορείτε να χρησιμοποιήσετε το https://publbox.com/ru/youtube το οποίο θα αναλάβει τη διαχείριση του καναλιού σας. Το Youtube είναι μια εξαιρετική πηγή εισοδήματος και όσο περισσότερα κανάλια τόσο το καλύτερο. Δεν μπορείτε να κάνετε χωρίς το Publbox.

Σημειώστε ότι όταν ορίζουμε ένα νήμα, θέτουμε τον στόχο του στο δικό μας λειτουργία διπλασιαστή, και περνάμε επίσης ένα όρισμα στη συνάρτηση. Ο λόγος που η παράμετρος args φαίνεται λίγο περίεργη είναι ότι πρέπει να περάσουμε μια ακολουθία στη συνάρτηση διπλασιασμού και χρειάζεται μόνο ένα όρισμα, επομένως πρέπει να προσθέσουμε ένα κόμμα στο τέλος για να δημιουργήσουμε την ακολουθία ενός από αυτά. Σημειώστε ότι εάν θέλετε να περιμένετε μέχρι να καθοριστεί το νήμα, μπορείτε να καλέσετε τη μέθοδό του Συμμετοχή(). Όταν εκτελείτε αυτόν τον κώδικα, θα λάβετε την ακόλουθη έξοδο:

Thread-1 0 Thread-2 2 Thread-3 4 Thread-4 6 Thread-5 8

Φυσικά, πιθανότατα δεν θέλετε να εκτυπώσετε το αποτέλεσμα σας στο stdout. Αυτό θα μπορούσε να καταλήξει σε πολύ χάος. Αντίθετα, πρέπει να χρησιμοποιήσετε μια λειτουργική μονάδα Python που ονομάζεται ξύλευση. Αυτή είναι μια μονάδα ασφαλής για νήμα και κάνει τέλεια τη δουλειά της. Ας ενημερώσουμε λίγο το παραπάνω παράδειγμα και ας προσθέσουμε μονάδα καταγραφής, και ταυτόχρονα ας ονομάσουμε τις ροές μας:

Εισαγωγή καταγραφής εισαγωγή νημάτων def get_logger(): logger = logging.getLogger("threading_example") logger.setLevel(logging.DEBUG) fh = logging.FileHandler("threading.log") fmt = "%(asctime)s - %( threadName)s - %(levelname)s - %(message)s" formatter = logging.Formatter(fmt) fh.setFormatter(formatter) logger.addHandler(fh) return logger def doubler(number, logger): """ A συνάρτηση που μπορεί να χρησιμοποιηθεί από ένα νήμα """ logger.debug("doubler function executing") result = αριθμός * 2 logger.debug("doubler function τελειώνει με: ()".format(result)) if __name__ == " __main__": logger = get_logger() thread_names = ["Mike", "George", "Wanda", "Dingbat", "Nina"] για i in range(5): my_thread = threading.Thread(target=doubler, name =thread_names[i], args=(i,logger)) my_thread.start()

Η μεγαλύτερη αλλαγή σε αυτόν τον κώδικα είναι η προσθήκη του συναρτήσεις get_logger. Αυτό το μέρος του κώδικα θα δημιουργήσει ένα καταγραφικό που έχει ρυθμιστεί για εντοπισμό σφαλμάτων. Αυτό θα αποθηκεύσει το αρχείο καταγραφής στον τρέχοντα φάκελο εργασίας (με άλλα λόγια, από όπου εκτελείται το σενάριο) και, στη συνέχεια, ορίζουμε τη μορφή κάθε γραμμής για καταγραφή. Η μορφή περιλαμβάνει χρονική σήμανση, όνομα ροής, επίπεδο καταγραφής και καταγεγραμμένο μήνυμα. Στη συνάρτηση διπλασιαστή αλλάζουμε τις εντολές εξόδου μας σε εντολές ξύλευση.

Σημειώστε ότι περνάμε το καταγραφικό στη συνάρτηση διπλασιαστή όταν δημιουργήστε ένα νήμα. Αυτό συμβαίνει γιατί αν ορίσετε ένα αντικείμενο καταγραφής σε κάθε νήμα, θα καταλήξετε σε πολλαπλάσια μονότονακαι το αρχείο καταγραφής σας θα περιέχει πολλές διπλότυπες γραμμές. Τέλος, ονομάζουμε τις ροές μας δημιουργώντας μια λίστα ονομάτων και, στη συνέχεια, ορίζουμε κάθε ροή σε ένα συγκεκριμένο όνομα χρησιμοποιώντας την παράμετρο name. Όταν εκτελείτε αυτόν τον κώδικα, θα πρέπει να λάβετε ένα αρχείο καταγραφής με το ακόλουθο περιεχόμενο:

Αυτό το αποτέλεσμα είναι αρκετά αυτονόητο, οπότε ας προχωρήσουμε. Θέλω να θίξω ένα ακόμη θέμα σε αυτό το άρθρο. Θα μιλήσουμε για κληρονομικότητα μιας κλάσης που ονομάζεται σπείρωμα.Νήμα. Ας δούμε ξανά το προηγούμενο παράδειγμα, αλλά αντί να καλέσουμε απευθείας το νήμα, θα δημιουργήσουμε τη δική μας υποκλάση. Εδώ είναι ο ενημερωμένος κώδικας:

Εισαγωγή καταγραφής εισαγωγή κλάσης νήματος MyThread(threading.Thread): def __init__(self, number, logger): threading.Thread.__init__(self) self.number = αριθμός self.logger = logger def run(self): """ Εκτέλεση the thread """ logger.debug("Calling doubler") doubler(self.number, self.logger) def get_logger(): logger = logging.getLogger("threading_example") logger.setLevel(logging.DEBUG) fh = καταγραφή .FileHandler("threading_class.log") fmt = "%(asctime)s - %(threadName)s - %(levelname)s - %(message)s" formatter = logging.Formatter(fmt) fh.setFormatter(formatter) logger.addHandler(fh) return logger def doubler(number, logger): """ Μια συνάρτηση που μπορεί να χρησιμοποιηθεί από ένα νήμα """ logger.debug("doubler function executing") result = αριθμός * 2 logger.debug( "doubler function έληξε με: ()".format(result)) if __name__ == "__main__": logger = get_logger() thread_names = ["Mike", "George", "Wanda", "Dingbat", "Nina" ] για i στο range(5): thread = MyThread(i, logger) thread.setName(thread_names[i]) thread.start()

Σε αυτό το παράδειγμα μόλις κληρονομήσαμε την κλάση σπείρωμα.Νήμα. Περάσαμε τον αριθμό που θέλουμε να διπλασιάσουμε και επίσης περάσαμε το αντικείμενο καταγραφής όπως κάναμε πριν. Αλλά αυτή τη φορά, θα διαμορφώσουμε διαφορετικά το όνομα ροής καλώντας τη συνάρτηση setNameστο αντικείμενο νήματος. Πρέπει ακόμα να καλέσουμε την έναρξη σε κάθε νήμα, αλλά να θυμάστε ότι δεν χρειάζεται να το ορίσουμε αυτό στην κλάση που κληρονομείται. Όταν καλείτε start ξεκινά το νήμα σας καλώντας μέθοδος εκτέλεσης. Στην τάξη μας, καλούμε τη συνάρτηση διπλασιαστή για να εκτελέσουμε τους υπολογισμούς μας. Η έξοδος είναι πολύ παρόμοια με αυτή στο προηγούμενο παράδειγμα, με τη διαφορά ότι πρόσθεσα μια επιπλέον γραμμή στην έξοδο. Δοκιμάστε το μόνοι σας και δείτε τι θα συμβεί.

Κλειδαριές και συγχρονισμός

Όταν έχετε περισσότερα από ένα νήμα στη διάθεσή σας, τότε ίσως χρειαστεί να καταλάβετε πώς αποφύγετε τις συγκρούσεις. Αυτό που εννοώ με αυτό είναι ότι μπορείτε να χρησιμοποιήσετε την περίπτωση όπου περισσότερα από ένα νήμα πρέπει να έχουν πρόσβαση στον ίδιο πόρο ταυτόχρονα. Εάν δεν σκέφτεστε τέτοια προβλήματα και δεν σχεδιάζετε ανάλογα, τότε μπορεί να καταλήξετε με τα χειρότερα προβλήματα σε μια πολύ άβολη στιγμή, και συνήθως όταν κυκλοφορήσει ο κώδικας.

Η λύση στο πρόβλημα είναι χρησιμοποιήστε κλειδαριές. Η κλειδαριά παρέχεται από τη λειτουργική μονάδα Python σπείρωμακαι μπορεί να κρατήσει ένα νήμα ή να μην κρατήσει καθόλου νήμα. Εάν ένα νήμα επιχειρήσει να αποκτήσει ένα κλείδωμα σε έναν πόρο που είναι ήδη κλειδωμένος, αυτό το νήμα θα περιμένει μέχρι να ξεκλειδωθεί το κλείδωμα. Ας δούμε ένα πρακτικό παράδειγμα ενός κώδικα που δεν έχει καμία λειτουργία κλειδώματος, αλλά θα προσπαθήσουμε να τον προσθέσουμε:

Σύνολο εισαγωγής νημάτων = 0 def update_total(amount): """ Ενημερώνει το σύνολο κατά το δεδομένο ποσό """ συνολικό συνολικό σύνολο += ποσό εκτύπωσης (σύνολο) εάν __name__ == "__main__": για i στο εύρος (10): my_thread = threading.Thread(target=update_total, args=(5,)) my_thread.start()

Μπορούμε να κάνουμε αυτό το παράδειγμα ακόμα πιο ενδιαφέρον προσθέτοντας μια κλήση ώρα.ύπνο. Ως εκ τούτου, το πρόβλημα εδώ είναι ότι ένα νήμα μπορεί να καλέσει update_totalκαι πριν ενημερωθεί, ένα άλλο νήμα μπορεί να το καλέσει και να προσπαθήσει να το ενημερώσει επίσης. Ανάλογα με τη σειρά των εργασιών, η τιμή μπορεί να προστεθεί μία φορά. Ας προσθέσουμε ένα κλείδωμα στη λειτουργία. Υπάρχουν δύο τρόποι για να γίνει αυτό. Το πρώτο είναι η χρήση δοκιμάστε/επιτέλους, αν θέλουμε να βεβαιωθούμε ότι έχει αφαιρεθεί η κλειδαριά. Εδώ είναι ένα παράδειγμα:

Εισαγωγή σύνολο νημάτων = 0 lock = threading.Lock() def update_total(amount): """ Ενημερώνει το σύνολο κατά το δεδομένο ποσό """ συνολικό συνολικό lock.acquire() try: total += ποσό τελικά: lock.release( ) print (σύνολο) εάν __name__ == "__main__": για το i στο range(10): my_thread = threading.Thread(target=update_total, args=(5,)) my_thread.start()

Εδώ απλά κρεμάμε την κλειδαριά πριν κάνουμε οτιδήποτε άλλο. Στη συνέχεια, προσπαθούμε να ενημερώσουμε σύνολοΚαι τελικά, αφαιρούμε την κλειδαριά και αφαιρούμε την τρέχουσα σύνολο. Μπορούμε να απλοποιήσουμε αυτήν την εργασία χρησιμοποιώντας έναν τελεστή Python που ονομάζεται με:

Εισαγωγή σύνολο νημάτων = 0 κλείδωμα = threading.Lock() def update_total(amount): """ Ενημερώνει το σύνολο κατά το δεδομένο ποσό """ συνολικό σύνολο με κλείδωμα: σύνολο += ποσό εκτύπωσης (σύνολο) εάν __name__ == "__main__ ": για i στο range(10): my_thread = threading.Thread(target=update_total, args=(5,)) my_thread.start()

Όπως μπορείτε να δείτε, δεν χρειαζόμαστε πλέον δοκιμάστε/επιτέλους, δεδομένου ότι ο διαχειριστής περιβάλλοντος παρέχεται από τον χειριστή με, τα έκανε όλα αυτά για εμάς. Φυσικά, μπορεί να βρεθείτε να γράφετε κώδικα όπου χρειάζονται πολλά νήματα με πρόσβαση σε πολλές λειτουργίες. Όταν ξεκινάς για πρώτη φορά να γράφεις ανταγωνιστικός κωδικός, μπορείτε να κάνετε κάτι σαν το εξής:

Εισαγωγή νήματος συνολικά = 0 κλείδωμα = threading.Lock() def do_something(): lock.acquire() try: print("Lock αποκτήθηκε στη συνάρτηση do_something") τελικά: lock.release() print("Το κλείδωμα απελευθερώθηκε στο do_something function") return "Τέλος κάνοντας κάτι" def do_something_else(): lock.acquire() try: print("Lock αποκτήθηκε στη συνάρτηση do_something_else") τελικά: lock.release() print("Το κλείδωμα απελευθερώθηκε στη συνάρτηση do_something_else") επιστρέψτε "Τελείωσε κάτι άλλο" εάν __name__ == "__main__": result_one = do_something() result_two = do_something_else()

Αυτός ο κωδικός λειτουργεί καλά σε αυτήν την περίπτωση, αλλά προϋποθέτει ότι έχετε πολλαπλά νήματακαλώντας και τις δύο αυτές συναρτήσεις. Ενώ το ένα νήμα εργάζεται σε λειτουργίες, το δεύτερο μπορεί, με τη σειρά του, να ενημερώσει τα δεδομένα και θα λάβετε ένα εσφαλμένο αποτέλεσμα. Το πρόβλημα είναι ότι μπορεί να μην παρατηρήσετε στην αρχή ότι υπάρχει κάτι λάθος με τα αποτελέσματα. Πώς να βρείτε μια λύση σε αυτό το πρόβλημα; Ας το εξετάσουμε. Το πρώτο πράγμα που μπορείτε να κάνετε είναι να κλειδώσετε δύο κλήσεις λειτουργιών. Ας προσπαθήσουμε να ενημερώσουμε το παραπάνω παράδειγμα για να έχουμε κάτι σαν το εξής:

Εισαγωγή νήματος συνολικά = 0 lock = threading.RLock() def do_something(): with lock: print("Lock αποκτήθηκε στη συνάρτηση do_something") print("Το κλείδωμα απελευθερώθηκε στη συνάρτηση do_something") return "Τέλος κάνοντας κάτι" def do_something_else (): with lock: print("Lock αποκτήθηκε στη συνάρτηση do_something_else") print("Lock απελευθερώθηκε στη συνάρτηση do_something_else") return "Τελειώσαμε κάτι άλλο" def main(): with lock: result_one = do_something() result_two = do_something_else () print (result_one) print (result_two) if __name__ == "__main__": main()

Όταν εκτελείτε αυτόν τον κωδικό, θα δείτε ότι απλώς κολλάει. Ο λόγος είναι ότι απλώς λέμε στη μονάδα σπείρωμακρεμάστε μια κλειδαριά. Όταν λοιπόν καλέσουμε την πρώτη συνάρτηση, βλέπει ότι η κλειδαριά είναι ήδη κρεμασμένη και μπλοκαρισμένη. Αυτό θα διαρκέσει μέχρι να αφαιρεθεί η κλειδαριά, κάτι που δεν θα συμβεί ποτέ, αφού αυτό δεν προβλέπεται στον κωδικό. Μια καλή λύση σε αυτή την περίπτωση είναι να χρησιμοποιήσετε μια κλειδαριά επανεισόδου. Μονάδα μέτρησης σπείρωμαπαρέχει ένα ως συνάρτηση RLLock. Απλώς αντικαταστήστε το κλείδωμα γραμμής = κλωστή.Κλείδωμα() στην κλειδαριά = σπείρωμα.RLlock() και δοκιμάστε να εκτελέσετε ξανά τον κώδικα. Τώρα πρέπει να το κερδίσει. Εάν θέλετε να δοκιμάσετε τον παραπάνω κώδικα αλλά να προσθέσετε νήματα σε αυτόν, τότε μπορούμε να τον αντικαταστήσουμε κλήσηεπί κύριοςμε τον εξής τρόπο:

Αν __name__ == "__main__": για το i στο range(10): my_thread = threading.Thread(target=main) my_thread.start()

Αυτό θα εκτελέσει την κύρια συνάρτηση σε κάθε νήμα, η οποία με τη σειρά της θα καλέσει τις άλλες δύο συναρτήσεις. Στο τέλος θα πάρετε μια αρκετά μεγάλη πληρωμή.

Χρονοδιακόπτες

Μονάδα μέτρησης σπείρωμαπεριλαμβάνει μια πολύ βολική τάξη που ονομάζεται Μετρών την ώραν, το οποίο μπορείτε να χρησιμοποιήσετε για να ενεργοποιήσετε μια ενέργεια μετά από ένα ορισμένο χρονικό διάστημα. Αυτή η τάξη ξεκινά το δικό της νήμα και αρχίζει να λειτουργεί από το ίδιο μέθοδος έναρξης(), όπως ακριβώς και οι κανονικές ροές. Μπορείτε επίσης να σταματήσετε το χρονόμετρο χρησιμοποιώντας τη μέθοδο ακύρωσης. Λάβετε υπόψη ότι μπορείτε να ακυρώσετε το χρονόμετρο πριν καν ξεκινήσει. Κάποτε είχα μια περίπτωση όπου χρειαζόταν να επικοινωνήσω με μια υποδιεργασία που είχα ξεκινήσει, αλλά χρειαζόμουν αντίστροφη μέτρηση. Παρόλο που υπάρχουν πολλοί διαφορετικοί τρόποι επίλυσης αυτού του συγκεκριμένου προβλήματος, η αγαπημένη μου λύση ήταν πάντα η χρήση Κατηγορία χρονοδιακόπτημονάδα σπειρώματος Για αυτό το παράδειγμα, θα ρίξουμε μια ματιά στη χρήση της εντολής ping. Σε Linux, εντολή pingθα λειτουργήσει μέχρι να το σκοτώσεις. Έτσι η κλάση Timer γίνεται ιδιαίτερα χρήσιμη στον κόσμο του Linux. Εδώ είναι ένα παράδειγμα:

Εισαγωγή υποδιεργασίας από threading εισαγωγή Χρονοδιακόπτης kill = διαδικασία λάμδα: process.kill() cmd = ["ping", "www.google.com"] ping = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) my_timer = Timer(5, kill, ) try: my_timer.start() stdout, stderr = ping.communicate() τελικά: my_timer.cancel() print (str(stdout))

Εδώ απλώς ρυθμίζουμε ένα λάμδα που μπορούμε να χρησιμοποιήσουμε για να σκοτώσουμε τη διαδικασία. Στη συνέχεια ξεκινάμε την εργασία μας pingκαι δημιουργήστε ένα αντικείμενο Timer. Σημειώστε ότι το πρώτο όρισμα είναι το χρονικό όριο σε δευτερόλεπτα, μετά η συνάρτηση που θα κληθεί και το όρισμα που θα μεταβιβαστεί στη συνάρτηση. Στην περίπτωσή μας, η συνάρτησή μας είναι λάμδα και της μεταβιβάζουμε μια λίστα ορισμάτων, όπου η λίστα περιέχει μόνο ένα στοιχείο. Εάν εκτελέσετε αυτόν τον κωδικό, θα εκτελεστεί για περίπου 5 δευτερόλεπτα πριν εμφανιστεί το αποτέλεσμα ping.

Άλλα στοιχεία νήματος

Μονάδα μέτρησης σπείρωμαπεριλαμβάνει επίσης υποστήριξη για άλλα αντικείμενα. Για παράδειγμα, μπορείτε να δημιουργήσετε σηματοφόρος, που είναι ένα από τα παλαιότερα χρονικά πρωτόγονα στην επιστήμη των υπολογιστών. Σηματοφόροι έλεγχοι εσωτερικός μετρητής, το οποίο θα μειωθεί όταν καλέσετε αποκτώκαι αυξάνεται όταν καλείτε ελευθέρωση. Ο μετρητής είναι σχεδιασμένος με τέτοιο τρόπο ώστε να μην μπορεί να πέσει κάτω από το μηδέν. Έτσι, αν συμβεί να καλέσετε αποκτώόταν είναι μηδέν, θα μπλοκάρει.

Ένα άλλο χρήσιμο εργαλείο που περιέχεται στη μονάδα είναι Εκδήλωση. Με αυτό μπορείτε να αποκτήσετε επικοινωνία μεταξύ δύο νημάτων χρησιμοποιώντας σήματα. Θα δούμε παραδείγματα χρήσης του Event στο επόμενο άρθρο. Τέλος, στην Python 3.2 προστέθηκε το αντικείμενο Εμπόδιο. Αυτό είναι ένα πρωτόγονο που διαχειρίζεται το thread pool, ανεξάρτητα από το πού περιμένουν τα νήματα τη σειρά τους. Για να περάσει το φράγμα, το νήμα πρέπει να καλέσει τη μέθοδο Περίμενε(), το οποίο θα μπλοκάρει μέχρι να πραγματοποιήσουν την κλήση όλα τα νήματα. Μετά από αυτό, όλες οι ροές θα συνεχιστούν ταυτόχρονα.

Επικοινωνία νήματος

Υπάρχουν πολλές περιπτώσεις όπου πρέπει να δημιουργήσετε νήματα που σχετίζονται μεταξύ τους. Όπως ανέφερα προηγουμένως μπορείτε να χρησιμοποιήσετε Εκδήλωσηγια το σκοπό αυτό. Αλλά ένας πιο βολικός τρόπος είναι η χρήση Ουρά. Στο παράδειγμά μας χρησιμοποιούμε και τις δύο μεθόδους! Ας δούμε πώς θα μοιάζει:

Εισαγωγή νήματος από εισαγωγή ουράς Ουρά def creator(data, q): """ Δημιουργεί δεδομένα προς κατανάλωση και περιμένει τον καταναλωτή να ολοκληρώσει την επεξεργασία του """ print("Δημιουργία δεδομένων και τοποθέτησή τους στην ουρά") για το στοιχείο στα δεδομένα : evt = threading.Event() q.put((item, evt)) print("Waiting for data to be double") evt.wait() def my_consumer(q): """ Καταναλώνει ορισμένα δεδομένα και δουλεύει σε αυτό Σε αυτήν την περίπτωση, το μόνο που κάνει είναι να διπλασιάσει την είσοδο """ ενώ True: data, evt = q.get() print("data found to processed: ()".format(data)) processed = data * 2 print (επεξεργασμένο) evt.set() q.task_done() if __name__ == "__main__": q = Queue() data = thread_one = threading.Thread(target=creator, args=(data, q)) thread_two = threading. Thread(target=my_consumer, args=(q,)) thread_one.start() thread_two.start() q.join()

Ας επιβραδύνουμε λίγο. Πρώτον, έχουμε μια λειτουργία δημιουργός(γνωστός και ως παραγωγός), τα οποία χρησιμοποιούμε για να δημιουργήσουμε τα δεδομένα με τα οποία θέλουμε να εργαστούμε (ή να χρησιμοποιήσουμε). Στη συνέχεια παίρνουμε μια άλλη συνάρτηση που χρησιμοποιούμε για την επεξεργασία των δεδομένων που καλείται my_consumer. Η συνάρτηση δημιουργού χρησιμοποιεί τη μέθοδο Ουράκαλείται put για να προσθέσει δεδομένα στην ουρά, τότε ο καταναλωτής θα ελέγξει με τη σειρά του αν υπάρχουν νέα δεδομένα και θα τα επεξεργαστεί όταν υπάρχουν. Ουράχειρίζεται όλα τα κλεισίματα και το άνοιγμα των κλειδαριών, έτσι προσωπικά δεν αντιμετωπίζετε αυτή τη μοίρα.

Σε αυτό το παράδειγμα, δημιουργήσαμε μια λίστα με τιμές που θέλουμε να αντιγράψουμε. Στη συνέχεια δημιουργούμε δύο νήματα, ένα για τη συνάρτηση δημιουργός/παραγωγός, δεύτερο για καταναλωτής(καταναλωτής). Παρατηρήστε ότι περνάμε ένα αντικείμενο Ουράκάθε νήμα, το οποίο είναι εντελώς μαγικό λαμβάνοντας υπόψη τον τρόπο χειρισμού των κλειδαριών. Η ουρά θα ξεκινήσει με το πρώτο νήμα να περνά δεδομένα στο δεύτερο. Όταν το πρώτο νήμα στέλνει κάποια δεδομένα στην ουρά, τα στέλνει και στο Εκδήλωση, μετά περιμένει να συμβούν τα γεγονότα για να ολοκληρωθούν. Στη συνέχεια, στη συνάρτηση καταναλωτή, γίνεται επεξεργασία των δεδομένων και μετά καλείται η μέθοδος διαμόρφωσης Εκδήλωση, το οποίο λέει στο πρώτο νήμα ότι το δεύτερο έχει ολοκληρώσει την επεξεργασία ώστε να μπορεί να συνεχίσει. Η τελευταία γραμμή κωδικών κλήσεων μέθοδος ένωσηςένα αντικείμενο ουράς που λέει στην ουρά να περιμένει μέχρι να ολοκληρωθεί η επεξεργασία των νημάτων. Το πρώτο νήμα τελειώνει όταν δεν έχει τίποτα άλλο να περάσει στην ουρά.

Ας το συνοψίσουμε

Καλύψαμε αρκετό υλικό. Και συγκεκριμένα:

  1. Βασικά στοιχεία εργασίας με τη μονάδα threading
  2. Πώς λειτουργούν οι κλειδαριές;
  3. Τι είναι το Event και πώς μπορεί να χρησιμοποιηθεί
  4. Πώς να χρησιμοποιήσετε το χρονόμετρο
  5. Επικοινωνία εντός νήματος με χρήση ουράς/συμβάντος

Τώρα ξέρετε πώς να χρησιμοποιείτε τα νήματα και σε τι είναι καλά. Ελπίζω να βρείτε κάποια χρήση για αυτά στον δικό σας κώδικα!



Έχετε ερωτήσεις;

Αναφέρετε ένα τυπογραφικό λάθος

Κείμενο που θα σταλεί στους συντάκτες μας: