טיפ ג'אווה: מתי להשתמש ב- ForkJoinPool לעומת ExecutorService

ספריית Fork / Join שהוצגה בג'אווה 7 מרחיבה את חבילת ה- Java המקבילה הקיימת עם תמיכה במקביליות חומרה, תכונה מרכזית של מערכות מרובות ליבות. בטיפ זה של ג'אווה מדלין אילי מדגים את השפעת הביצועים של החלפת מחלקת Java 6 ExecutorServiceב- Java 7 ForkJoinPoolביישום סורק אינטרנט.

סורקי אינטרנט, המכונים גם עכבישים ברשת, הם המפתח להצלחה של מנועי החיפוש. תוכניות אלה סורקות ללא הרף את האינטרנט, אוספות מיליוני עמודי נתונים ומחזירות אותם למאגרי מידע של מנועי חיפוש. לאחר מכן הנתונים באינדקס ומעובדים באלגוריתם, וכתוצאה מכך תוצאות חיפוש מהירות ומדויקות יותר. אמנם הם משמשים המפורסמים ביותר לאופטימיזציה של חיפוש, אך ניתן להשתמש בסורקי אינטרנט למשימות אוטומטיות כגון אימות קישורים או מציאת והחזרת נתונים ספציפיים (כגון כתובות דוא"ל) באוסף של דפי אינטרנט.

מבחינה ארכיטקטונית, רוב סורקי האינטרנט הם תוכניות רב-הברגה בעלות ביצועים גבוהים, אם כי עם פונקציונליות ודרישות פשוטות יחסית. בניית סורק אתרים היא אפוא דרך מעניינת לתרגל, כמו גם להשוות טכניקות תכנות מרובות הברגה או במקביל.

החזרת טיפים של Java!

טיפים של Java הם מאמרים קצרים המונעים על ידי קוד, המזמינים את קוראי JavaWorld לשתף את כישורי התכנות והתגליות שלהם. ספר לנו אם יש לך טיפ לחלוק עם קהילת JavaWorld. בדוק גם בארכיון Java Tips לקבלת טיפים נוספים לתכנות מצד עמיתים שלך.

במאמר זה אעבור על שתי גישות לכתיבת סורק אינטרנט: האחת באמצעות Java 6 ExecutorService, והשנייה של ForkJoinPool של Java 7. על מנת לעקוב אחר הדוגמאות, יהיה עליך להתקין (נכון לכתיבת שורות אלה) עדכון 2 של Java 7 בסביבת הפיתוח שלך, כמו גם את ספריית הצד השלישי HtmlParser.

שתי גישות לקבילות Java

ExecutorServiceבכיתה היא חלק java.util.concurrentבמהפכה הציגה ב- Java 5 (וחלק Java 6, כמובן), אשר לפשט חוט-טיפול על פלטפורמת Java. ExecutorServiceהוא אקספקטור המספק שיטות לניהול מעקב אחר התקדמות וסיום משימות אסינכרוניות. לפני הצגתם java.util.concurrent, מפתחי Java הסתמכו על ספריות צד שלישי או כתבו שיעורים משלהם לניהול מקבילות בתוכניות שלהם.

Fork / Join, שהוצג ב- Java 7, אינו מיועד להחליף או להתחרות במחלקות השירות המקבילות הקיימות; במקום זאת הוא מעדכן ומשלים אותם. Fork / Join נותן מענה לצורך בחלוקה-וכיבוש, או בעיבוד משימות רקורסיביות בתוכניות Java (ראה משאבים).

ההיגיון של Fork / Join הוא פשוט מאוד: (1) הפרד (מזלג) כל משימה גדולה למשימות קטנות יותר; (2) עיבוד כל משימה בשרשור נפרד (הפרדת אותן למשימות קטנות עוד יותר במידת הצורך); (3) הצטרף לתוצאות.

שני היישומים של סורק האינטרנט הבאים הם תוכניות פשוטות המדגימות את התכונות והפונקציונליות של Java 6 ExecutorServiceו- Java 7 ForkJoinPool.

בנייה ומידוד של סורק האינטרנט

המשימה של סורק האינטרנט שלנו תהיה למצוא קישורים ולעקוב אחריהם. מטרתו יכולה להיות אימות קישורים, או איסוף נתונים. (אתה יכול, למשל, להורות לתוכנית לחפש באינטרנט תמונות של אנג'לינה ג'ולי, או בראד פיט).

ארכיטקטורת היישומים כוללת:

  1. ממשק החושף פעולות בסיסיות לאינטראקציה עם קישורים; כלומר, קבל את מספר הקישורים שביקרת, הוסף קישורים חדשים לביקור בתור, סמן קישור כביקור
  2. יישום לממשק זה שיהווה גם את נקודת המוצא של היישום
  3. שרשור / פעולה רקורסיבית שתחזיק את ההיגיון העסקי כדי לבדוק האם כבר ביקרו בקישור. אם לא, הוא יאסוף את כל הקישורים בדף המקביל, ייצור שרשור חדש / משימה רקורסיבית ויגיש אותו ל- ExecutorServiceאוForkJoinPool
  4. ExecutorServiceאו ForkJoinPoolלטפל במשימות מחכות

שים לב שקישור נחשב ל"ביקור "לאחר שהוחזרו כל הקישורים בדף המקביל.

בנוסף להשוואה של קלות הפיתוח באמצעות כלים מקבילים הקיימים ב- Java 6 ו- Java 7, נשווה את ביצועי היישומים על סמך שני מדדים:

  • כיסוי חיפוש : מודד את הזמן הדרוש לביקור ב -1,500 קישורים נפרדים
  • כוח עיבוד : מודד את הזמן בשניות הנדרש לביקור ב -3,000 קישורים שאינם מובחנים ; זה כמו למדוד כמה קילו ביט לשנייה שעובד חיבור האינטרנט שלך.

למרות שהם יחסית פשוטים, אמות המידה הללו יספקו לפחות חלון קטן לביצועים של Java במקביל ב- Java 6 לעומת Java 7 עבור דרישות יישום מסוימות.

סורק אינטרנט של Java 6 שנבנה עם ExecutorService

לצורך יישום סורק האינטרנט של Java 6 נשתמש במאגר פתילים קבוע של 64 אשכולות, אותו אנו יוצרים על ידי קריאת Executors.newFixedThreadPool(int)שיטת המפעל. רישום 1 מציג את יישום הכיתה הראשית.

רישום 1. בניית WebCrawler

package insidecoding.webcrawler; import java.util.Collection; import java.util.Collections; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import insidecoding.webcrawler.net.LinkFinder; import java.util.HashSet; /** * * @author Madalin Ilie */ public class WebCrawler6 implements LinkHandler { private final Collection visitedLinks = Collections.synchronizedSet(new HashSet()); // private final Collection visitedLinks = Collections.synchronizedList(new ArrayList()); private String url; private ExecutorService execService; public WebCrawler6(String startingURL, int maxThreads) { this.url = startingURL; execService = Executors.newFixedThreadPool(maxThreads); } @Override public void queueLink(String link) throws Exception { startNewThread(link); } @Override public int size() { return visitedLinks.size(); } @Override public void addVisited(String s) { visitedLinks.add(s); } @Override public boolean visited(String s) { return visitedLinks.contains(s); } private void startNewThread(String link) throws Exception { execService.execute(new LinkFinder(link, this)); } private void startCrawling() throws Exception { startNewThread(this.url); } /** * @param args the command line arguments */ public static void main(String[] args) throws Exception { new WebCrawler("//www.javaworld.com", 64).startCrawling(); } }

בבנאי הנ"ל WebCrawler6, אנו יוצרים מאגר פתילים בגודל קבוע של 64 אשכולות. לאחר מכן אנו מתחילים את התוכנית בקריאה startCrawlingלשיטה, היוצרת את השרשור הראשון ומגישה אותו ל- ExecutorService.

לאחר מכן, אנו יוצרים LinkHandlerממשק החושף שיטות עוזרות לאינטראקציה עם כתובות אתרים. הדרישות הן כדלקמן: (1) סמן כתובת אתר כביקרה addVisited()בשיטה; (2) קבל את מספר כתובות האתרים שבהם ביקרת באמצעות size()השיטה; (3) לקבוע אם כבר ביקרו בכתובת אתר visited()בשיטה; ו- (4) להוסיף כתובת URL חדשה בתור באמצעות queueLink()השיטה.

רישום 2. ממשק LinkHandler

package insidecoding.webcrawler; /** * * @author Madalin Ilie */ public interface LinkHandler { /** * Places the link in the queue * @param link * @throws Exception */ void queueLink(String link) throws Exception; /** * Returns the number of visited links * @return */ int size(); /** * Checks if the link was already visited * @param link * @return */ boolean visited(String link); /** * Marks this link as visited * @param link */ void addVisited(String link); }

כעת, כשאנחנו סורקים עמודים, עלינו להפעיל את שאר הנושאים, מה שאנו עושים באמצעות LinkFinderהממשק, כפי שמוצג ברשימה 3. שימו לב linkHandler.queueLink(l)לשורה.

רישום 3. LinkFinder

package insidecoding.webcrawler.net; import java.net.URL; import org.htmlparser.Parser; import org.htmlparser.filters.NodeClassFilter; import org.htmlparser.tags.LinkTag; import org.htmlparser.util.NodeList; import insidecoding.webcrawler.LinkHandler; /** * * @author Madalin Ilie */ public class LinkFinder implements Runnable { private String url; private LinkHandler linkHandler; /** * Used fot statistics */ private static final long t0 = System.nanoTime(); public LinkFinder(String url, LinkHandler handler) { this.url = url; this.linkHandler = handler; } @Override public void run() { getSimpleLinks(url); } private void getSimpleLinks(String url) { //if not already visited if (!linkHandler.visited(url)) { try { URL uriLink = new URL(url); Parser parser = new Parser(uriLink.openConnection()); NodeList list = parser.extractAllNodesThatMatch(new NodeClassFilter(LinkTag.class)); List urls = new ArrayList(); for (int i = 0; i < list.size(); i++) { LinkTag extracted = (LinkTag) list.elementAt(i); if (!extracted.getLink().isEmpty() && !linkHandler.visited(extracted.getLink())) { urls.add(extracted.getLink()); } } //we visited this url linkHandler.addVisited(url); if (linkHandler.size() == 1500) { System.out.println("Time to visit 1500 distinct links = " + (System.nanoTime() - t0)); } for (String l : urls) { linkHandler.queueLink(l); } } catch (Exception e) { //ignore all errors for now } } } }

ההיגיון של LinkFinderפשוט: (1) אנו מתחילים לנתח URL; (2) לאחר שנאסוף את כל הקישורים בתוך העמוד המתאים, אנו מסמנים את הדף כביקור; ו- (3) אנו שולחים כל קישור שנמצא לתור על ידי קריאת queueLink()השיטה. שיטה זו תיצור למעשה שרשור חדש ותשלח אותו אל ה- ExecutorService. אם קיימים אשכולות "בחינם" בבריכה, השרשור יבוצע; אחרת הוא יוצב בתור המתנה. לאחר שנגיע ל -1,500 קישורים נפרדים, אנו מדפיסים את הנתונים הסטטיסטיים והתוכנית ממשיכה לפעול.

סורק אינטרנט של Java 7 עם ForkJoinPool

מסגרת Fork / Join שהוצגה בג'אווה 7 היא למעשה יישום של אלגוריתם Divide and Conquer (ראה משאבים), שבו מרכז ForkJoinPoolמבצע סניפים ForkJoinTask. לדוגמא זו נשתמש ב- ForkJoinPool"מגובה" על ידי 64 אשכולות. אני אומר מגובה כי ForkJoinTaskהם קלים יותר מחוטים. ב- Fork / Join, ניתן לארח מספר גדול של משימות על ידי מספר קטן יותר של שרשורים.

בדומה להטמעה של Java 6, אנו מתחילים בהפעלת WebCrawler7קונסטרוקטור ForkJoinPoolלאובייקט המגובה ב -64 אשכולות.

רישום 4. יישום Java 7 LinkHandler

package insidecoding.webcrawler7; import java.util.Collection; import java.util.Collections; import java.util.concurrent.ForkJoinPool; import insidecoding.webcrawler7.net.LinkFinderAction; import java.util.HashSet; /** * * @author Madalin Ilie */ public class WebCrawler7 implements LinkHandler { private final Collection visitedLinks = Collections.synchronizedSet(new HashSet()); // private final Collection visitedLinks = Collections.synchronizedList(new ArrayList()); private String url; private ForkJoinPool mainPool; public WebCrawler7(String startingURL, int maxThreads) { this.url = startingURL; mainPool = new ForkJoinPool(maxThreads); } private void startCrawling() { mainPool.invoke(new LinkFinderAction(this.url, this)); } @Override public int size() { return visitedLinks.size(); } @Override public void addVisited(String s) { visitedLinks.add(s); } @Override public boolean visited(String s) { return visitedLinks.contains(s); } /** * @param args the command line arguments */ public static void main(String[] args) throws Exception { new WebCrawler7("//www.javaworld.com", 64).startCrawling(); } }

שים לב LinkHandlerשהממשק ברישום 4 כמעט זהה ליישום Java 6 מרישום 2. זה חסר רק את queueLink()השיטה. השיטות החשובות ביותר להסתכל הן הקונסטרוקטור startCrawling()והשיטה. בבנאי, אנו יוצרים חדש ForkJoinPoolבגיבוי של 64 אשכולות. (בחרתי 64 אשכולות במקום 50 או מספר עגול אחר, מכיוון ForkJoinPoolשבג'אוודוק נאמר שמספר האשכולות חייב להיות כוח של שניים.) הבריכה קוראת לחדשה LinkFinderAction, שתפעיל עוד באופן רקורסיבי ForkJoinTasks. רישום 5 מראה את LinkFinderActionהכיתה: