Đa luồng python hoạt động như thế nào?

Phần tìm hiểu sâu này về các thư viện song song hóa Python - đa xử lý và phân luồng - sẽ giải thích nên sử dụng cái nào khi nào đối với các bộ vấn đề khác nhau của nhà khoa học dữ liệu

  • Đa luồng python hoạt động như thế nào?

Sumit Ghosh

Ngày 7 tháng 9 năm 2019 14 phút đọc

Đa luồng python hoạt động như thế nào?

Sớm hay muộn, mọi dự án khoa học dữ liệu đều phải đối mặt với thách thức không thể tránh khỏi. tốc độ, vận tốc. Làm việc với các tập dữ liệu lớn hơn dẫn đến việc xử lý chúng chậm hơn, do đó, cuối cùng bạn sẽ phải suy nghĩ về việc tối ưu hóa thời gian chạy thuật toán của mình. Như hầu hết các bạn đã biết, song song hóa là một bước cần thiết của quá trình tối ưu hóa này. Python cung cấp hai thư viện tích hợp để song song hóa. đa xử lý và phân luồng. Trong bài viết này, chúng ta sẽ khám phá cách các nhà khoa học dữ liệu có thể lựa chọn giữa hai phương pháp và những yếu tố nào cần được lưu ý khi làm như vậy

Điện toán song song và Khoa học dữ liệu

Như các bạn đã biết, khoa học dữ liệu là khoa học xử lý lượng lớn dữ liệu và trích xuất những hiểu biết hữu ích từ chúng. Thông thường, các thao tác chúng tôi thực hiện trên dữ liệu có thể dễ dàng song song hóa, nghĩa là các tác nhân xử lý khác nhau có thể chạy thao tác trên dữ liệu từng phần một, sau đó kết hợp các kết quả ở cuối để có được kết quả hoàn chỉnh

Để hình dung rõ hơn về khả năng song song hóa, hãy xem xét một phép loại suy trong thế giới thực. Giả sử bạn cần dọn ba phòng trong nhà. Bạn có thể tự làm tất cả, lần lượt dọn phòng hoặc bạn có thể nhờ hai anh chị em của mình giúp bạn, mỗi người dọn một phòng. Trong cách tiếp cận thứ hai, mỗi bạn đang làm việc song song trên một phần của toàn bộ nhiệm vụ, do đó giảm tổng thời gian cần thiết để hoàn thành nó. Đây là tính song song trong hành động

Xử lý song song có thể đạt được trong Python theo hai cách khác nhau. đa xử lý và phân luồng

Đa xử lý và phân luồng. Học thuyết

Về cơ bản, đa xử lý và phân luồng là hai cách để đạt được tính toán song song, sử dụng các quy trình và luồng tương ứng làm tác nhân xử lý. Để hiểu cách thức hoạt động của chúng, chúng ta phải làm rõ quy trình và luồng là gì

Đa luồng python hoạt động như thế nào?

Quá trình

Một tiến trình là một thể hiện của một chương trình máy tính đang được thực thi. Mỗi quy trình có không gian bộ nhớ riêng mà nó sử dụng để lưu trữ các lệnh đang được chạy, cũng như bất kỳ dữ liệu nào nó cần lưu trữ và truy cập để thực thi

chủ đề

Chủ đề là các thành phần của một quá trình, có thể chạy song song. Có thể có nhiều luồng trong một tiến trình và chúng chia sẻ cùng một không gian bộ nhớ, tôi. e. không gian bộ nhớ của tiến trình cha. Điều này có nghĩa là mã được thực thi cũng như tất cả các biến được khai báo trong chương trình sẽ được chia sẻ bởi tất cả các luồng

Đa luồng python hoạt động như thế nào?

Quy trình và Chủ đề, Bởi I, Cburnett, CC BY-SA 3. 0, Liên kết

Ví dụ: chúng ta hãy xem xét các chương trình đang chạy trên máy tính của bạn ngay bây giờ. Có thể bạn đang đọc bài viết này trên một trình duyệt có nhiều tab đang mở. Bạn cũng có thể đang nghe nhạc qua ứng dụng Spotify trên máy tính cùng lúc. Trình duyệt và ứng dụng Spotify là các quy trình khác nhau; . Các tab khác nhau trong trình duyệt của bạn có thể được chạy trong các luồng khác nhau. Spotify có thể phát nhạc trong một luồng, tải nhạc từ internet ở một luồng khác và sử dụng luồng thứ ba để hiển thị GUI. Điều này sẽ được gọi là đa luồng. Điều tương tự cũng có thể được thực hiện với đa xử lý—nhiều quy trình—cũng vậy. Trên thực tế, hầu hết các trình duyệt hiện đại như Chrome và Firefox đều sử dụng đa xử lý chứ không phải đa luồng để xử lý nhiều tab

