1. 程式人生 > 其它 >力扣300、最長上升子序列動態規劃刷題

力扣300、最長上升子序列動態規劃刷題

文章Python多執行緒與多程序中介紹了並行,併發,多執行緒和多程序的概念。多執行緒 / 多程序是解決併發問題的模型之一,本文要介紹的協程也是實現併發程式設計的一種方式。

協程使用的非同步IO (asynchronous IO)不是多執行緒或者多程序的,它是一種單執行緒、單程序的設計。也就是說,協程可以實現併發排程,但它本身並不是併發的(單執行緒下的”併發“)。相比多執行緒和多程序,協程沒有程序上下文切換導致的資源佔用,執行效率更高。

目錄

最開始伺服器併發使用的是多執行緒 / 多程序的方式,隨著網際網路的快速發展,網路使用者數大量增長,遇到了C10K 瓶頸,也就是同時連線到伺服器的客戶端數量超過 10000 個,導致伺服器無法提供正常服務,解決這個問題的其中一個方案就是非同步程式設計。NGINX 提出了事件迴圈

,通過啟動一個統一的排程器,讓排程器來決定一個時刻去執行哪個任務,於是省去了多執行緒中啟動執行緒、管理執行緒、同步鎖等各種開銷。Node.js中使用 async / await 解決回撥地獄(callback hell)問題。

Python 2 使用生成器實現協程,Python2.5 中,使用yield 關鍵字使生成器有了記憶功能,Python 3.7 提供了新的基於 asyncio 和 async / await 的方法。除了Python,協程也在其它語言中得到實現,比如 golang 的 goroutine,luajit 的 coroutine,scala 的 actor 等,本文主要介紹Python中協程的使用方法。

協程(Coroutine)允許執行被掛起與被恢復,在執行任務(task)A時可以隨時中斷去執行任務B,通過排程器來進行任務自由切換,這一整個過程中只有一個執行緒在執行。協程是協作式多工的的輕量級執行緒,協程之間的切換不需要涉及任何系統呼叫或任何阻塞呼叫。

在IO密集型的多執行緒實現中,如果I/O 操作非常頻繁,多執行緒會進行頻繁的執行緒切換,並且執行緒數不能無限增加,所以使用協程非常好的方法。python 協程可以使用asyncio 模組實現,下面先來介紹asyncio。

Asyncio

先來區分一下 Sync(同步)和 Async(非同步)的概念。

  • 同步指操作一個接一個地執行,下一個操作必須等上一個操作完成後才能執行。
  • 非同步指不同操作間可以相互交替執行,如果其中的某個操作被 block 了,程式並不會等待,而是會找出可執行的操作繼續執行。

Asyncio 是單執行緒的,它只有一個主執行緒,但是可以進行多個不同的任務(task),這裡的任務,就是特殊的 future 物件。這些任務被一個叫做 event loop 的物件所控制,event loop 物件控制任務的交替執行,直到所有任務完成,可以把這裡的任務類比成多執行緒裡的多個執行緒。

在Python 3.7 以上版本中,可以使用asyncio庫來實現協程,可參考官方文件:https://docs.python.org/3/library/asyncio-eventloop.html,下面看一個協程例子:

import asyncio
import time

async def worker_1():
    print('worker_1 start')
    await asyncio.sleep(2)
    print('worker_1 done')

async def worker_2():
    print('worker_2 start')
    await asyncio.sleep(1)
    print('worker_2 done')

async def main():
    task1 = asyncio.create_task(worker_1())
    task2 = asyncio.create_task(worker_2())
    tasks = [task1,task2]
    print('before await')
    await asyncio.gather(*tasks)
    # for task in tasks:
    #     await task
    #     print(task._state)
    
start = time.time()
asyncio.run(main())
end = time.time()
print('Running time: %s Seconds'%(end-start))

