Khi nói đến đồng thời so với song song, có thể thấy rõ ràng vì chúng đề cập đến cùng một khái niệm trong quá trình thực thi các chương trình máy tính trong môi trường đa luồng. Vâng, sau khi xem định nghĩa của chúng trong từ điển Oxford, bạn có thể có xu hướng nghĩ như vậy. Tuy nhiên, khi bạn đi sâu hơn vào các khái niệm này liên quan đến
Khi nói đến đồng thời so với song song, có thể thấy rõ ràng vì chúng đề cập đến cùng một khái niệm trong quá trình thực thi chương trình máy tính trong môi trường đa luồng. Vâng, sau khi xem định nghĩa của chúng trong từ điển Oxford, bạn có thể có xu hướng nghĩ như vậy. Tuy nhiên, khi bạn đi sâu hơn vào các khái niệm này liên quan đến cách CPU thực thi các lệnh chương trình, bạn sẽ nhận thấy rằng đồng thời và song song là hai khái niệm riêng biệt.
Bài viết này đi sâu hơn vào tính đồng thời và tính song song, cách chúng thay đổi và cách chúng hoạt động cùng nhau để cải thiện năng suất thực thi chương trình. Cuối cùng, bài viết sẽ thảo luận về hai chiến lược nào phù hợp nhất để thu thập dữ liệu web. Vậy chúng ta hãy bắt đầu.
Trước tiên, để đơn giản hóa vấn đề, chúng ta sẽ bắt đầu với đồng thời trong một ứng dụng duy nhất được thực thi trong một bộ xử lý duy nhất. Dictionary.com định nghĩa đồng thời là một hành động hoặc nỗ lực kết hợp và sự xuất hiện của các sự kiện đồng thời. Tuy nhiên, người ta có thể nói như vậy về thực thi song song khi các thực thi trùng nhau, và do đó định nghĩa này có phần gây hiểu lầm trong thế giới lập trình máy tính.
Trong cuộc sống hàng ngày, bạn sẽ có các lệnh thực thi đồng thời trên máy tính của mình. Ví dụ, bạn có thể đọc một bài viết trên blog trên trình duyệt trong khi nghe nhạc trên Windows Media Player. Sẽ có một quy trình khác đang chạy: tải xuống tệp PDF từ một trang web khác—tất cả các ví dụ này là các quy trình riêng biệt.
Trước khi phát minh ra các ứng dụng thực thi đồng thời, CPU thực hiện tuần tự các chương trình. Điều này ngụ ý rằng các lệnh của một chương trình phải hoàn tất thực thi trước khi CPU chuyển sang chương trình tiếp theo.
Ngược lại, thực thi đồng thời xen kẽ mỗi quy trình một chút cho đến khi tất cả hoàn tất.
Trong môi trường thực thi đa luồng bộ xử lý đơn, một chương trình thực thi khi chương trình khác bị chặn đối với đầu vào của người dùng. Bây giờ bạn có thể hỏi môi trường đa luồng là gì. Đó là một tập hợp các luồng chạy độc lập với nhau—thêm thông tin về luồng ở phần tiếp theo.
Bây giờ, dễ nhầm lẫn giữa đồng thời và song song hơn. Ý chúng tôi muốn nói đến đồng thời trong các ví dụ trên là các tiến trình không chạy song song.
Thay vào đó, hãy nói rằng một tiến trình yêu cầu hoàn thành một hoạt động Nhập/Xuất, thì Hệ điều hành sẽ phân bổ CPU cho một tiến trình khác trong khi nó hoàn thành hoạt động Nhập/Xuất của mình. Quy trình này sẽ tiếp tục cho đến khi tất cả các tiến trình hoàn tất việc thực thi của chúng.
Tuy nhiên, vì việc chuyển đổi các tác vụ của Hệ điều hành diễn ra trong vòng nano hoặc micro giây nên người dùng sẽ thấy các quy trình được thực hiện song song,
Không giống như trong thực thi tuần tự, CPU có thể không thực thi toàn bộ quy trình/chương trình cùng một lúc với các kiến trúc hiện tại. Thay vào đó, hầu hết các máy tính có thể chia toàn bộ quy trình thành nhiều thành phần nhẹ chạy độc lập với nhau theo thứ tự tùy ý. Các thành phần nhẹ này được gọi là luồng.
Ví dụ, Google Docs có thể có nhiều luồng hoạt động đồng thời. Trong khi một luồng tự động lưu công việc của bạn, một luồng khác có thể chạy ở chế độ nền, kiểm tra chính tả và ngữ pháp.
Hệ điều hành xác định thứ tự và luồng nào cần ưu tiên, tùy thuộc vào hệ thống.
Bây giờ bạn đã biết cách thực hiện các chương trình máy tính trong môi trường có một CPU duy nhất. Ngược lại, máy tính hiện đại thực hiện nhiều quy trình đồng thời trong nhiều CPU, được gọi là thực hiện song song. Hầu hết các kiến trúc hiện tại đều có nhiều CPU.
Như bạn có thể thấy trong sơ đồ bên dưới, CPU thực thi từng luồng thuộc về một tiến trình song song với nhau.
Trong chế độ song song, Hệ điều hành chuyển đổi các luồng đến và đi từ CPU trong các khoảng thời gian macro hoặc micro giây tùy thuộc vào kiến trúc hệ thống. Để Hệ điều hành đạt được khả năng thực thi song song, các lập trình viên máy tính sử dụng khái niệm được gọi là lập trình song song. Trong lập trình song song, các lập trình viên phát triển mã để tận dụng tối đa nhiều CPU.
Với rất nhiều tên miền sử dụng web scraping để thu thập dữ liệu từ các trang web, một nhược điểm đáng kể là tốn thời gian để thu thập một lượng lớn dữ liệu. Nếu bạn không phải là một nhà phát triển dày dạn kinh nghiệm, bạn có thể lãng phí rất nhiều thời gian để thử nghiệm các kỹ thuật cụ thể trước khi chạy mã mà không có lỗi và hoàn hảo.
Phần bên dưới nêu ra một số lý do tại sao việc thu thập dữ liệu web lại chậm.
Đầu tiên, trình thu thập dữ liệu phải điều hướng đến trang web mục tiêu trong quá trình thu thập dữ liệu web. Sau đó, nó sẽ phải kéo và truy xuất các thực thể từ các thẻ HTML mà bạn muốn thu thập. Cuối cùng, trong hầu hết các trường hợp, bạn sẽ lưu dữ liệu vào một tệp bên ngoài như định dạng CSV.
Vì vậy, như bạn có thể thấy, hầu hết các tác vụ trên đều yêu cầu hoạt động I/O bị ràng buộc nặng nề như kéo dữ liệu từ các trang web và sau đó lưu vào các tệp bên ngoài. Việc điều hướng đến các trang web mục tiêu thường phụ thuộc vào các yếu tố bên ngoài như tốc độ mạng hoặc chờ đợi trong khi mạng khả dụng.
Như bạn có thể thấy trong hình bên dưới, thời gian tiêu thụ cực kỳ chậm này có thể cản trở thêm quá trình thu thập dữ liệu khi bạn phải thu thập ba hoặc nhiều trang web. Giả sử rằng bạn thực hiện thao tác thu thập dữ liệu theo trình tự.
Do đó, dù bằng cách nào đi nữa, bạn cũng phải áp dụng đồng thời hoặc song song cho các hoạt động thu thập dữ liệu của mình. Chúng ta sẽ xem xét song song trước ở phần tiếp theo.
Tôi chắc rằng bây giờ bạn đã có cái nhìn tổng quan về đồng thời và song song. Phần này sẽ tập trung vào đồng thời trong web scraping với một ví dụ mã hóa đơn giản trong Python.
Trong ví dụ này, chúng tôi sẽ thu thập URL của các quốc gia theo danh sách thủ đô dựa trên dân số từ Wikipedia . Chương trình sẽ lưu các liên kết và sau đó chuyển đến từng trang trong số 240 trang và lưu HTML của các trang đó cục bộ.
Để chứng minh tác động của tính đồng thời, chúng tôi sẽ trình bày hai chương trình — một chương trình thực thi tuần tự và chương trình còn lại thực thi đồng thời với nhiều luồng.
Sau đây là mã:
import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin
import time
def get_countries():
countries = 'https://en.wikipedia.org/wiki/List_of_national_capitals_by_population'
all_countries = []
response = requests.get(countries)
soup = BeautifulSoup(response.text, "html.parser")
countries_pl = soup.select('th .flagicon+ a')
for link_pl in countries_pl:
link = link_pl.get("href")
link = urljoin(countries, link)
all_countries.append(link)
return all_countries
def fetch(link):
res = requests.get(link)
with open(link.split("/")[-1]+".html", "wb") as f:
f.write(res.content)
def main():
clinks = get_countries()
print(f"Total pages: {len(clinks)}")
start_time = time.time()
for link in clinks:
fetch(link)
duration = time.time() - start_time
print(f"Downloaded {len(links)} links in {duration} seconds")
main()
Giải thích mã
Đầu tiên, chúng tôi nhập các thư viện, bao gồm BeautifulSoap, để trích xuất dữ liệu HTML. Các thư viện khác bao gồm yêu cầu truy cập trang web, urllib để nối các URL như bạn sẽ khám phá và thư viện thời gian để tìm ra tổng thời gian thực hiện cho chương trình.
nhập yêu cầu từ bs4 nhập BeautifulSoup từ urllib.parse nhập urljoin nhập thời gian
Chương trình đầu tiên bắt đầu với mô-đun chính, gọi hàm get_countries(). Sau đó, hàm này truy cập URL Wikipedia được chỉ định trong biến Countries thông qua thể hiện BeautifulSoup thông qua trình phân tích cú pháp HTML.
Sau đó, nó sẽ tìm kiếm URL cho danh sách các quốc gia trong bảng bằng cách trích xuất giá trị trong thuộc tính href của thẻ neo.
Các liên kết mà bạn lấy được là các liên kết tương đối. Hàm urljoin sẽ chuyển đổi chúng thành các liên kết tuyệt đối. Các liên kết này sau đó được thêm vào mảng all_countries, mảng này trả về hàm chính
Sau đó, hàm fetch lưu nội dung HTML trong mỗi liên kết dưới dạng tệp HTML. Đây là những gì các đoạn mã này thực hiện:
def fetch(link): res = requests.get(link) với open(link.split("/")[-1]+".html", "wb") là f: f.write(res.content)
Cuối cùng, hàm main in ra thời gian cần thiết để lưu các tập tin ở định dạng HTML. Trong máy tính của chúng tôi, mất 131,22 giây.
Vâng, lần này chắc chắn có thể được thực hiện nhanh hơn. Chúng ta sẽ tìm hiểu ở phần tiếp theo, nơi cùng một chương trình được thực hiện với nhiều luồng.
Trong phiên bản đa luồng, chúng ta sẽ phải điều chỉnh những thay đổi nhỏ để chương trình có thể thực thi nhanh hơn.
Hãy nhớ rằng, đồng thời là về việc tạo nhiều luồng và thực thi chương trình. Có hai cách để tạo luồng — thủ công và sử dụng lớp ThreadPoolExecutor.
Sau khi tạo luồng thủ công, bạn có thể sử dụng hàm join trên tất cả các luồng cho phương pháp thủ công. Bằng cách đó, phương pháp chính sẽ đợi tất cả các luồng hoàn tất quá trình thực thi của chúng.
Trong chương trình này, chúng ta sẽ thực thi mã với lớp ThreadPoolExecutor là một phần của mô-đun concurrent.futures. Vì vậy, trước hết, bạn phải đặt dòng bên dưới vào chương trình trên.
từ concurrent.futures nhập ThreadPoolExecutor
Sau đó, bạn có thể thay đổi vòng lặp for lưu nội dung HTML theo định dạng HTML như sau:
với ThreadPoolExecutor(max_workers=32) làm trình thực thi: executor.map(fetch, clinks)
Mã trên tạo một nhóm luồng với tối đa 32 luồng. Đối với mỗi CPU, tham số max_workers khác nhau và bạn cần thử nghiệm với các giá trị khác nhau. Nó không nhất thiết tương đương với số luồng càng cao thì thời gian thực thi càng nhanh.
Vì vậy, máy tính của chúng tôi cho ra kết quả là 15,14 giây , tốt hơn nhiều so với khi chúng tôi thực hiện tuần tự.
Trước khi chuyển sang phần tiếp theo, đây là mã cuối cùng cho chương trình thực thi đồng thời:
import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin
from concurrent.futures import ThreadPoolExecutor
import time
def get_countries():
countries = 'https://en.wikipedia.org/wiki/List_of_national_capitals_by_population'
all_countries = []
response = requests.get(countries)
soup = BeautifulSoup(response.text, "html.parser")
countries_pl = soup.select('th .flagicon+ a')
for link_pl in countries_pl:
link = link_pl.get("href")
link = urljoin(countries, link)
all_countries.append(link)
return all_countries
def fetch(link):
res = requests.get(link)
with open(link.split("/")[-1]+".html", "wb") as f:
f.write(res.content)
def main():
clinks = get_countries()
print(f"Total pages: {len(clinks)}")
start_time = time.time()
with ThreadPoolExecutor(max_workers=32) as executor:
executor.map(fetch, clinks)
duration = time.time()-start_time
print(f"Downloaded {len(clinks)} links in {duration} seconds")
main()
Bây giờ chúng tôi hy vọng rằng bạn đã hiểu được về thực thi đồng thời. Để giúp bạn phân tích tốt hơn, chúng ta hãy xem cùng một chương trình hoạt động như thế nào trong môi trường đa bộ xử lý với các quy trình thực thi song song trong nhiều CPU.
Đầu tiên bạn phải nhập mô-đun cần thiết:
từ đa xử lý nhập Pool, cpu_count
Python cung cấp phương thức cpu_count(), đếm số CPU trong máy của bạn. Phương thức này chắc chắn hữu ích trong việc xác định số lượng chính xác các tác vụ mà nó có thể thực hiện song song.
Bây giờ bạn phải thay thế mã bằng vòng lặp for trong quá trình thực thi tuần tự bằng mã này:
với Pool (cpu_count()) là p: p.map(fetch,clinks)
Sau khi chạy đoạn mã này, thời gian thực thi tổng thể là 20,10 giây , nhanh hơn tương đối so với thời gian thực thi tuần tự trong chương trình đầu tiên.
Đến thời điểm này, chúng tôi hy vọng rằng bạn đã có cái nhìn tổng quan về lập trình song song và lập trình tuần tự - việc lựa chọn sử dụng phương pháp nào chủ yếu phụ thuộc vào tình huống cụ thể mà bạn gặp phải.
Đối với kịch bản web scraping, chúng tôi khuyên bạn nên bắt đầu bằng thực thi đồng thời rồi chuyển sang giải pháp song song. Chúng tôi hy vọng bạn thích đọc bài viết này. Đừng quên đọc các bài viết khác có liên quan đến web scraping như bài viết này trên blog của chúng tôi.