Το προφίλ μας στο Google Plus
0

ScanDir v2.0: Ευκαιρία για multi-threaded programming!

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

Να τονίσουμε ευθύς εξ αρχής ότι δεν μας ενδιαφέρει μια ξερή παράθεση εντολών. Πολύ περισσότερο, σκοπεύουμε να κάνουμε μια τεχνική συζήτηση περί του προγραμματισμού γενικότερα και περί των σχετικών εργαλείων που μας παρέχονται. Μάλιστα, αυτό που τελικά θα θέλαμε είναι αυτός ο νέος Web Crawler που θα σας δώσουμε να αποτελέσει κάτι σαν “παράπλευρη συνέπεια” της όλης συζήτησης, υπό την έννοια ότι θα επιθυμούσαμε να εστιάσουμε περισσότερο στο “πώς” παρά στο “τι”.

Επειδή βέβαια είμαστε και άνθρωποι της πράξης, θα δώσουμε το εκτελέσιμο, τον πηγαίο κώδικα, καθώς κι ένα ακόμα απαραίτητο βοηθητικό αρχείο (θα τα δούμε όλα αυτά σε λίγο), ώστε να τα κάνετε ό,τι εσείς θέλετε. Το πρόγραμμα έχει γραφτεί σε C#, στο δωρεάν Visual Studio 2013 Community Edition και απαιτεί την παρουσία του .NET 4.5.

.NET: Φίλος ή εχθρός;
Πριν καταλήξουμε να χωρίζουμε τα εργαλεία σε φιλικά και εχθρικά, οφείλουμε να πούμε ότι ο τίτλος αυτής της παραγράφου είναι λάθος — ή καλύτερα “μαρκετίστικος”. Το .NET είναι ένα εργαλείο προγραμματισμού. Τα εργαλεία δεν μορούν να είναι φιλικά ή εχθρικά προς τον άνθρωπο, αλλά μόνο η χρήση τους μπορεί να χαρακτηριστεί έτσι ή αλλιώς. Σε γενικές γραμμές, κατά την άποψη του γράφοντα το .NET αποτελεί μια προσπάθεια της Microsoft επιεικώς …άριστη. Σχετικά πρόσφατα, μάλιστα, ο κώδικας του .NET διατέθηκε στην κοινότητα του Open Source μέσω του GitHub. (Διαβάστε περισσότερα και στο επίσημο blog: http://blogs.msdn.com/b/dotnet). Πρακτικά, με το .NET μπορεί κάποιος ν’ αναπτύσσει εφαρμογές για την πλατφόρμα που επιθυμεί (Windows, Linux, Android κ.ά.).

Θέλουμε στο σημείο αυτό να επισημάνουμε ότι δεν αγαπάμε τα στερεότυπα (“η κακιά Microsoft”, “η καλή Java” κ.ο.κ.). Γενικά, προσπαθούμε ώστε η άποψή μας να μην είναι εξαρτημένη από τη μόδα ή από την όποια ανάγκη να ανήκουμε κάπου. Αντίθετα, προσπαθούμε ώστε η οπτική μας να είναι τεχνολογική ή/και επιστημονική — και ουχί οικονομική :) Υπό αυτό το πρίσμα, θα παραδεχτούμε οτι το .NET έδωσε μια ώθηση στον τομέα της ανάπτυξης λογισμικού –για το γράφοντα όχι απλά ώθηση, αλλά δυνατή κλωτσιά–, εννοείται προς το καλύτερο. Μάλιστα τα θετικά του .NET αφορούν τόσο στην ασφάλεια, όσο και στο performance. Επίσης, θεωρούμε κακή επιλογή (για πολλούς λόγους, τόσο επιστημονικούς όσο και επαγγελματικούς) το να μη διδάσκεται το συγκεκριμένο περιβάλλον ανάπτυξης σε πολλά τμήματα πληροφορικής των Ελληνικών Πανεπιστημίων. Για να απαντήσουμε λοιπόν στο ψευτο-ερώτημα μας, πιστεύουμε ότι το .NET είναι καρα-φίλος!

