Использование Java ThreadPool
Я пытаюсь написать многопоточный веб-краулер.
мой основной класс записи имеет следующий код:
ExecutorService exec = Executors.newFixedThreadPool(numberOfCrawlers);
while(true){
URL url = frontier.get();
if(url == null)
return;
exec.execute(new URLCrawler(this, url));
}
URLCrawler извлекает указанный URL, анализирует HTML извлекает ссылки из него и планирует невидимые ссылки обратно в frontier.
граница очередь uncrawled URL-адреса. Проблема в том, как написать метод get (). Если очередь пуста, следует дождаться завершения всех URLCrawlers, а затем повторить попытку. Он должен возвращать null только тогда, когда очередь пуста, и в настоящее время нет активного URLCrawler.
моей первой идеей было использовать AtomicInteger для подсчета текущего количества рабочих URLCrawlers и вспомогательный объект для вызовов notifyAll()/wait (). Каждый искатель при запуске увеличивает число текущих рабочих URLCrawlers, а при выходе уменьшает его и уведомляет объект, что он завершил.
но я читал, что notify()/notifyAll () и wait() несколько устаревшие методы для выполнения потока связь.
что я должен использовать в этой схеме работать? Это похоже на M производителей и N потребителей, вопрос в том, как бороться с exaustion производителей.
6 ответов
Я думаю, что использование wait / notify оправдано в этом случае. Не могу придумать никакого прямого способа сделать это с помощью j.u.c.
В классе, давайте вызовем координатора:
private final int numOfCrawlers;
private int waiting;
public boolean shouldTryAgain(){
synchronized(this){
waiting++;
if(waiting>=numOfCrawlers){
//Everybody is waiting, terminate
return false;
}else{
wait();//spurious wake up is okay
//waked up for whatever reason. Try again
waiting--;
return true;
}
}
public void hasEnqueued(){
synchronized(this){
notifyAll();
}
}
затем,
ExecutorService exec = Executors.newFixedThreadPool(numberOfCrawlers);
while(true){
URL url = frontier.get();
if(url == null){
if(!coordinator.shouldTryAgain()){
//all threads are waiting. No possibility of new jobs.
return;
}else{
//Possible that there are other jobs. Try again
continue;
}
}
exec.execute(new URLCrawler(this, url));
}//while(true)
Я не уверен, что понимаю ваш дизайн, но это может быть работа для Semaphore
один из вариантов-сделать " frontier "блокирующей очередью, поэтому любой поток, пытающийся" получить " от него, будет заблокирован. Как только любой другой URLCrawler помещает объекты в эту очередь, любые другие потоки будут автоматически уведомлены (с объектом dequeued)
Я думаю, что основной строительный блок для вашего случая использования-это "защелка", похожая на CountDownLatch, но в отличие от CountDownLatch, которая позволяет увеличение Граф также.
интерфейс для такой защелки может быть
public interface Latch {
public void countDown();
public void countUp();
public void await() throws InterruptedException;
public int getCount();
}
юридические значения для счетчиков будут равны 0 и выше. Метод await () позволит вам блокировать, пока счетчик не опустится до нуля.
Если у вас есть такая защелка, ваш случай может быть описан довольно легко. Я также подозреваю, очередь (граница) может быть устранена в этом решении (исполнитель предоставляет ее в любом случае, поэтому она несколько избыточна). Я бы переписал вашу основную процедуру как
ExecutorService executor = Executors.newFixedThreadPool(numberOfCrawlers);
Latch latch = ...; // instantiate a latch
URL[] initialUrls = ...;
for (URL url: initialUrls) {
executor.execute(new URLCrawler(this, url, latch));
}
// now wait for all crawling tasks to finish
latch.await();
ваш URLCrawler будет использовать защелку таким образом:
public class URLCrawler implements Runnable {
private final Latch latch;
public URLCrawler(..., Latch l) {
...
latch = l;
latch.countUp(); // increment the count as early as possible
}
public void run() {
try {
List<URL> secondaryUrls = crawl();
for (URL url: secondaryUrls) {
// submit new tasks directly
executor.execute(new URLCrawler(..., latch));
}
} finally {
// as a last step, decrement the count
latch.countDown();
}
}
}
что касается реализаций защелки, может быть несколько возможных реализаций, начиная от той, которая основана на wait() и notifyAll (), которая использует блокировку и условие, до реализации, которая использует AbstractQueuedSynchronizer. Все эти реализации, я думаю, будут довольно простыми. Обратите внимание, что версия wait()-notifyAll() и версия Lock-Condition будут основаны на взаимном исключении, в то время как версия AQS будет использовать CAS (compare-and-swap) и, таким образом, может масштабироваться лучше в определенных ситуациях.
вопрос немного старый, но я думаю, что нашел простое, рабочее решение:
расширьте класс ThreadPoolExecutor, как показано ниже. Новая функциональность сохраняет активное количество задач (к сожалению, при условии getActiveCount()
недостоверна). Если taskCount.get() == 0
и больше нет задач в очереди, это означает, что ничего не нужно делать, и исполнитель завершает работу. У вас есть критерии выхода. Кроме того, если вы создадите своего исполнителя, но не отправите никаких задач, он не будет блок:
public class CrawlingThreadPoolExecutor extends ThreadPoolExecutor {
private final AtomicInteger taskCount = new AtomicInteger();
public CrawlingThreadPoolExecutor() {
super(8, 8, 0, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
}
@Override
protected void beforeExecute(Thread t, Runnable r) {
super.beforeExecute(t, r);
taskCount.incrementAndGet();
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
taskCount.decrementAndGet();
if (getQueue().isEmpty() && taskCount.get() == 0) {
shutdown();
}
}
}
еще одна вещь, вы должны сделать, это реализовать Runnable
таким образом, он сохраняет ссылку на Executor
вы используете, чтобы иметь возможность отправлять новые задачи. Вот макет:
public class MockFetcher implements Runnable {
private final String url;
private final Executor e;
public MockFetcher(final Executor e, final String url) {
this.e = e;
this.url = url;
}
@Override
public void run() {
final List<String> newUrls = new ArrayList<>();
// Parse doc and build url list, and then:
for (final String newUrl : newUrls) {
e.execute(new MockFetcher(this.e, newUrl));
}
}
}
Я хотел бы предложить AdaptiveExecuter. На основе характерных значений, вы можете выбрать для сериализации или parallalize потока для выполнения. В приведенном ниже примере PUID-это строка / объект, который я хотел использовать для принятия этого решения. Вы можете изменить логику в соответствии с вашим кодом. Некоторые части кода комментируются, чтобы позволить дальнейшие эксперименты.
класс AdaptiveExecutor реализует исполнитель { конечные задачи очереди = new LinkedBlockingQueue(); Работоспособный активный ; //ExecutorService threadExecutor=исполнители.newCachedThreadPool(); статический ExecutorService threadExecutor=исполнители.newFixedThreadPool(4);
AdaptiveExecutor() {
System.out.println("Initial Queue Size=" + tasks.size());
}
public void execute(final Runnable r) {
/* if immediate start is needed do either of below two
new Thread(r).start();
try {
threadExecutor.execute(r);
} catch(RejectedExecutionException rEE ) {
System.out.println("Thread Rejected " + new Thread(r).getName());
}
*/
tasks.offer(r); // otherwise, queue them up
scheduleNext(new Thread(r)); // and kick next thread either serial or parallel.
/*
tasks.offer(new Runnable() {
public void run() {
try {
r.run();
} finally {
scheduleNext();
}
}
});
*/
if ((active == null)&& !tasks.isEmpty()) {
active = tasks.poll();
try {
threadExecutor.submit(active);
} catch (RejectedExecutionException rEE) {
System.out.println("Thread Rejected " + new Thread(r).getName());
}
}
/*
if ((active == null)&& !tasks.isEmpty()) {
scheduleNext();
} else tasks.offer(r);
*/
//tasks.offer(r);
//System.out.println("Queue Size=" + tasks.size());
}
private void serialize(Thread th) {
try {
Thread activeThread = new Thread(active);
th.wait(200);
threadExecutor.submit(th);
} catch (InterruptedException iEx) {
}
/*
active=tasks.poll();
System.out.println("active thread is " + active.toString() );
threadExecutor.execute(active);
*/
}
private void parallalize() {
if(null!=active)
threadExecutor.submit(active);
}
protected void scheduleNext(Thread r) {
//System.out.println("scheduleNext called") ;
if(false==compareKeys(r,new Thread(active)))
parallalize();
else serialize(r);
}
private boolean compareKeys(Thread r, Thread active) {
// TODO: obtain names of threads. If they contain same PUID, serialize them.
if(null==active)
return true; // first thread should be serialized
else return false; //rest all go parallel, unless logic controlls it
}
}