先來介紹一下程式碼中使用到的魔法工具:

  • async 修飾詞將main,worker_1,worker_2方法宣告為非同步函式,當呼叫非同步函式時,會返回一個協程物件(coroutine object):

    <coroutine object worker_1 at 0x000002A65D14EC48>
    
  • await:同步呼叫,阻塞程式,執行對應的協程函式。await asyncio.sleep(5)表示程式暫停等待5s,await worker_1() 則會執行 worker_1() 函式,當前的呼叫執行結束後才觸發下一次呼叫。

  • async 和 await 關鍵字一般組合使用,如果任務執行的過程需要等待,則將其放入等待狀態的列表中,然後繼續執行預備狀態列表裡的任務。

  • asyncio.create_task():建立任務,任務建立後就會被排程執行,進入事件迴圈等待執行。使用這種方式建立任務後,就不會出現阻塞。

  • await asyncio.gather(*tasks, return_exception=False):執行tasks序列的所有任務,等待所有任務都結束才結束主程式,單星號*解包任務列表,也可以這樣寫:

    for task in tasks:
       await task
    
  • asyncio.run:執行,執行時拿到 event loop物件,執行完成後關閉,這是Python3.7+引入的方法。以前的版本可以使用如下方式:

    loop = asyncio.get_event_loop()
    try:
        loop.run_until_complete(asyncio.wait(tasks))
    finally:
        loop.close()
    

執行一下程式碼,執行結果:

before await
worker_1 start
worker_2 start
worker_2 done
worker_1 done
Running time: 2.0120482444763184 Seconds

執行流程如下:

  1. asyncio.run(main()),事件迴圈開啟
  2. asyncio.create_task()建立任務task1 和 task2 ,進入事件迴圈等待執行,列印“before await”。
  3. await task1 執行,事件排程器開始排程 worker_1。
  4. worker_1 開始執行,執行到 await asyncio.sleep(2), 從當前任務切出,事件排程器開始排程 worker_2。
  5. worker_2 開始執行,執行到 await asyncio.sleep(1) ,從當前任務切出。
  6. 1s後,worker_2 的 sleep 完成,事件排程器將控制權重新傳給 task_2,輸出 'worker_2 done',task_2 完成任務,從事件迴圈中退出。
  7. 事件排程器在 await task1 處繼續等待
  8. 2s後,worker_1 的 sleep 完成,事件排程器將控制權重新傳給 task_1,task_1 完成任務,從事件迴圈中退出;
  9. 協程所有任務結束,事件迴圈結束。

到這裡,想必你已經知道協程的概念和asyncio的使用方法了,下面來實現一個使用協程爬蟲的程式。

協程爬蟲

爬蟲是一個比較典型的I/O密集型任務,除了使用多執行緒實現外,也可以用協程來實現。實際上執行緒能實現的,協程也都能做到。

下面使用協程來實現抓取部落格https://hiyongz.github.io/上的所有文章,獲取部落格名稱、釋出時間和字數。

單執行緒版本:

import time

import requests
from bs4 import BeautifulSoup

def main():
    baseurl = "https://hiyongz.github.io"
    header = {
        'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Safari/537.36'
    }
    # init_page = requests.get(url).content
    init_page = requests.get(url=baseurl, headers=header).content
    init_soup = BeautifulSoup(init_page, 'lxml')

    # 獲取文章頁數
    nav_tag = init_soup.find('nav', class_="pagination")
    page_number_tag = nav_tag.find_all('a', class_="page-number")
    page_number = int(page_number_tag[1].text)
    article_num = 0
    for num in range(page_number):
        if num >=1:
            url = baseurl + f'/page/{num+1}/'
        else:
            url = baseurl

        init_page = requests.get(url=url, headers=header).content
        init_soup = BeautifulSoup(init_page, 'lxml')
        all_articles = init_soup.find('div', class_="content index posts-expand")
        for each_article in all_articles.find_all('header', class_="post-header"):
            all_a_tag = each_article.find_all('a')

            article_name = all_a_tag[0].text
            article_url = all_a_tag[0].attrs['href']

            response_item = requests.get(url=baseurl+article_url, headers=header).content
            soup_item = BeautifulSoup(response_item, 'lxml')
            time_tag = soup_item.find('time')
            publish_time = time_tag.text
            word_tag = each_article.find_all(title="本文字數")
            word_count = word_tag[0].text
            word_count = word_count.strip().split('\n')[1]
            article_num = article_num + 1
            print(f'{article_name} {baseurl+article_url} {publish_time} {word_count}')

    print(f'一共有{article_num}篇部落格文章')

start = time.time()
main()
end = time.time()
print('Running time: %s Seconds'%(end-start))

執行結果(部分):