chi tiết kỹ thuật

  • Tất cả các luồng của một quy trình sống trong cùng một không gian bộ nhớ, trong khi các quy trình có không gian bộ nhớ riêng

  • Các luồng nhẹ hơn và có chi phí thấp hơn so với các quy trình. Quá trình sinh sản chậm hơn một chút so với chủ đề sinh sản

  • Chia sẻ các đối tượng giữa các luồng dễ dàng hơn vì chúng chia sẻ cùng một không gian bộ nhớ. Để đạt được điều tương tự giữa các quy trình, chúng tôi phải sử dụng một số loại mô hình IPC (giao tiếp giữa các quy trình), thường được cung cấp bởi HĐH

Cạm bẫy của tính toán song song

Giới thiệu tính song song cho một chương trình không phải lúc nào cũng là một trò chơi có tổng dương; . Những cái quan trọng nhất như sau

  • Điều kiện của cuộc đua. Như chúng ta đã thảo luận, các luồng có không gian bộ nhớ dùng chung và do đó chúng có thể có quyền truy cập vào các biến dùng chung. Tình trạng dồn đuổi xảy ra khi nhiều luồng cố gắng thay đổi cùng một biến đồng thời. Bộ lập lịch luồng có thể tùy ý hoán đổi giữa các luồng, vì vậy chúng tôi không có cách nào biết được thứ tự mà các luồng sẽ cố gắng thay đổi dữ liệu. Điều này có thể dẫn đến hành vi không chính xác trong một trong hai luồng, đặc biệt nếu các luồng quyết định thực hiện điều gì đó dựa trên giá trị của biến. Để ngăn điều này xảy ra, một khóa loại trừ lẫn nhau (hoặc mutex) có thể được đặt xung quanh đoạn mã sửa đổi biến để chỉ một luồng có thể ghi vào biến tại một thời điểm
  • chết đói. Tình trạng chết đói xảy ra khi một luồng bị từ chối truy cập vào một tài nguyên cụ thể trong thời gian dài hơn và kết quả là toàn bộ chương trình bị chậm lại. Điều này có thể xảy ra do tác dụng phụ ngoài ý muốn của thuật toán lập lịch luồng được thiết kế kém
  • Bế tắc. Việc lạm dụng khóa mutex cũng có một nhược điểm - nó có thể gây ra bế tắc trong chương trình. Bế tắc là trạng thái khi một luồng đang đợi một luồng khác giải phóng khóa, nhưng luồng khác đó cần một tài nguyên để hoàn thành mà luồng đầu tiên đang giữ. Bằng cách này, cả hai luồng đều dừng lại và chương trình tạm dừng. Bế tắc có thể được coi là một trường hợp cực kỳ đói. Để tránh điều này, chúng ta phải cẩn thận không đưa vào quá nhiều ổ khóa phụ thuộc lẫn nhau.
  • ổ khóa. Livelock là khi các luồng tiếp tục chạy trong một vòng lặp nhưng không đạt được bất kỳ tiến triển nào. Điều này cũng phát sinh từ thiết kế kém và sử dụng khóa mutex không đúng cách

Đa xử lý và phân luồng trong Python

Khóa phiên dịch viên toàn cầu

Khi nói đến Python, có một số điều kỳ lạ cần lưu ý. Chúng tôi biết rằng các luồng chia sẻ cùng một không gian bộ nhớ, vì vậy phải thực hiện các biện pháp phòng ngừa đặc biệt để hai luồng không ghi vào cùng một vị trí bộ nhớ. Trình thông dịch CPython xử lý việc này bằng cơ chế có tên là

import threading
import random
from functools import reduce


def func(number):
    random_list = random.sample(range(1000000), number)
    return reduce(lambda x, y: x*y, random_list)

    
number = 50000
thread1 = threading.Thread(target=func, args=(number,))
thread2 = threading.Thread(target=func, args=(number,))

thread1.start()
thread2.start()

thread1.join()
thread2.join()
8 hoặc Khóa thông dịch viên toàn cầu

Từ wiki Python

Trong CPython, khóa trình thông dịch toàn cầu, hoặc GIL, là một mutex bảo vệ quyền truy cập vào các đối tượng Python, ngăn nhiều luồng thực thi mã byte Python cùng một lúc. Khóa này là cần thiết chủ yếu vì quản lý bộ nhớ của CPython không an toàn cho luồng

Kiểm tra các trang trình bày ở đây để có cái nhìn chi tiết hơn về Python

import threading
import random
from functools import reduce


