2017-08-03 61 views
5

Piszę pająka, aby indeksować strony internetowe. Wiem, że asyncio może mój najlepszy wybór. Używam więc coroutines do asynchronicznego przetwarzania pracy. Teraz podrapuję się, jak wyjść z programu przez przerwanie klawiatury. Program może zamknąć się dobrze po wykonaniu wszystkich prac. Kod źródłowy może być uruchamiany w pythonie 3.5 i jest podany poniżej.Jak zgrabnie zamknąć procesory za pomocą Ctrl + C?

import asyncio 
import aiohttp 
from contextlib import suppress 

class Spider(object): 
    def __init__(self): 
     self.max_tasks = 2 
     self.task_queue = asyncio.Queue(self.max_tasks) 
     self.loop = asyncio.get_event_loop() 
     self.counter = 1 

    def close(self): 
     for w in self.workers: 
      w.cancel() 

    async def fetch(self, url): 
     try: 
      async with aiohttp.ClientSession(loop = self.loop) as self.session: 
       with aiohttp.Timeout(30, loop = self.session.loop): 
        async with self.session.get(url) as resp: 
         print('get response from url: %s' % url) 
     except: 
      pass 
     finally: 
      pass 

    async def work(self): 
     while True: 
      url = await self.task_queue.get() 
      await self.fetch(url) 
      self.task_queue.task_done() 

    def assign_work(self): 
     print('[*]assigning work...') 
     url = 'https://www.python.org/' 
     if self.counter > 10: 
      return 'done' 
     for _ in range(self.max_tasks): 
      self.counter += 1 
      self.task_queue.put_nowait(url) 

    async def crawl(self): 
     self.workers = [self.loop.create_task(self.work()) for _ in range(self.max_tasks)] 
     while True: 
      if self.assign_work() == 'done': 
       break 
      await self.task_queue.join() 
     self.close() 

def main(): 
    loop = asyncio.get_event_loop() 
    spider = Spider() 
    try: 
     loop.run_until_complete(spider.crawl()) 
    except KeyboardInterrupt: 
     print ('Interrupt from keyboard') 
     spider.close() 
     pending = asyncio.Task.all_tasks() 
     for w in pending: 
      w.cancel() 
      with suppress(asyncio.CancelledError): 
       loop.run_until_complete(w) 
    finally: 
     loop.stop() 
     loop.run_forever() 
     loop.close() 

if __name__ == '__main__': 
    main() 

Ale jeśli nacisnąć „Ctrl + C”, podczas gdy jest uruchomiony, może wystąpić jakieś dziwne błędy. Chodzi mi o to, że czasami program można zamknąć z wdziękiem za pomocą "Ctrl + C". Brak komunikatu o błędzie. Jednak w niektórych przypadkach program będzie nadal działał po naciśnięciu klawisza "Ctrl + C" i nie zostanie zatrzymany, dopóki wszystkie prace nie zostaną wykonane. Jeśli w tym momencie naciśnie "Ctrl + C", "Zadanie zostało zniszczone, ale jest w toku!" będzie tam.

Przeczytałem kilka tematów o asyncio i dodałem trochę kodu w main(), aby z wdziękiem zamknąć blogi. Ale to nie działa. Czy ktoś ma podobne problemy?

Odpowiedz

3

Założę problem się tutaj:

except: 
    pass 

Ty should never do takiego. Twoja sytuacja jest jeszcze jednym przykładem tego, co może się stać inaczej.

Po anulowaniu zadania i oczekiwaniu na jego anulowanie, asyncio.CancelledError podniesione wewnątrz zadania i shouldn't be ukryte w dowolnym miejscu w środku. Wiersz, w którym oczekujesz na anulowanie zadania, powinien podnieść ten wyjątek, w przeciwnym razie zadanie będzie kontynuowane.

Dlatego robisz

task.cancel() 
with suppress(asyncio.CancelledError): 
    loop.run_until_complete(task) # this line should raise CancelledError, 
            # otherwise task will continue 

faktycznie anulować zadanie.