markdown基本語法介紹 https://hiyongz.github.io/posts/markdown-basic-syntax/ 2021-06-12 6.8k
Python中的閉包 https://hiyongz.github.io/posts/python-notes-for-function-closures/ 2021-06-10 2.4k
演算法筆記:位運算 https://hiyongz.github.io/posts/algorithm-notes-for-bitwise-operation/ 2021-06-08 2.8k
常見搜尋演算法(二):二分查詢 https://hiyongz.github.io/posts/algorithm-notes-for-binary-search/ 2021-06-03 1.1k
.............
一共有124篇部落格文章
Running time: 107.27503871917725 Seconds

使用協程(由於requests 庫不相容 asyncio, 下面使用aiohttp 庫進行介面請求):

import time

import requests
from bs4 import BeautifulSoup
import asyncio
import aiohttp


async def fetch_content(url, header):
    async with aiohttp.ClientSession(
            headers=header, connector=aiohttp.TCPConnector(ssl=False)
    ) as session:
        async with session.get(url) as response:
            return await response.text()

async def main():
    baseurl = "https://hiyongz.github.io"
    header = {
        'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Safari/537.36'
    }
    article_names, article_urls,publishs_time,words_count = [], [], [], []
    init_page = requests.get(url=baseurl, headers=header).content
    init_soup = BeautifulSoup(init_page, 'lxml')
    # 獲取文章頁數
    nav_tag = init_soup.find('nav', class_="pagination")
    page_number_tag = nav_tag.find_all('a', class_="page-number")
    page_number = int(page_number_tag[1].text)
    for num in range(page_number):
        if num >= 1:
            url = baseurl + f'/page/{num+1}/'
        else:
            url = baseurl
        # article_names, article_urls, publishs_time, words_count = [], [], [], []
        init_page = requests.get(url=url, headers=header).content
        init_soup = BeautifulSoup(init_page, 'lxml')
        all_articles = init_soup.find('div', class_="content index posts-expand")

        for each_article in all_articles.find_all('header', class_="post-header"):
            all_a_tag = each_article.find_all('a')
            article_name = all_a_tag[0].text
            article_url = all_a_tag[0].attrs['href']

            article_names.append(article_name)
            article_urls.append(baseurl+article_url)

    tasks = [fetch_content(url, header) for url in article_urls]
    article_num = len(article_urls)
    pages = await asyncio.gather(*tasks)

    for article_name, article_url, page in zip(article_names, article_urls, pages):
        soup_item = BeautifulSoup(page, 'lxml')
        time_tag = soup_item.find('time')
        publish_time = time_tag.text
        word_tag = soup_item.find_all(title="本文字數")
        word_count = word_tag[0].text
        word_count = word_count.strip().split('\n')[1]
        print('{} {} {} {}'.format(article_name, article_url,publish_time,word_count))
    print(f'一共有{article_num}篇部落格文章')

start=time.time()
asyncio.run(main())
end=time.time()
print('Running time: %s Seconds'%(end-start))

執行結果(部分):

一共有124篇部落格文章
Running time: 14.071799755096436 Seconds

可以看到速度提升了很多。

多執行緒、多程序和協程如何選擇

Python多執行緒與多程序中介紹了多執行緒和多程序,它們都有各自的應用場景,在實際應用中,如何選擇呢?

  • I/O 密集型任務,並且 I/O 操作很慢,需要很多工協同實現,使用協程。
  • I/O 密集型任務,但是 I/O 操作很快,只需要有限數量的任務/執行緒,使用多執行緒就可以,當然也可以使用協程。
  • CPU 密集型任務,使用多程序。

總結

本文主要介紹了協程的概念以及python中協程的實現方法,注意asyncio 是單執行緒的,通過內部 event loop 機制實現併發地執行多個不同的任務,從而實現併發的效果。還要注意的就是asyncio比多執行緒有更大的自主控制權,你需要知道程式在什麼時候需要暫停、等待 I/O,在使用協程時要注意。

在I/O 操作多且慢的情況下使用協程比多執行緒效率更高,因為 Asyncio 內部任務切換遠比執行緒切換的資源損耗要小;並且 Asyncio 可以開啟的任務數量也比多執行緒多。

--THE END--

歡迎關注公眾號:「測試開發小記」及時接收最新技術文章!