def func(number):
    random_list = random.sample(range(1000000), number)
    return reduce(lambda x, y: x*y, random_list)

    
number = 50000
thread1 = threading.Thread(target=func, args=(number,))
thread2 = threading.Thread(target=func, args=(number,))

thread1.start()
thread2.start()

thread1.join()
thread2.join()
8

import threading
import random
from functools import reduce


def func(number):
    random_list = random.sample(range(1000000), number)
    return reduce(lambda x, y: x*y, random_list)

    
number = 50000
thread1 = threading.Thread(target=func, args=(number,))
thread2 = threading.Thread(target=func, args=(number,))

thread1.start()
thread2.start()

thread1.join()
thread2.join()
8 hoàn thành công việc của mình, nhưng phải trả giá. Nó tuần tự hóa hiệu quả các hướng dẫn ở cấp độ thông dịch viên. Cách thức hoạt động của nó như sau. để bất kỳ luồng nào thực hiện bất kỳ chức năng nào, nó phải có khóa toàn cục. Chỉ một luồng duy nhất có thể có được khóa đó tại một thời điểm, điều đó có nghĩa là trình thông dịch cuối cùng sẽ chạy các hướng dẫn một cách an toàn. Thiết kế này làm cho luồng quản lý bộ nhớ trở nên an toàn, nhưng do đó, nó không thể sử dụng nhiều lõi CPU. Trong CPU lõi đơn, đó là điều mà các nhà thiết kế đã nghĩ đến khi phát triển CPython, đó không phải là vấn đề lớn. Nhưng khóa toàn cầu này cuối cùng lại trở thành một nút cổ chai nếu bạn đang sử dụng CPU đa lõi

Tuy nhiên, nút cổ chai này trở nên không liên quan nếu chương trình của bạn có nút cổ chai nghiêm trọng hơn ở nơi khác, chẳng hạn như trong mạng, IO hoặc tương tác người dùng. Trong những trường hợp đó, luồng là một phương pháp song song hoàn toàn hiệu quả. Nhưng đối với các chương trình bị ràng buộc bởi CPU, luồng kết thúc khiến chương trình chậm hơn. Hãy khám phá điều này với một số trường hợp sử dụng ví dụ

Các trường hợp sử dụng cho Threading

Các chương trình GUI luôn sử dụng luồng để làm cho các ứng dụng phản hồi nhanh. Ví dụ: trong một chương trình soạn thảo văn bản, một luồng có thể đảm nhiệm việc ghi lại các đầu vào của người dùng, một luồng khác có thể chịu trách nhiệm hiển thị văn bản, luồng thứ ba có thể kiểm tra chính tả, v.v. Ở đây, chương trình phải chờ tương tác của người dùng, đây là điểm nghẽn lớn nhất. Sử dụng đa xử lý sẽ không làm cho chương trình nhanh hơn

Một trường hợp sử dụng khác cho phân luồng là các chương trình bị ràng buộc IO hoặc bị ràng buộc mạng, chẳng hạn như trình quét web. Trong trường hợp này, nhiều luồng có thể xử lý song song nhiều trang web. Các luồng phải tải xuống các trang web từ Internet và đó sẽ là nút cổ chai lớn nhất, vì vậy luồng là một giải pháp hoàn hảo ở đây. Các máy chủ web, bị ràng buộc bởi mạng, hoạt động tương tự; . Một ví dụ liên quan khác là Tensorflow, sử dụng nhóm luồng để chuyển đổi dữ liệu song song

Các trường hợp sử dụng cho đa xử lý

Đa xử lý vượt trội hơn luồng trong trường hợp chương trình sử dụng nhiều CPU và không phải thực hiện bất kỳ tương tác IO hoặc người dùng nào. Ví dụ: bất kỳ chương trình nào chỉ xử lý các con số sẽ thấy sự tăng tốc đáng kể từ đa xử lý; . Một ví dụ thú vị trong thế giới thực là Pytorch Dataloader, sử dụng nhiều quy trình con để tải dữ liệu vào GPU

Song song hóa trong Python, trong hành động

Python cung cấp hai thư viện -

import multiprocessing
import random
from functools import reduce


def func(number):
    random_list = random.sample(range(1000000), number)
    return reduce(lambda x, y: x*y, random_list)

    
number = 50000
process1 = multiprocessing.Process(target=func, args=(number,))
process2 = multiprocessing.Process(target=func, args=(number,))

process1.start()
process2.start()

process1.join()
process2.join()
0 và
import multiprocessing
import random
from functools import reduce


def func(number):
    random_list = random.sample(range(1000000), number)
    return reduce(lambda x, y: x*y, random_list)

    