Λειτουργικότητα και χαρακτηριστικά
Να αναφέρουμε ότι, κάποτε, σε ένα παράλληλο σύμπαν, είχε δημοσιευτεί το ScanDir v.1.0, μια παλαιότερη version του προγράμματός μας, το οποίο είχε λιγότερα χαρακτηριστικά σε σύγκριση με τη σημερινή έκδοση. Για να μην πολυλογούμε, αρκεί να πούμε ότι η έκδοση 2 είναι –το παραδεχόμαστε– αισθητά βελτιωμένη. Όμως τι ακριβώς κάνει το πρόγραμμα; Στην είσοδό του δέχεται ένα λεξικό με ονόματα αρχείων καθώς κι ένα URL, κι ακολούθως εξετάζει αν τα αρχεία αυτά υπάρχουν στο υπό εξέταση site (που καθορίζεται από το URL). Ένα αντίστοιχο, πολύ γνωστό πρόγραμμα που κάνει παρόμοια δουλειά, είναι το DirBuster. Τα λεξικά του μπορείτε να τα χρησιμοποιείτε και με το ScanDir v2.0, αρκεί να έχετε διαγράψει τις αρχικές γραμμές με τα σχόλια (είναι οι γραμμές που αρχίζουν με το χαρακτήρα #). Για την ευκολία σας, θα σας δώσουμε ένα λεξικό με 225000 περίπου ονόματα, ώστε να κάνετε πιο άνετα τις πρώτες δοκιμές σας. Εννοείται, περιττό να σημειώσουμε, ότι μπορείτε να δημιουργήσετε και τα δικά σας λεξικά.

Τα νέα προχωρημένα χαρακτηριστικά του ScanDir v2.0, τα οποία θα αναλυθούν στις επόμενες παραγράφους, είναι τα ακόλουθα:

  • Χρήση πολλαπλών threads, το πλήθος των οποίων ορίζεται δυναμικά από το χρήστη
  • Τα αρχεία του dictionary, όταν αυτό ανοιχτεί, μοιράζονται (κατά το δυνατόν ομοιόμορφα) στα N threads κι εκτελούνται παράλληλα ή σχεδόν παράλληλα (θα μιλήσουμε γι’ αυτό)
  • Ο χρήστης έχει τη δυνατότητα να ορίσει κάποιον proxy, μέσω του οποίου θα ελεγχθεί το site (έτσι, θα κρύψει την πραγματική δημόσια διεύθυνση IP του υπολογιστή του).
  • Γίνεται χρήση συγκεκριμένων χαρακτηριστικών (εντολών) του .NET, ώστε να αποφεύγονται προβλήματα επικάλυψης (και άρα μη σωστής λειτουργίας) των πολλαπλών threads.
  • Ο χρήστης ορίζει ποιο θα είναι το extension των προς έλεγχο αρχείων (π.χ., .php, .asp, .aspx κ.ά.). Έτσι, είναι δυνατόν να ελέγχονται sites σε PHP, ASP, ASP.NET κ.ο.κ.

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

Τα threads και η χρήση τους
Η έννοια των threads αποτελεί κλασικό advanced topic στη βιβλιογραφία περί προγραμματισμού. Περιττό να πούμε ότι δε νοείται επιστήμονας ή επαγγελματίας προγραμματιστής που να μη γνωρίζει περί των threads και πώς μπορεί να τα χειριστεί. Πρέπει όμως να σημειώσουμε ότι τα threads είναι και κάτι σαν taboo για τους προγραμματιστές. Γενικά αποφεύγονται, αφού αρκετές φορές το debugging ενός προγράμματος με πολλά threads είναι από εφιαλτική έως ακατόρθωτη εργασία. Μολαταύτα, ένα πρόγραμμα που κάνει σωστή χρήστη των threads αποδεικνύεται χρήσιμο στην πράξη — και ξεχωρίζει.

Γενικά, καλό είναι να γνωρίζουμε τα ακόλουθα. Τη στιγμή που εκτελείται ένα πρόγραμμα σε .NET, δημιουργεί ένα process στη μνήμη (για να το πούμε απλά) και μέσα σε αυτό δεσμεύεται κι ένα default thread. Το πρόγραμμά μας τρέχει “μέσα” σε αυτό το thread. Στην ουσία, το λειτουργικό σύστημα είναι αυτό που φτιάχνει το εν λόγω process, σε συνεργασία με την εικονική μηχανή (virtual machine, VM — στην περίπτωσή μας υλοποιείται από το .NET). Μέσα σε κάθε process μπορούν να τρέχουν ένα ή περισσότερα threads. Συνήθως, στα απλά και μικρά προγράμματα, το thread είναι ένα. Είναι αυτό που φιλοξενεί τόσο τις συναρτήσεις που φτιάχνουμε εμείς, όσο κι αυτές που απαιτεί και χρησιμοποιεί το UI (User Interface) λόγω της χρήσης των components (που επίσης βάζουμε εμείς). Επειδή οι συναρτήσεις και τα components του UI μοιράζονται το ίδιο thread, έχουμε το περίφημο φαινόμενο του “παγώματος” του προγράμματος που εκτελεί κάποια “βαριά” (πείτε τη και χρονοβόρα) εργασία, από μια συνάρτηση που έχουμε φτιάξει. Πολλοί θα έχετε παρατηρήσει το περίφημο μήνυμα “Not Responding”, στον Task Manager των Windows, καθώς και το “ξάσπρισμα” του παραθύρου, που βασικά σημαίνει “μη με αγγίζεις, κάνω κάτι άλλο”.

Το φαινόμενο μπορούμε να το ξεπεράσουμε βάζοντας σε ένα άλλο (ή σε άλλα) thread(s) τις συναρτήσεις που απαιτούν επεξεργασία, απελευθερώνοντας έτσι το UI. Ωραίο ακούγεται αυτό –και είναι– αλλά θέλει λίγο προσοχή και εμπειρία, αφού εύκολα μπορεί να χαθεί η μπάλα. Πρέπει να έχουμε υπόψη το εξής: Όταν ένα process εκτελεί περισσότερα του ενός threads, τότε αυτά εκτελούνται ασύγχρονα και, μερικές φορές, παράλληλα! Στο παρόν άρθρο μας απασχολεί το “ασύγχρονα” και το “παράλληλα” το αφήνουμε στην άκρη. Δεν είναι τώρα ο σκοπός μας να εξηγήσουμε τι ακριβώς σημαίνει “παραλληλία” για ένα VM σε συνεργασία με το λειτουργικό, πώς και πότε υλοποιείται και πόσο ρόλο παίζουν τα περίφημα cores του επεξεργαστή. Θεωρήστε, λοιπόν, ότι δεν έχουμε παράλληλη επεξεργασία ή, ισοδύναμα, ότι ο επεξεργαστής μας μπορεί να τρέχει μόνο ένα thread την φορά (για το καλό σας το λέμε — πιστέψτε μας :D)

Ας εστιάσουμε λοιπόν στο “ασύγχρονα”. Ο όρος σημαίνει πως αν, π.χ., έχουμε 10 threads που τρέχουν κάτω από ένα process, τη χρονική στιγμή t δεν είναι δυνατόν να γνωρίζουμε ποιο thread απασχολεί τον επεξεργαστή. Ας το δούμε αυτό και μ’ ένα παράδειγμα. Έστω ότι έχουμε φτιάξει μια συνάρτηση που λέγεται HeavyWork(a, b, c) και που αποτελείται από 200 γραμμές κώδικα κι εκτελεί μια πολύ βαριά εργασία. Επίσης, έχουμε φτιάξει τη HeavyWork(a, b, c) έτσι ώστε να τρέχει σε 10 threads. Τρέχουν, δηλαδή, 10 διαφορετικές HeavyWork(a, b, c) μέσα στο process μας. Κανείς –μα κανείς– δεν μας εγγυάται ότι αυτές θα τρέχουν κατά σειρά (δηλαδή πρώτα η 1η, αφού τελειώσει η 2η κ.ο.κ.). Στην πραγματικότητα, θα συμβεί το εξής: Θα ξεκινήσει η 1η και πριν τελειώσει θα διακοπεί και θα ξεκινήσει η 2η και πριν τελειώσει θα διακοπεί και θα ξεκινήσει η 3η κο.κ. έως ότου ξεκινήσει η 10η. Επειδή η 10η είναι η τελευταία, μη νομίζετε ότι δεν θα διακοπεί από κάποια άλλη. Αντιθέτως: Θα διακοπεί κάποια στιγμή, ώστε να συνεχίσει κάποια άλλη συνάρτηση –δεν ξέρουμε ποια ακριβώς– που είχε σταματήσει πριν και τώρα το λειτουργικό αποφάσισε ότι περίμενε αρκετά.

Σκεφτείτε τώρα λίγο το ακόλουθο σενάριο. Έχουμε τον επεξεργαστή, στο οποίο το ΛΣ δίνει και παίρνει εργασίες. Τη στιγμή t του δίνει το thread 1 κι ο επεξεργαστής αρχίζει να δουλεύει μέσω αυτού του thread. Τη χρονική στιγμή t+1 του λέει “παράτα το thread 1 και πιάσε το thread 2”. Ο καημένος ο επεξεργαστής όμως δεν είχε προλάβει να τελειώσει όλες τις γραμμές επεξεργασίας της συνάρτησης HeavyWork(a, b,c), που τρέχει στο thread 1 κι όπως είπαμε έχει 200 γραμμές. Ας πούμε ότι πρόλαβε να κάνει τις πρώτες 10 γραμμές. Τα βροντάει όλα, λοιπόν, και πιάνει τη HeavyWork(a, b, c) στο thread 2. Σκεφτείτε τώρα κάτι ακόμα πιο σύνθετο: Ότι δεν είμαστε στον πρώτο γύρο, αλλά στον τρίτο. Μ’ άλλα λόγια, το λειτουργικό έπιασε το thread 2 και το άφησε άλλες δύο φορές πριν, ενώ τώρα το πιάνει για την τρίτη φορά. Θυμάται άραγε ο επεξεργαστής σε τι κατάσταση είχε αφήσει τη HeavyWork(a, b, c), τελευταία φορά που δούλεψε το thread 2; Όταν μιλάμε για “κατάσταση”, εννοούμε, π.χ., ότι αν ο επεξεργαστής παράτησε τη συνάρτηση τελευταία φορά στη γραμμή 120 και τώρα του έρχεται από το λειτουργικό η γραμμή 121, η οποία περιέχει μια μεταβλητή που είχε οριστεί στη γραμμή 5, θυμάται ο επεξεργαστής την τιμή της ή την έχει μπερδέψει/ανακατέψει με την τιμή της ίδιας μεταβλητής, άλλων threads που ήδη έχουν τρέξει την ίδια function; Η απάντηση, δυστυχώς, είναι αρνητική: δεν θυμάται. Για την ακρίβεια, δεν μπορεί να θυμάται. Η τιμή της μεταβλητής θα είναι η τιμή που απέδωσε σε αυτή το πιο πρόσφατο thread. Άρα, σε μια τέτοια περίπτωση το πρόγραμμα θα δούλευε χαοτικά.

Υπάρχει άραγε τρόπος ώστε να προστατέψουμε τη συνάρτηση μας και να πούμε στο λειτουργικό να μη διακόπτει τη διαδικασία (και να περνά σε άλλο thread), αν πρώτα δεν έχει εκτελεστεί πλήρως η συνάρτηση HeavyWork(a, b, c); Η απάντηση, ευτυχώς, είναι θετική: υπάρχει. Πρόκειται για το γνωστό Critical Section, που σημαίνει ότι μπορούμε να ορίσουμε μια περιοχή κώδικα η οποία θα εκτελείται ή όλη ή καθόλου. Όποιος έχει ασχοληθεί με βάσεις δεδομένων, θα κατάλαβε ότι η λογική είναι η ίδια με το περίφημο Isolation Level ή αλλιώς τον μηχανισμό κλειδώματος (βλέπε lock()). Μπορούμε, δηλαδή, να “λοκάρουμε” ένα μέρος του κώδικά μας και να πούμε πως όταν αυτό τρέχει μέσα σ’ ένα thread, πρέπει οπωσδήποτε να τελειώσει πριν το thread αλλάξει με κάποιο άλλο. Επίσης, η C# μάς δίνει εντολές ώστε να προλαβαίνουμε προβλήματα επικάλυψης με το να υποχρεώνουμε τo thread Ν να περιμένει να τελειώσει το N-1 πριν ξεκινήσει (βλέπε συνάρτηση join()).

Όσοι φίλοι ενδιαφέρονται για περισσότερη εμβάθυνση, μπορούν να διαβάσουν το σχετικό άρθρο στο MSDN: http://bit.ly/MSDN-Threads-Threading. Επίσης, ένα πολύ κατατοπιστικό άρθρο για το πως ακριβώς η καρδιά του .NET Virtual Machine (η CLR, Common Language Runtime) χειρίζεται τα threads, καθώς και αναφορές για την παράλληλη επεξεργασία, μπορείτε να διαβάσετε σε αυτό το άρθρο του MSDN: http://bit.ly/threadMGMCLR. Το τελευταίο είναι λίγο παλιό, αλλά παρέχει απαραίτητες γνώσεις για όσους επιθυμούν να μάθουν περισσότερα.

Μεθοδολογία και threads στην πράξη
To πρόγραμμά μας διαβάζει ένα λεξικό με μερικές χιλιάδες ονόματα αρχείων και για καθένα ελέγχει αν υπάρχει σε ένα δεδομένο site. Αποφασίσαμε, λοιπόν, αυτές τις δοκιμές να τις υλοποιήσουμε με threads. Μάλιστα, το πλήθος των threads θα δίνεται από το χρήστη κατά την κλήση του προγράμματος. Φυσικά, υπάρχει ένα ελάχιστο και ένα μέγιστο όριο στο πλήθος των threads — και τα έχουμε ορίσει σε 1 και 100 αντίστοιχα.

Επίσης, αποφασίσαμε να μοιράσουμε ομοιόμορφα σε κάθε thread το πλήθος των αρχείων που θα ελεγχθούν. Για παράδειγμα, αν το λεξικό μας έχει 100 ονόματα αρχείων και ο χρήστης ορίσει 5 threads, τότε κάθε thread θα αναλάβει να ελέγξει από 20 ονόματα (βλ. εικόνα 1). Φυσικά το πρόγραμμα προβλέπει και τις περιπτώσεις εξαιρέσεων. Αναλυτικότερα:

  • Αν το πλήθος των ονομάτων είναι μικρότερο από το πλήθος των threads, τότε ο αριθμός των threads που θα εκτελεστούν “εκφυλίζεται” στο πλήθος των ονομάτων και συνεπώς κάθε thread θα αναλάβει ένα όνομα.
  • Αν η κατανομή των ονομάτων στα threads δεν μπορεί να γίνει ομοιόμορφα, π.χ., επειδή υπάρχουν 10 ονομάτα στο λεξικό κι έχουμε ορίσει 3 threads, τότε το τελευταίο thread θα αναλάβει τα ονόματα που περισσεύουν. Έτσι, για 10 ονόματα το thread1 θα αναλάβει 3 ονόματα, το thread2 επίσης 3 ονόματα και το thread3 θα αναλάβει 4 ονόματα.

Μόλις έχουμε ολοκληρώσει την έρευνα σε site, χρησιμοποιώντας λεξικό με 100 βασικά ονόματα τύπου .aspx (.NET).

Ακολουθεί ο κώδικας που υλοποιεί τα παραπάνω.

// Divide the whole dictionary in N threads and execute...
// read dictionary from disk

try
{
	this.dictionaryFileLines = File.ReadAllLines(textBoxFile.Text);
}

catch (Exception ex)
{
	MessageBox.Show("Αποτυχία ανάγνωσης του αρχείου:\n"+ ex.Message, "Πρόβλημα!", 
        	MessageBoxButtons.OK,
		MessageBoxIcon.Error);
	return;
}

int iNumOfThreads = Convert.ToInt16(numericUpDownThreads.Value);
int iDictionaryLength = dictionaryFileLines.Count();
if (iNumOfThreads > iDictionaryLength)
	iNumOfThreads = iDictionaryLength;

label0.Text =  (Convert.ToInt32(label0.Text) + iDictionaryLength).ToString();

int step = iDictionaryLength / iNumOfThreads;
int n2 = 0;
int[] rangesFrom = new int[iNumOfThreads]; // Keeps the 'from' range.
int[] rangesTo = new int[iNumOfThreads];   // Keeps the 'to' range.
int rangesSize = 0;  // The size of the above arrays. 
Boolean goon = true;

// Create two parallel arrays with the ranges (from,to) to assign  N threads to M filenames.

for (int i = 0; i < iDictionaryLength; i += step)
{
	rangesSize++;
	if (rangesSize >= iNumOfThreads)  // force this to be the last step.
        {
        	n2 = (iDictionaryLength - 1);
		goon = false;
	}
	else
	{
		n2 = (i + step - 1);
	}

Aυτό που κάνουμε στο παραπάνω τμήμα κώδικα είναι η υλοποίηση της λογικής του μοιράσματος των ονομάτων αρχείων ανά thread. Για να γίνει πιο κατανοητός ο αλγόριθμος θα τον περιγράψουμε με ένα παράδειγμα. Έστω ότι το πλήθος των ονομάτων στο λεξικό είναι 100 και ότι το πλήθος των threads είναι 5. Άρα, θα θέλουμε 20 ονόματα ανά thread. Κατ’ αρχάς διαβάζουμε τα περιεχόμενα του λεξικού από το δίσκο και τα βάζουμε μέσα σε ένα array of strings, το dictionaryFileLines. Μετά, υπολογίζουμε κάποιες βασικές τιμές που τις καταχωρούμε στις αντίστοιχες μεταβλητές. (Δεν χρειάζεται ανάλυση εδώ, αφού και μια απλή ανάγνωση του κώδικα είναι αρκετή.) Τέλος, φτιάχνουμε δύο δυναμικά arrays (rangesFrom και rangesTo) τα οποία θα κρατάνε το εύρος των τιμών που θα “δουλέψουν” καθένα από τα 5 threads. Δηλαδή, στην περίπτωσή μας θα έχουμε:

========================
idx	rangesFrom	rangesTo
========================
0	0			19
1	20			39
2	40			59
3	60			79
4	80			99
========================

Αυτό που μένει λοιπόν είναι να φτιάξουμε μια συνάρτηση που θα λαμβάνει ως παραμέτρους ζευγάρια <από, έως> κάθε γραμμής παραπάνω και θα “τσιμπάει” τα αντίστοιχα ονόματα από το λεξικό, προκειμένου να ελέγξει αν υπάρχουν στο προς εξέταση site. Η συνάρτηση αυτή θα κληθεί 5 φορές, όσες και οι γραμμές του πίνακα ή αλλιώς το πλήθος των threads που ορίσαμε. Άρα, αν η συνάρτηση κληθεί σε 5 διαφορετικά threads τότε έχουμε υλοποιήσει τον στόχο μας. Αυτό λοιπόν το καταφέρνει ο ακόλουθος κώδικας:

// Start the thread per each array 
for (int i = 0; i < rangesSize; i++)
{
	int from = rangesFrom[i];
	int to = rangesTo[i];
	string s = "Starting thread..." + (from+1).ToString() + " to " + (to+1).ToString();
	AddString(s);
	Thread myNewThread = new Thread(() => processLines(from, to));
	myNewThread.Start();
	MyThreads.Add(myNewThread); // keep threads in list
}

Απ’ ό,τι βλέπετε, η βασική function που καλείται είναι η processLines(from, to). Για την ακρίβεια, καλείται τόσες φορές όσα είναι τα threads που έχει ορίσει ο χρήστης να τρέχουν. Οι δε παράμετροι from και to περιέχουν τις γραμμές των arrays rangesFrom και rangesTo. Η συνάρτηση αυτή επιτελεί το βασικό σκοπό το προγράμματος: Διαβάζει τις λέξεις (από from έως to) που βρίσκονται στο λεξικό και για κάθε λέξη εξετάζει την ύπαρξη αντίστοιχου αρχείου στο προς εξέταση site. Ο βασικός κώδικάς της είναι:

private void processLines(int from, int to)
{
	...
	// read a line of text
	for (int i = from; i <= to && !this.closeRequested; i++)
	{
		lock (this)
		{
			if (this.closeRequested)
				break;
			//Create the url
			url = initial_url + dictionaryFileLines[i] + textBoxFileType.Text;
			recordsRead++;
			ShowIteration(Convert.ToString(recordsRead));
			//Try to GET the url
			int res = get_URL(url);
			...
			url += textBoxFileType.Text;
			if (!normal)
				break;
		} //lock
	} // for
}

Απ’ ό,τι βλέπετε, έχουμε την επαναληπτική διαδικασία (from-to), η οποία διακόπτεται όταν η τιμή this.closeRequested πάρει την τιμή false. Πληροφοριακά, λέμε ότι την τιμή false θα την πάρει όταν ο χρήστης πατήσει το κουμπί “Ακύρωση”. Η this.closeRequested δεν είναι παρά μια private μεταβλητή της τρέχουσας κλάσης, γεγονός που την κάνει προσβάσιμη από κάθε function αυτής — καθώς και από κάθε thread.

H βασική function που κάνει την εργασία είναι η get_URL(url). Αυτή είναι που ελέγχει την ύπαρξη του αρχείου, αφού του “κολλήσει” και τον τύπο του αρχείου. Η συγκεκριμένη εργασία δεν πρέπει να διακοπεί ή/και να αναμειχθεί από/με άλλο thread. Γι’ αυτό το λόγο βάζουμε τον κώδικα ανάμεσα στο περίφημο lock(this), ώστε να εκτελεστεί ολόκληρο το τμήμα κώδικα και όλα τα άλλα threads να περιμένουν να τελειώσει.

Στο σημείο δημιουργίας threads παρατηρήστε ότι κάθε φορά που ξεκινάμε ένα νέο (new Thread) το καταχωρούμε σε μια λίστα με το όνομα MyThreads. Αυτό το κάνουμε ώστε κάθε φορά να ξέρουμε ποια threads έχουμε ενεργά κι αν ο χρήστης αποφασίσει να κλείσει το πρόγραμμα πριν ολοκληρωθεί η διαδικασία να μπορέσουμε να τα ακυρώσουμε με τον εξής κώδικα:

private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
	...
	// Navigate all active threads (if any) and force abort...
	foreach (Thread MyThread in MyThreads)
		MyThread.Abort();
}

Όπως βλέπετε, έχουμε σταθεί στο FormClosing event. Με αυτόν τον τρόπο, είτε κλείσει ο χρήστης την εφαρμογή μας από το κουμπί “Έξοδος”, είτε από το περίφημο [Χ] (πάνω δεξιά), είτε από το built-in μενού του παραθύρου (πάνω αριστερά), είτε πατώντας απλά [CTRL+F4], ο κώδικας θα εκτελεστεί.

Ακόμα μια αναφορά θεωρούμε ότι αξίζει τον κόπο: Στον κώδικα παραπάνω, καλείται η function AddString(s). Αυτή παίρνει ως παράμετρο μια σειρά χαρακτήρων (string) και στην περίπτωσή μας αναφέρεται σε ένα ενημερωτικό μήνυμα που θα εμφανιστεί στο κεντρικό component ενημέρωσης της εφαρμογής μας. Στην εικόνα 1 θα δείτε την αντίστοιχη ενημέρωση στην αναφορά “Starting thread…”. Αν πάμε στην αντίστοιχη συνάρτηση θα δούμε το ακόλουθο τμήμα κώδικα:

private void AddString(String s)
{
	richTextBoxOutput.Invoke(new Action(() => richTextBoxOutput.AppendText(s + Environment.NewLine)));
}

Αν έχετε δουλέψει με τα components του .NET, θα αναρωτηθείτε γιατί παίζουμε με το Invoke και δεν κάνουμε κατευθείαν ανάθεση, ως ακολούθως:

private void AddString(String s)
{
	richTextBoxOutput.AppendText(s + Environment.NewLine);
}

Χμ, τα πράγματα δεν είναι τόσο απλά εδώ. Όπως είπαμε στην αρχή, όταν ένα πρόγραμμα C# εκτελείται, μπαίνει κάτω από ένα νέο process και ξεκινάει ένα αρχικό thread που μέσα σε αυτό εκτός των άλλων θα “τρέξουν” όλα τα UI Components, δηλαδή ό,τι βλέπουμε στη φόρμα μας (labels, textboxes, checkboxes κ.λπ.). Αν τώρα εμείς φτιάξουμε νέα threads, τότε πρέπει να ξέρουμε ότι μέσα από αυτά δεν είναι δυνατόν να αναφερθούμε απ’ ευθείας σε ένα UI Component που τρέχει σε άλλο thread (το default), αφού το inter-thread communication δεν επιτρέπεται σε UI components. Για να μπορέσουμε, π.χ., να εμφανίσουμε ένα απλό μήνυμα στη φόρμα μας όταν βρισκόμαστε σε κάποιο διαφορετικό thread από το default, πρέπει να το κάνουμε με ειδικό χειρισμό. Εδώ έρχεται η έννοια των delegates. Το delegate γενικά αναφέρεται σε μια συνάρτηση (σε ένα reference προς συνάρτηση, για να μιλήσουμε με όρους .NET) η οποία (συνάρτηση) θα κληθεί όταν πραγματοποιηθεί ένα συγκεκριμένο event. Τα delegates χρησιμοποιούνται συχνά σε καταστάσεις inter-thread communication, όπως στην περίπτωσή μας όπου η συνάρτηση AddString ενδέχεται να κληθεί όχι μόνο μέσα από το default thread αλλά και μέσα από κάποιο άλλο thread. Υπάρχουν πολλοί τρόποι που μπορούμε να υλοποιήσουμε κάτι τέτοιο. Ο πιο απλός είναι να κάνουμε χρήση της default member function ονόματι invoke, την οποία παρέχει το .NET 4.5 για κάθε UI component (δείτε και την υλοποίηση της συνάρτησης addstring).

Χρήσιμο είναι να αναφέρουμε ότι στο πρόγραμμα έχουμε υλοποιήσει μια τεχνική validation κατά την οποία ενεργοποιούμε ένα component μόνον όταν έχουν συμπληρωθεί τα απαραίτητα πεδία και συνεπώς μπορεί να κληθεί, διαφορετικά παραμένει ανενεργό. Στην περίπτωσή μας, αν παρατηρήσετε το κουμπί “Εκτέλεση” όταν ξεκινά το πρόγραμμα, θα δείτε ότι είναι disabled. Μόλις γεμίζουμε τα απαραίτητα πεδία που απαιτούνται για να κληθεί (μόλις, δηλαδή, επιλέξουμε λεξικό, αρχικό κατάλογο, URL και τύπο αρχείου), τότε γίνεται enabled. Αν πάλι διαγράψουμε κάποιο από τα περιεχόμενα των απαραίτητων πεδίων, τότε ξαναγίνεται disabled. Αυτή η συμπεριφορά υλοποιείται σχετικά εύκολα, αν “καθήσουμε” επάνω στο TextChanged event κάθε απαραίτητου component. Για παράδειγμα, ο κώδικας για το textbox που περιέχει το όνομα του λεξικού είναι:

private void textBoxFile_TextChanged(object sender, EventArgs e)
{
	if (textBoxSite.Text.Length > 0 &&
		textBoxFile.Text.Length > 0 &&
		textBoxFileType.Text.Length > 0)
		btn_Ektelesh.Enabled = true;
	else
		btn_Ektelesh.Enabled = false;
}

Επίλογος
Παρουσιάσαμε μερικά προχωρημένα θέματα προγραμματισμού σε C#, κάτω από το περιβάλλον .NET 4.5 κι εφαρμόζοντάς τα σε ένα πρόγραμμα web crawling. Μαζί με τη σύντομη ανάλυσή μας σας δίνουμε και τον κώδικα του προγράμματος (http://bit.ly/dh042scandir2). Όπως πάντα, μπορείτε να το κάνετε ό,τι θέλετε, αρκεί να πειραματιστείτε με στόχο τη γνώση και να μην προκαλέσετε προβλήματα σε κάποιον χρήστη ή …server ;)

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

Όπως θα έχετε ήδη διαπιστώσει με την τέχνη του προγραμματισμού δεν παίρνουμε μία λύση για κάθε πιθανό πρόβλημα ή ανάγκη. Υπάρχουν πάρα πολλοί τρόποι για να υλοποιήσεις κάτι. Με το ScanDir v2.0 προτείνουμε έναν τρόπο — και δεν ξέρουμε αν είναι ο καλύτερος. Ίσως δεν μας ενδιαφέρει και πολύ. Το όριο είναι η φαντασία μας και το μεταφορικό μέσο το μεράκι μας — έτσι πρέπει να είναι. Προτείνουμε να μελετήσετε και να επεκτείνετε το πρόγραμμα, ώστε να καλύπτει κι άλλες ανάγκες ή να κάνει εντελώς διαφορετικά πράγματα από αυτά για τα οποία φτιάχτηκε.

Leave a Reply

You must be logged in to post a comment.

Σύνδεση

Αρχείο δημοσιεύσεων