Upd:

Ale nadal trudno zrozumieć, dlaczego oryginalny kod może rzucić dobrze przez 'Ctrl + C' w niepewnej prawdopodobieństwa?

To uzależnienie od stanu swoich zadań:

  1. Jeśli w chwili naciśnięcia „Ctrl + C” wszystkie zadania zostały wykonane, non je podniesie CancelledError na oczekiwaniu i kod zostanie zakończona normalnie.
  2. Jeśli w tym momencie naciśniesz "Ctrl + C" niektóre zadania oczekują, ale blisko, aby zakończyć ich wykonywanie, twój kod utknie trochę po anulowaniu zadań i zakończy się, gdy zadania zostaną zakończone wkrótce po nim.
  3. Jeśli w tym momencie naciśniesz "Ctrl + C" niektóre zadania oczekują, a dalsza od zakończenia, twój kod utknie, próbując anulować te zadania (których nie można wykonać za pomocą ). Inny "Ctrl + C" przerwie proces anulowania , ale zadania nie zostaną anulowane lub zakończone, a otrzymasz ostrzeżenie "Zadanie zostało zniszczone, ale oczekuje!".
+0

Przypuszczam, że masz rację. "except: pass" jest przypadkiem! Dodaję "raise" po "pass" w "except:" i mogłoby wyjść dobrze z "Ctrl + C". Więc jeśli chcę rejestrować błędy, powinienem przejąć wyjątki, aby main() mógł złapać te wyjątki, w tym asyncio.CancelledError. Ale wciąż nie rozumiem, dlaczego oryginalny kod mógł wyjść dobrze z "Ctrl + C" z niepewnym prawdopodobieństwem? Jeśli struktura "try-except" w fetch() może przechwycić wszystkie wyjątki, main() nic nie zrobi, w konsekwencji błąd wystąpi za każdym razem. – xssl

+0

@xssl, zaktualizowałem odpowiedź, aby pokazać, co może się zdarzyć w różnych przypadkach. –

0

Zakładam, że używasz dowolnego smaku Uniksa; jeśli tak nie jest, moje komentarze mogą nie odnosić się do twojej sytuacji.

naciśnięcie klawiszy Ctrl - C w terminalu wysyła wszystkie procesy związane z tym terminalem sygnał SIGINT. Proces Pythona przechwytuje ten sygnał uniksowy i tłumaczy go na wyjątek zrzucania KeyboardInterrupt. W aplikacji z gwintem (nie jestem pewien, czy wewnętrznie używają wątków, ale brzmi to tak, jakby to robiono), zazwyczaj tylko jeden wątek (główny wątek) odbiera ten sygnał, a zatem reaguje w ten sposób. Jeśli nie jest przygotowany specjalnie do tej sytuacji, zakończy się z powodu wyjątku.

Następnie administracja wątków będzie czekała na zakończenie działających wątków, zanim proces Unix jako całość zakończy się kodem wyjścia. Może to zająć dość dużo czasu. Zobacz this question about killing fellow threads i dlaczego nie jest to ogólnie możliwe.

To, co chcesz zrobić, jak przypuszczam, to natychmiastowe zabicie twojego procesu, zabicie wszystkich wątków w jednym kroku.

Najprostszym sposobem osiągnięcia tego celu jest naciśnij Ctrl- \. Spowoduje to wysłanie SIGQUIT zamiast SIGINT, która zazwyczaj wpływa również na inne wątki i powoduje ich zakończenie.

Jeśli to nie wystarczy (bo z jakiegoś powodu trzeba odpowiednio reagować na Ctrl - C), można wysłać do siebie sygnał:

import os, signal 

os.kill(os.getpid(), signal.SIGQUIT) 

To powinno rozwiązać wszystkie wątki uruchomione chyba że oni szczególnie łapią SIGQUIT, w którym to przypadku nadal możesz użyć SIGKILL do wykonania twardego zabicia na nich. Nie daje to jednak żadnej możliwości reagowania i może prowadzić do problemów.