number = 50000
process1 = multiprocessing.Process(target=func, args=(number,))
process2 = multiprocessing.Process(target=func, args=(number,))

process1.start()
process2.start()

process1.join()
process2.join()
1- cho các phương thức song song hóa cùng tên. Bất chấp sự khác biệt cơ bản giữa chúng, hai thư viện cung cấp API rất giống nhau (kể từ Python 3. 7). Hãy xem chúng hoạt động

import threading
import random
from functools import reduce


def func(number):
    random_list = random.sample(range(1000000), number)
    return reduce(lambda x, y: x*y, random_list)

    
number = 50000
thread1 = threading.Thread(target=func, args=(number,))
thread2 = threading.Thread(target=func, args=(number,))

thread1.start()
thread2.start()

thread1.join()
thread2.join()

Bạn có thể thấy rằng tôi đã tạo một hàm

import multiprocessing
import random
from functools import reduce


def func(number):
    random_list = random.sample(range(1000000), number)
    return reduce(lambda x, y: x*y, random_list)

    
number = 50000
process1 = multiprocessing.Process(target=func, args=(number,))
process2 = multiprocessing.Process(target=func, args=(number,))

process1.start()
process2.start()

process1.join()
process2.join()
2 để tạo một danh sách các số ngẫu nhiên và sau đó nhân tất cả các phần tử của nó một cách tuần tự. Đây có thể là một quy trình khá nặng nề nếu số lượng mặt hàng đủ lớn, chẳng hạn như 50k hoặc 100k

Sau đó, tôi đã tạo hai luồng sẽ thực hiện cùng một chức năng. Các đối tượng luồng có phương thức

import multiprocessing
import random
from functools import reduce


def func(number):
    random_list = random.sample(range(1000000), number)
    return reduce(lambda x, y: x*y, random_list)

    
number = 50000
process1 = multiprocessing.Process(target=func, args=(number,))
process2 = multiprocessing.Process(target=func, args=(number,))

process1.start()
process2.start()

process1.join()
process2.join()
3 khởi động luồng không đồng bộ. Nếu chúng ta muốn đợi chúng kết thúc và quay trở lại, chúng ta phải gọi phương thức
import multiprocessing
import random
from functools import reduce


def func(number):
    random_list = random.sample(range(1000000), number)
    return reduce(lambda x, y: x*y, random_list)

    
number = 50000
process1 = multiprocessing.Process(target=func, args=(number,))
process2 = multiprocessing.Process(target=func, args=(number,))

process1.start()
process2.start()

process1.join()
process2.join()
4, và đó là những gì chúng ta đã làm ở trên

Như bạn có thể thấy, API để tạo một luồng mới cho một tác vụ ở chế độ nền khá đơn giản. Điều tuyệt vời là API dành cho đa xử lý cũng gần như giống hệt nhau;

import multiprocessing
import random
from functools import reduce


def func(number):
    random_list = random.sample(range(1000000), number)
    return reduce(lambda x, y: x*y, random_list)

    
number = 50000
process1 = multiprocessing.Process(target=func, args=(number,))
process2 = multiprocessing.Process(target=func, args=(number,))

process1.start()
process2.start()

process1.join()
process2.join()

Nó đây rồi—chỉ cần tráo đổi

import multiprocessing
import random
from functools import reduce


def func(number):
    random_list = random.sample(range(1000000), number)
    return reduce(lambda x, y: x*y, random_list)

    
number = 50000
process1 = multiprocessing.Process(target=func, args=(number,))
process2 = multiprocessing.Process(target=func, args=(number,))

process1.start()
process2.start()

process1.join()
process2.join()
5 với
import multiprocessing
import random
from functools import reduce


def func(number):
    random_list = random.sample(range(1000000), number)
    return reduce(lambda x, y: x*y, random_list)

    
number = 50000
process1 = multiprocessing.Process(target=func, args=(number,))
process2 = multiprocessing.Process(target=func, args=(number,))

process1.start()
process2.start()

process1.join()
process2.join()
6 và bạn có cùng một chương trình được triển khai bằng cách sử dụng đa xử lý

Rõ ràng là còn rất nhiều điều bạn có thể làm với điều này, nhưng điều đó không nằm trong phạm vi của bài viết này, vì vậy chúng tôi sẽ không đi sâu vào vấn đề này ở đây. Kiểm tra các tài liệu ở đây và ở đây nếu bạn muốn tìm hiểu thêm

điểm chuẩn

Bây giờ chúng ta đã có ý tưởng về cách mã triển khai song song hóa trông như thế nào, hãy quay lại các vấn đề về hiệu suất. Như chúng tôi đã lưu ý trước đây, phân luồng không phù hợp với các tác vụ bị ràng buộc bởi CPU; . Chúng tôi có thể xác thực điều này bằng một số điểm chuẩn đơn giản

