2017-08-03 23 views
5

Tôi đang viết một con nhện để thu thập thông tin các trang web. Tôi biết asyncio có lẽ là lựa chọn tốt nhất của tôi. Vì vậy, tôi sử dụng coroutines để xử lý công việc không đồng bộ. Bây giờ tôi gãi đầu của tôi về làm thế nào để thoát khỏi chương trình bằng cách ngắt bàn phím. Chương trình có thể đóng cửa tốt sau khi tất cả các công trình đã được thực hiện. Mã nguồn có thể được chạy trong python 3.5 và được attatched dưới đây.Làm thế nào để duyên dáng tắt coroutines với 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() 

Nhưng nếu tôi nhấn 'Ctrl + C' trong khi đang chạy, một số lỗi lạ có thể xảy ra. Tôi có nghĩa là đôi khi chương trình có thể được tắt bởi 'Ctrl + C' một cách duyên dáng. Không có thông báo lỗi. Tuy nhiên, trong một số trường hợp, chương trình sẽ vẫn chạy sau khi nhấn 'Ctrl + C' và sẽ không dừng cho đến khi tất cả các công việc đã được thực hiện. Nếu tôi nhấn 'Ctrl + C' tại thời điểm đó, 'Tác vụ đã bị hủy nhưng đang chờ xử lý!' Sẽ ở đó.

Tôi đã đọc một số chủ đề về asyncio và thêm một số mã trong main() để đóng coroutines một cách duyên dáng. Nhưng nó không hoạt động. Có ai khác có vấn đề tương tự không?

Trả lời

3

Tôi đặt cược vấn đề xảy ra ở đây:

except: 
    pass 

Bạn should never do điều như vậy. Và tình huống của bạn là một ví dụ nữa về những gì có thể xảy ra nếu không.

Khi bạn hủy tác vụ và đang chờ hủy, asyncio.CancelledError được nâng lên bên trong công việc và shouldn't be bị chặn ở bất kỳ đâu bên trong. Dòng nơi bạn chờ đợi hủy công việc của mình sẽ tăng ngoại lệ này, nếu không tác vụ sẽ tiếp tục thực hiện.

Đó là lý do tại sao bạn làm

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

để thực sự hủy bỏ nhiệm vụ.

UPD:

Nhưng tôi vẫn hầu như không hiểu tại sao mã gốc có thể bỏ cũng bởi 'Ctrl + C' tại một xác suất bấp bênh?

Nó phụ thuộc của nhà nước các nhiệm vụ của bạn:

  1. Nếu tại thời điểm bạn bấm 'Ctrl + C' tất cả các nhiệm vụ được thực hiện, không của họ sẽ tăng CancelledError trên chờ và mã của bạn sẽ hoàn thành bình thường.
  2. Nếu tại thời điểm bạn nhấn 'Ctrl + C' một số tác vụ đang chờ xử lý, nhưng gần xong để thực thi, mã của bạn sẽ bị kẹt một chút khi hủy công việc và hoàn tất khi công việc được hoàn tất ngay sau đó.
  3. Nếu tại thời điểm bạn nhấn 'Ctrl + C' một số tác vụ đang chờ xử lý và sắp hết, mã của bạn sẽ bị kẹt cố gắng hủy các tác vụ này (không thể thực hiện ). Một 'Ctrl + C' khác sẽ làm gián đoạn quá trình hủy nhưng các tác vụ sẽ không bị hủy hoặc hoàn tất sau đó bạn sẽ nhận được cảnh báo 'Tác vụ đã bị hủy nhưng đang chờ xử lý!'.
+0

Tôi cho rằng bạn đã đúng. 'ngoại trừ: vượt qua' là trường hợp! Tôi thêm 'nâng cao' sau khi 'vượt qua' trong 'ngoại trừ:' và nó có thể thoát tốt bằng 'Ctrl + C'. Vì vậy, nếu tôi muốn đăng nhập các lỗi, tôi nên reraise trường hợp ngoại lệ để chính() có thể bắt những ngoại lệ bao gồm asyncio.CancelledError. Nhưng tôi vẫn khó hiểu tại sao mã gốc có thể thoát ra tốt bằng 'Ctrl + C' với xác suất không chắc chắn? Nếu cấu trúc 'try-except' trong fetch() có thể nắm bắt tất cả các ngoại lệ, main() sẽ capture không có gì, do đó lỗi sẽ xuất hiện mọi lúc. – xssl

+0

@xssl, tôi đã cập nhật câu trả lời để hiển thị những gì có thể xảy ra trong các trường hợp khác nhau. –

0

Tôi cho rằng bạn đang sử dụng bất kỳ hương vị nào của Unix; nếu điều này không đúng, nhận xét của tôi có thể không áp dụng cho trường hợp của bạn.

Nhấn Ctrl - C trong một thiết bị đầu cuối gửi tất cả các quá trình liên quan đến tty này tín hiệu SIGINT. Một quá trình Python bắt tín hiệu Unix này và chuyển nó thành ném một ngoại lệ KeyboardInterrupt. Trong một ứng dụng luồng (Tôi không chắc chắn nếu nội dung async nội bộ là sử dụng chủ đề, nhưng nó rất nhiều âm thanh như nó) thường chỉ có một sợi (chủ đề chính) nhận được tín hiệu này và do đó phản ứng trong thời trang này. Nếu nó không được chuẩn bị đặc biệt cho tình huống này, nó sẽ chấm dứt do ngoại lệ.

Sau đó, quản trị luồng sẽ đợi cho các chuỗi đồng nghiệp đang chạy để chấm dứt trước khi quá trình Unix kết thúc bằng mã thoát. Quá trình này có thể mất khá nhiều thời gian. Xem this question about killing fellow threads và tại sao điều này không thể nói chung.

Những gì bạn muốn làm, tôi cho rằng, là giết quá trình của bạn ngay lập tức, giết chết tất cả các chủ đề trong một bước.

Cách dễ nhất để đạt được điều này là nhấn Ctrl - \. Điều này sẽ gửi một số SIGQUIT thay vì SIGINT thường ảnh hưởng đến các chủ đề đồng nghiệp và khiến chúng chấm dứt.

Nếu đây là không đủ (bởi vì đối với bất cứ lý do bạn cần phải phản ứng đúng trên Ctrl - C), bạn có thể gửi cho mình một tín hiệu:

import os, signal 

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

này nên chấm dứt tất cả các chủ đề chạy trừ khi họ đặc biệt bắt được SIGQUIT trong trường hợp đó bạn vẫn có thể sử dụng SIGKILL để thực hiện một hành động giết người khó khăn trên chúng. Tuy nhiên, điều này không cung cấp cho họ bất kỳ tùy chọn phản ứng nào và có thể dẫn đến các sự cố.

Các vấn đề liên quan