Đầu tiên, hãy xem phân luồng so sánh với đa xử lý như thế nào đối với mẫu mã mà tôi đã chỉ cho bạn ở trên. Hãy nhớ rằng tác vụ này không liên quan đến bất kỳ loại IO nào, vì vậy đây là tác vụ thuần túy ràng buộc với CPU

Đa luồng python hoạt động như thế nào?

Và hãy xem điểm chuẩn tương tự cho tác vụ bị ràng buộc IO. Ví dụ: chức năng sau -

import requests

def func(number):
    url = 'http://example.com/'
    for i in range(number):
        response = requests.get(url)
        with open('example.com.txt', 'w') as output:
            output.write(response.text)

Chức năng này chỉ đơn giản là tìm nạp một trang web và lưu nó vào một tệp cục bộ, nhiều lần trong một vòng lặp. Vô dụng nhưng đơn giản và do đó rất phù hợp để trình diễn. Hãy nhìn vào điểm chuẩn

Đa luồng python hoạt động như thế nào?

Bây giờ có một số điều cần lưu ý từ hai biểu đồ này

  • Trong cả hai trường hợp, một quá trình đơn lẻ mất nhiều thời gian thực hiện hơn một luồng đơn lẻ. Rõ ràng, các quy trình có nhiều chi phí hoạt động hơn các luồng

  • Đối với tác vụ ràng buộc CPU, nhiều quy trình hoạt động tốt hơn nhiều luồng. Tuy nhiên, sự khác biệt này trở nên kém nổi bật hơn một chút khi chúng tôi đang sử dụng song song hóa 8x. Vì bộ xử lý trong máy tính xách tay của tôi là lõi tứ nên tối đa bốn quy trình có thể sử dụng nhiều lõi một cách hiệu quả. Vì vậy, khi tôi sử dụng nhiều quy trình hơn, nó không mở rộng tốt như vậy. Tuy nhiên, nó vẫn vượt trội so với luồng rất nhiều vì luồng không thể sử dụng nhiều lõi

  • Đối với tác vụ giới hạn IO, nút cổ chai không phải là CPU. Vì vậy, những hạn chế thông thường do GIL không áp dụng ở đây và đa xử lý không có lợi thế. Không chỉ vậy, chi phí hoạt động thấp của các luồng thực sự làm cho chúng nhanh hơn đa xử lý và luồng kết thúc hoạt động tốt hơn đa xử lý một cách nhất quán

Sự khác biệt, ưu điểm và nhược điểm
  • Các luồng chạy trong cùng một không gian bộ nhớ;

  • Theo dõi từ điểm trước. chia sẻ các đối tượng giữa các luồng dễ dàng hơn, nhưng mặt trái của đồng xu là bạn phải thực hiện thêm biện pháp để đồng bộ hóa đối tượng để đảm bảo rằng hai luồng không ghi vào cùng một đối tượng cùng một lúc và điều kiện chủng tộc không

  • Do chi phí lập trình được thêm vào của đồng bộ hóa đối tượng, lập trình đa luồng dễ bị lỗi hơn. Mặt khác, lập trình đa quy trình rất dễ thực hiện đúng

  • Các luồng có chi phí hoạt động thấp hơn so với các quy trình;

  • Do các hạn chế do GIL đặt ra trong Python, các luồng không thể đạt được tính song song thực sự khi sử dụng nhiều lõi CPU. Đa xử lý không có bất kỳ hạn chế nào như vậy

  • Lập lịch quy trình được xử lý bởi HĐH, trong khi lập lịch luồng được thực hiện bởi trình thông dịch Python

  • Các tiến trình con có thể bị gián đoạn và có thể bị hủy, trong khi các luồng con thì không. Bạn phải đợi chủ đề kết thúc hoặc

    import multiprocessing
    import random
    from functools import reduce
    
    
    def func(number):
        random_list = random.sample(range(1000000), number)
        return reduce(lambda x, y: x*y, random_list)
    
        
    number = 50000
    process1 = multiprocessing.Process(target=func, args=(number,))
    process2 = multiprocessing.Process(target=func, args=(number,))
    
    process1.start()
    process2.start()
    
    process1.join()
    process2.join()
    
    4

Từ tất cả các cuộc thảo luận này, chúng ta có thể kết luận như sau -

  • Luồng nên được sử dụng cho các chương trình liên quan đến IO hoặc tương tác người dùng

  • Đa xử lý nên được sử dụng cho các chương trình chuyên sâu về tính toán, bị ràng buộc bởi CPU

Từ quan điểm của một nhà khoa học dữ liệu

Một quy trình xử lý dữ liệu điển hình có thể được chia thành các bước sau

  1. Đọc dữ liệu thô và lưu trữ vào bộ nhớ chính hoặc GPU
  2. Thực hiện tính toán, sử dụng CPU hoặc GPU
  3. Lưu trữ thông tin được khai thác trong cơ sở dữ liệu hoặc đĩa

Hãy khám phá cách chúng tôi có thể giới thiệu tính song song trong các tác vụ này để chúng có thể được tăng tốc

Bước 1 liên quan đến việc đọc dữ liệu từ đĩa, vì vậy rõ ràng đĩa IO sẽ là nút cổ chai cho bước này. Như chúng ta đã thảo luận, các luồng là lựa chọn tốt nhất để song song hóa loại hoạt động này. Tương tự, bước 3 cũng là một ứng cử viên lý tưởng cho việc giới thiệu phân luồng

Tuy nhiên, bước 2 bao gồm các tính toán liên quan đến CPU hoặc GPU. Nếu đó là một tác vụ dựa trên CPU, việc sử dụng phân luồng sẽ không có ích gì; . Chỉ sau đó, chúng tôi mới có thể khai thác nhiều lõi của CPU và đạt được tính song song. Nếu đó là tác vụ dựa trên GPU, vì GPU đã triển khai kiến ​​trúc song song lớn ở cấp độ phần cứng, nên việc sử dụng đúng giao diện (thư viện và trình điều khiển) để tương tác với GPU sẽ đảm nhận phần còn lại

Đa luồng python hoạt động như thế nào?

Bây giờ bạn có thể đang nghĩ, “Đường dẫn dữ liệu của tôi trông hơi khác so với cái này; . ” Tuy nhiên, bạn sẽ có thể quan sát phương pháp được sử dụng ở đây để quyết định giữa phân luồng và đa xử lý. Các yếu tố bạn nên xem xét là

  • Cho dù nhiệm vụ của bạn có bất kỳ hình thức IO nào
  • Cho dù IO là nút cổ chai của chương trình của bạn
  • Nhiệm vụ của bạn có phụ thuộc vào khối lượng tính toán lớn của CPU hay không

Với những yếu tố này, cùng với những điều rút ra ở trên, bạn sẽ có thể đưa ra quyết định. Ngoài ra, hãy nhớ rằng bạn không phải sử dụng một dạng song song duy nhất trong suốt chương trình của mình. Bạn nên sử dụng cái này hay cái kia cho các phần khác nhau trong chương trình của mình, tùy theo phần nào phù hợp với phần cụ thể đó

Bây giờ chúng ta sẽ xem xét hai tình huống ví dụ mà một nhà khoa học dữ liệu có thể gặp phải và cách bạn có thể sử dụng điện toán song song để tăng tốc chúng

Kịch bản. Tải xuống email

Giả sử bạn muốn phân tích tất cả các email trong hộp thư đến của công ty khởi nghiệp do chính bạn phát triển và hiểu xu hướng. ai là người gửi thường xuyên nhất, từ khóa phổ biến nhất xuất hiện trong email là gì, ngày nào trong tuần hoặc giờ nào trong ngày bạn nhận được nhiều email nhất, v.v. Tất nhiên, bước đầu tiên của dự án này sẽ là tải email xuống máy tính của bạn

Lúc đầu, hãy thực hiện tuần tự mà không sử dụng bất kỳ phép song song nào. Mã để sử dụng ở bên dưới và nó sẽ khá dễ hiểu. Có một chức năng

import multiprocessing
import random
from functools import reduce


def func(number):
    random_list = random.sample(range(1000000), number)
    return reduce(lambda x, y: x*y, random_list)

    
number = 50000
process1 = multiprocessing.Process(target=func, args=(number,))
process2 = multiprocessing.Process(target=func, args=(number,))

process1.start()
process2.start()

process1.join()
process2.join()
8 lấy danh sách id email làm đầu vào và tải chúng xuống một cách tuần tự. Điều này gọi chức năng này với danh sách id của 100 email cùng một lúc

import multiprocessing
import random
from functools import reduce


def func(number):
    random_list = random.sample(range(1000000), number)
    return reduce(lambda x, y: x*y, random_list)

    
number = 50000
process1 = multiprocessing.Process(target=func, args=(number,))
process2 = multiprocessing.Process(target=func, args=(number,))

process1.start()
process2.start()

process1.join()
process2.join()
2

Mất thời gian.

import multiprocessing
import random
from functools import reduce


def func(number):
    random_list = random.sample(range(1000000), number)
    return reduce(lambda x, y: x*y, random_list)

    
number = 50000
process1 = multiprocessing.Process(target=func, args=(number,))
process2 = multiprocessing.Process(target=func, args=(number,))

process1.start()
process2.start()

process1.join()
process2.join()
9 giây

Bây giờ, hãy giới thiệu một số khả năng song song hóa vào tác vụ này để tăng tốc mọi thứ. Trước khi đi sâu vào viết mã, chúng ta phải quyết định giữa phân luồng và đa xử lý. Như bạn đã học cho đến nay, luồng là lựa chọn tốt nhất khi nói đến các tác vụ có một số IO làm nút cổ chai. Nhiệm vụ hiện tại rõ ràng thuộc về danh mục này, vì nó đang truy cập máy chủ IMAP qua internet. Vì vậy, chúng tôi sẽ đi với

import requests

def func(number):
    url = 'http://example.com/'
    for i in range(number):
        response = requests.get(url)
        with open('example.com.txt', 'w') as output:
            output.write(response.text)
0

Phần lớn mã chúng ta sẽ sử dụng sẽ giống với mã chúng ta đã sử dụng trong trường hợp tuần tự. Sự khác biệt duy nhất là chúng tôi sẽ chia danh sách 100 id email thành 10 phần nhỏ hơn, mỗi phần chứa 10 id, sau đó tạo 10 chuỗi và gọi hàm

import multiprocessing
import random
from functools import reduce


def func(number):
    random_list = random.sample(range(1000000), number)
    return reduce(lambda x, y: x*y, random_list)

    
number = 50000
process1 = multiprocessing.Process(target=func, args=(number,))
process2 = multiprocessing.Process(target=func, args=(number,))

process1.start()
process2.start()

process1.join()
process2.join()
8 với một phần khác với mỗi chuỗi. Tôi đang sử dụng lớp
import requests

def func(number):
    url = 'http://example.com/'
    for i in range(number):
        response = requests.get(url)
        with open('example.com.txt', 'w') as output:
            output.write(response.text)
2 từ thư viện chuẩn Python để phân luồng

import multiprocessing
import random
from functools import reduce


def func(number):
    random_list = random.sample(range(1000000), number)
    return reduce(lambda x, y: x*y, random_list)

    
number = 50000
process1 = multiprocessing.Process(target=func, args=(number,))
process2 = multiprocessing.Process(target=func, args=(number,))

process1.start()
process2.start()

process1.join()
process2.join()
7

Mất thời gian.

import requests

def func(number):
    url = 'http://example.com/'
    for i in range(number):
        response = requests.get(url)
        with open('example.com.txt', 'w') as output:
            output.write(response.text)
3 giây

Như bạn có thể thấy, phân luồng, tăng tốc đáng kể

Kịch bản. Phân loại bằng Scikit-Learn

Giả sử bạn gặp sự cố phân loại và bạn muốn sử dụng trình phân loại rừng ngẫu nhiên cho việc này. Vì đây là một thuật toán học máy tiêu chuẩn và nổi tiếng, chúng ta đừng phát minh lại bánh xe và chỉ sử dụng

import requests

def func(number):
    url = 'http://example.com/'
    for i in range(number):
        response = requests.get(url)
        with open('example.com.txt', 'w') as output:
            output.write(response.text)
4

Đoạn mã dưới đây phục vụ mục đích trình diễn. Tôi đã tạo một tập dữ liệu phân loại bằng cách sử dụng hàm trợ giúp

import requests

def func(number):
    url = 'http://example.com/'
    for i in range(number):
        response = requests.get(url)
        with open('example.com.txt', 'w') as output:
            output.write(response.text)
5, sau đó đào tạo một
import requests

def func(number):
    url = 'http://example.com/'
    for i in range(number):
        response = requests.get(url)
        with open('example.com.txt', 'w') as output:
            output.write(response.text)
6 về điều đó. Ngoài ra, tôi đang định thời gian cho phần mã thực hiện công việc cốt lõi là điều chỉnh mô hình

import threading
import random
from functools import reduce


def func(number):
    random_list = random.sample(range(1000000), number)
    return reduce(lambda x, y: x*y, random_list)

    
number = 50000
thread1 = threading.Thread(target=func, args=(number,))
thread2 = threading.Thread(target=func, args=(number,))

thread1.start()
thread2.start()

thread1.join()
thread2.join()
2

Mất thời gian.

import requests

def func(number):
    url = 'http://example.com/'
    for i in range(number):
        response = requests.get(url)
        with open('example.com.txt', 'w') as output:
            output.write(response.text)
7 giây

Bây giờ chúng ta sẽ xem xét cách chúng ta có thể giảm thời gian chạy của thuật toán này. Chúng tôi biết rằng thuật toán này có thể được song song hóa ở một mức độ nào đó, nhưng loại song song hóa nào sẽ phù hợp? . Vì vậy, đa xử lý sẽ là sự lựa chọn hợp lý

May mắn thay,

import requests

def func(number):
    url = 'http://example.com/'
    for i in range(number):
        response = requests.get(url)
        with open('example.com.txt', 'w') as output:
            output.write(response.text)
8 đã triển khai đa xử lý vào thuật toán này và chúng tôi sẽ không phải viết lại từ đầu. Như bạn có thể thấy trong đoạn mã bên dưới, chúng tôi chỉ cần cung cấp một tham số
import requests

def func(number):
    url = 'http://example.com/'
    for i in range(number):
        response = requests.get(url)
        with open('example.com.txt', 'w') as output:
            output.write(response.text)
9—số lượng quy trình mà nó sẽ sử dụng—để kích hoạt tính năng đa xử lý

import threading
import random
from functools import reduce


def func(number):
    random_list = random.sample(range(1000000), number)
    return reduce(lambda x, y: x*y, random_list)

    
number = 50000
thread1 = threading.Thread(target=func, args=(number,))
thread2 = threading.Thread(target=func, args=(number,))

thread1.start()
thread2.start()

thread1.join()
thread2.join()
6

Mất thời gian.

import multiprocessing
import random
from functools import reduce


def func(number):
    random_list = random.sample(range(1000000), number)
    return reduce(lambda x, y: x*y, random_list)

    
number = 50000
process1 = multiprocessing.Process(target=func, args=(number,))
process2 = multiprocessing.Process(target=func, args=(number,))

process1.start()
process2.start()

process1.join()
process2.join()
20 giây

Như mong đợi, đa xử lý làm cho nó nhanh hơn một chút

Sự kết luận

Hầu hết nếu không muốn nói là tất cả các dự án khoa học dữ liệu sẽ thấy tốc độ tăng mạnh với điện toán song song. Trên thực tế, nhiều thư viện khoa học dữ liệu phổ biến đã tích hợp tính song song, bạn chỉ cần kích hoạt nó. Vì vậy, trước khi tự mình thử triển khai nó, hãy xem qua tài liệu của thư viện bạn đang sử dụng và kiểm tra xem nó có hỗ trợ xử lý song song hay không (nhân tiện, tôi chắc chắn khuyên bạn nên kiểm tra dask). Trong trường hợp không, bài viết này sẽ hỗ trợ bạn tự thực hiện

Sẵn sàng để xây dựng, đào tạo và triển khai AI?

Bắt đầu miễn phí với nền tảng AI hợp tác của FloydHub

Dùng thử FloydHub miễn phí


Thông tin về các Tác giả

Sumit là một người đam mê máy tính và bắt đầu lập trình từ khi còn nhỏ; . Bất cứ khi nào anh ấy không lập trình, bạn có thể thấy anh ấy đọc triết học, chơi ghi-ta, chụp ảnh hoặc viết blog. Bạn có thể kết nối với Sumit trên Twitter, LinkedIn, Github và trang web của anh ấy

Đa luồng hoạt động như thế nào trong Python với GIL?

GIL chỉ cho phép một chuỗi hệ điều hành thực thi mã byte Python tại bất kỳ thời điểm nào và hậu quả của điều này là không thể tăng tốc mã Python sử dụng nhiều CPU bằng cách phân phối . Tuy nhiên, đây không phải là tác động tiêu cực duy nhất của GIL.

Đa luồng Python có sử dụng nhiều lõi không?

Các quy trình Python thường sử dụng một luồng đơn vì GIL. Mặc dù có GIL, các thư viện thực hiện các tác vụ tính toán nặng như numpy, scipy và pytorch sử dụng triển khai dựa trên C hoàn toàn, cho phép sử dụng nhiều lõi .

Đa luồng trong Python có hiệu quả không?

Đây là hai ưu điểm chính. Đa luồng trong Python hợp lý hóa việc sử dụng tài nguyên hiệu quả vì các luồng chia sẻ cùng một bộ nhớ và không gian dữ liệu. Nó cũng cho phép xuất hiện đồng thời nhiều tác vụ và giảm thời gian phản hồi.

Python có thể chạy nhiều luồng cùng một lúc không?

Tóm lại, phân luồng trong Python cho phép nhiều luồng được tạo trong một quy trình duy nhất, nhưng do GIL, không có luồng nào chạy cùng một lúc. Threading is still a very good option when it comes to running multiple I/O bound tasks concurrently.