Là chuỗi cấu trúc dữ liệu Python

Khi làm việc với các luồng trong Python, bạn sẽ thấy rất hữu ích khi có thể chia sẻ dữ liệu giữa các tác vụ khác nhau. Một trong những ưu điểm của các luồng trong Python là chúng chia sẻ cùng một không gian bộ nhớ và do đó việc trao đổi thông tin tương đối dễ dàng. Tuy nhiên, một số cấu trúc có thể giúp bạn đạt được các mục tiêu cụ thể hơn

Trong bài viết trước, chúng ta đã đề cập đến cách bắt đầu và đồng bộ hóa các luồng và bây giờ là lúc mở rộng hộp công cụ để xử lý việc trao đổi thông tin giữa chúng

Cách tiếp cận đầu tiên và ngây thơ nhất là sử dụng cùng một biến trong các luồng khác nhau. Chúng tôi đã sử dụng tính năng này trong hướng dẫn trước, nhưng không thảo luận rõ ràng về nó. Hãy xem cách chúng ta có thể sử dụng bộ nhớ dùng chung thông qua một ví dụ rất đơn giản

from threading import Thread, Event
from time import sleep

event = Event[]

def modify_variable[var]:
    while True:
        for i in range[len[var]]:
            var[i] += 1
        if event.is_set[]:
            break
        sleep[.5]
    print['Stop printing']


my_var = [1, 2, 3]
t = Thread[target=modify_variable, args=[my_var, ]]
t.start[]
while True:
    try:
        print[my_var]
        sleep[1]
    except KeyboardInterrupt:
        event.set[]
        break
t.join[]
print[my_var]

Ví dụ trên gần như tầm thường, nhưng nó có một tính năng rất quan trọng. Chúng tôi bắt đầu một chủ đề mới bằng cách chuyển một đối số,

[6563461, 6563462, 6563463]
0, là một danh sách các số. Chuỗi sẽ tăng giá trị của các số lên một, với độ trễ nhất định. Trong ví dụ này, chúng tôi sử dụng các sự kiện để kết thúc chuỗi một cách nhẹ nhàng, nếu bạn chưa quen với chúng, hãy xem hướng dẫn trước

Đoạn mã quan trọng trong ví dụ này là dòng

[6563461, 6563462, 6563463]
1. Câu lệnh in đó tồn tại trong luồng chính, tuy nhiên, nó có quyền truy cập vào thông tin được tạo trong luồng con. Hành vi này có thể thực hiện được nhờ chia sẻ bộ nhớ giữa các luồng khác nhau. Có thể truy cập cùng một không gian bộ nhớ là hữu ích, nhưng nó cũng có thể gây ra một số rủi ro. Trong ví dụ trên, chúng tôi chỉ bắt đầu một luồng, nhưng chúng tôi không giới hạn ở đó. Ví dụ, chúng ta có thể bắt đầu một số chủ đề

t = Thread[target=modify_variable, args=[my_var, ]]
t2 = Thread[target=modify_variable, args=[my_var, ]]
t.start[]
t2.start[]

Và bạn sẽ thấy rằng

[6563461, 6563462, 6563463]
0 và thông tin của nó được chia sẻ trên tất cả các chủ đề. Điều này tốt cho các ứng dụng như ứng dụng ở trên, trong đó luồng nào thêm một vào biến không quan trọng. Hay không? . Hãy loại bỏ
[6563461, 6563462, 6563463]
3

def modify_variable[var]:
    while True:
        for i in range[len[var]]:
            var[i] += 1
        if event.is_set[]:
            break
        # sleep[.5]
    print['Stop printing']

Bây giờ, khi chúng tôi chạy mã, sẽ không có trạng thái ngủ giữa lần lặp này và lần lặp tiếp theo. Hãy chạy nó trong một thời gian ngắn, giả sử là 5 giây, chúng ta có thể làm như sau

from time import time
[...]

my_var = [1, 2, 3]
t = Thread[target=modify_variable, args=[my_var, ]]
t.start[]
t0 = time[]
while time[]-t0 < 5:
    print[my_var]
    sleep[1]
event.set[]
t.join[]
print[my_var]

Tôi đã loại bỏ các phần mã lặp lại. Nếu bạn chạy mã này, bạn sẽ nhận được kết quả là số lượng rất lớn. Trong trường hợp của tôi, tôi đã nhận

[6563461, 6563462, 6563463]

Tuy nhiên, có một đặc điểm rất quan trọng cần lưu ý. Ba số liên tiếp. Điều này được mong đợi vì biến bắt đầu là

[6563461, 6563462, 6563463]
4 và chúng tôi đang thêm một biến vào mỗi biến. Hãy bắt đầu một luồng thứ hai lần này và xem đầu ra là gì

my_var = [1, 2, 3]
t = Thread[target=modify_variable, args=[my_var, ]]
t2 = Thread[target=modify_variable, args=[my_var, ]]
t.start[]
t2.start[]
t0 = time[]
while time[]-t0 < 5:
    try:
        print[my_var]
        sleep[1]
    except KeyboardInterrupt:
        event.set[]
        break
event.set[]
t.join[]
t2.join[]
print[my_var]

Tôi có đầu ra là các giá trị sau

[5738447, 5686971, 5684220]

Trước tiên, bạn có thể lưu ý rằng chúng không lớn hơn trước, nghĩa là chạy hai luồng thay vì một luồng thực sự có thể chậm hơn đối với thao tác này. Một điều khác cần lưu ý là các giá trị không liên tiếp với nhau. Và đây là một hành vi rất quan trọng có thể xuất hiện khi làm việc với nhiều luồng trong Python. Nếu bạn thực sự suy nghĩ kỹ, bạn có thể giải thích vấn đề này đến từ đâu không?

Trong phần hướng dẫn trước, chúng ta đã thảo luận rằng các luồng được xử lý bởi hệ điều hành, hệ điều hành này sẽ quyết định khi nào bật hoặc tắt một luồng. Chúng tôi không kiểm soát được hệ điều hành quyết định làm gì. Trong ví dụ trên, vì không có

[6563461, 6563462, 6563463]
3 trong vòng lặp, hệ điều hành sẽ phải quyết định khi nào nên dừng một luồng và bắt đầu một luồng khác. Tuy nhiên, điều đó không giải thích hoàn toàn đầu ra mà chúng tôi đang nhận được. Không thành vấn đề nếu một chuỗi chạy trước và dừng lại, v.v. chúng tôi luôn thêm
[6563461, 6563462, 6563463]
6 vào mỗi phần tử

Vấn đề với đoạn mã trên là ở dòng

[6563461, 6563462, 6563463]
7, đây thực sự là hai thao tác. Đầu tiên, nó sao chép giá trị từ
[6563461, 6563462, 6563463]
8 và quảng cáo
[6563461, 6563462, 6563463]
9. Sau đó, nó lưu giá trị trở lại
[6563461, 6563462, 6563463]
8. Ở giữa hai thao tác này, hệ điều hành có thể quyết định chuyển từ tác vụ này sang tác vụ khác. Trong trường hợp như vậy, giá trị mà cả hai tác vụ nhìn thấy trong danh sách là như nhau và do đó, thay vì thêm
[6563461, 6563462, 6563463]
6 hai lần, chúng tôi chỉ thực hiện một lần. Nếu bạn muốn làm điều đó nổi bật hơn nữa, bạn có thể bắt đầu hai luồng, một luồng cộng và một luồng trừ khỏi danh sách và điều đó sẽ cho bạn gợi ý nhanh về luồng nào chạy nhanh hơn. Trong trường hợp của tôi, tôi nhận được đầu ra sau

[-8832, -168606, 2567]

Nhưng nếu tôi chạy nó lần khác, tôi nhận được

[97998, 133432, 186591]

lưu ý Bạn có thể nhận thấy rằng có độ trễ giữa

my_var = [1, 2, 3]
t = Thread[target=modify_variable, args=[my_var, ]]
t2 = Thread[target=modify_variable, args=[my_var, ]]
t.start[]
t2.start[]
t0 = time[]
while time[]-t0 < 5:
    try:
        print[my_var]
        sleep[1]
    except KeyboardInterrupt:
        event.set[]
        break
event.set[]
t.join[]
t2.join[]
print[my_var]
2 của cả hai luồng, điều này có thể mang lại lợi thế nhất định cho luồng đầu tiên được bắt đầu. Tuy nhiên, điều đó một mình không thể giải thích sản lượng được tạo ra

Để giải quyết vấn đề mà chúng ta đã tìm thấy trong các ví dụ trước, chúng ta phải chắc chắn rằng không có hai luồng nào cố gắng ghi cùng một lúc vào cùng một biến. Đối với điều đó, chúng ta có thể sử dụng một

my_var = [1, 2, 3]
t = Thread[target=modify_variable, args=[my_var, ]]
t2 = Thread[target=modify_variable, args=[my_var, ]]
t.start[]
t2.start[]
t0 = time[]
while time[]-t0 < 5:
    try:
        print[my_var]
        sleep[1]
    except KeyboardInterrupt:
        event.set[]
        break
event.set[]
t.join[]
t2.join[]
print[my_var]
3

from threading import Lock
[...]
data_lock = Lock[]
def modify_variable[var]:
    while True:
        for i in range[len[var]]:
            with data_lock:
                var[i] += 1
        if event.is_set[]:
            break
        # sleep[.5]
    print['Stop printing']

Lưu ý rằng chúng tôi đã thêm một dòng

my_var = [1, 2, 3]
t = Thread[target=modify_variable, args=[my_var, ]]
t2 = Thread[target=modify_variable, args=[my_var, ]]
t.start[]
t2.start[]
t0 = time[]
while time[]-t0 < 5:
    try:
        print[my_var]
        sleep[1]
    except KeyboardInterrupt:
        event.set[]
        break
event.set[]
t.join[]
t2.join[]
print[my_var]
4 vào chức năng. Nếu chạy lại đoạn mã trên, bạn sẽ thấy rằng các giá trị chúng ta nhận được luôn liên tiếp. Khóa đảm bảo rằng chỉ có một luồng sẽ truy cập vào biến tại một thời điểm

Các ví dụ về tăng hoặc giảm giá trị từ một danh sách hầu như không quan trọng, nhưng chúng chỉ ra hướng hiểu về sự phức tạp của việc quản lý bộ nhớ khi xử lý lập trình đồng thời. Chia sẻ bộ nhớ là một tính năng hay, nhưng nó cũng đi kèm với rủi ro

Một trong những tình huống phổ biến mà các luồng được sử dụng là khi bạn có một số tác vụ chậm mà bạn không thể tối ưu hóa. Ví dụ: hãy tưởng tượng bạn đang tải xuống dữ liệu từ một trang web bằng cách sử dụng. Hầu hết thời gian bộ xử lý sẽ không hoạt động. Điều này có nghĩa là bạn có thể sử dụng thời gian đó cho việc khác. Nếu bạn muốn tải xuống toàn bộ trang web [còn gọi là cạo], bạn nên tải xuống nhiều trang cùng một lúc. Hãy tưởng tượng bạn có một danh sách các trang bạn muốn tải xuống và bạn bắt đầu một số chủ đề, mỗi chủ đề tải xuống một trang. Nếu bạn không cẩn thận về cách thực hiện điều này, bạn có thể sẽ tải xuống hai lần giống nhau, như chúng ta đã thấy trong phần trước

Đây là nơi mà một đối tượng khác có thể rất hữu ích khi làm việc với các luồng. hàng đợi. Hàng đợi là một đối tượng nhận dữ liệu theo thứ tự,. e. bạn đặt dữ liệu vào từng phần tử một. Sau đó, dữ liệu có thể được sử dụng theo cùng một thứ tự, được gọi là Nhập trước xuất trước [FIFO]. Một ví dụ rất đơn giản sẽ là

t = Thread[target=modify_variable, args=[my_var, ]]
t2 = Thread[target=modify_variable, args=[my_var, ]]
t.start[]
t2.start[]
0

Trong ví dụ này, bạn thấy rằng chúng tôi tạo một

my_var = [1, 2, 3]
t = Thread[target=modify_variable, args=[my_var, ]]
t2 = Thread[target=modify_variable, args=[my_var, ]]
t.start[]
t2.start[]
t0 = time[]
while time[]-t0 < 5:
    try:
        print[my_var]
        sleep[1]
    except KeyboardInterrupt:
        event.set[]
        break
event.set[]
t.join[]
t2.join[]
print[my_var]
5, sau đó chúng tôi đưa vào hàng đợi các số từ 0 đến 19. Sau đó, chúng tôi tạo một vòng lặp
my_var = [1, 2, 3]
t = Thread[target=modify_variable, args=[my_var, ]]
t2 = Thread[target=modify_variable, args=[my_var, ]]
t.start[]
t2.start[]
t0 = time[]
while time[]-t0 < 5:
    try:
        print[my_var]
        sleep[1]
    except KeyboardInterrupt:
        event.set[]
        break
event.set[]
t.join[]
t2.join[]
print[my_var]
6 lấy dữ liệu ra khỏi hàng đợi và in nó. Đây là hành vi cơ bản của hàng đợi trong Python. Bạn nên chú ý đến thực tế là các số được in theo thứ tự mà chúng được thêm vào hàng đợi

Quay lại ví dụ ở đầu bài, chúng ta có thể sử dụng queue để chia sẻ thông tin giữa các thread. Chúng ta có thể sửa đổi hàm sao cho thay vì một danh sách làm đối số, nó chấp nhận một hàng đợi mà từ đó nó sẽ đọc các phần tử. Sau đó, nó sẽ xuất kết quả ra hàng đợi đầu ra

t = Thread[target=modify_variable, args=[my_var, ]]
t2 = Thread[target=modify_variable, args=[my_var, ]]
t.start[]
t2.start[]
1

Để sử dụng mã ở trên, chúng tôi sẽ cần tạo hai hàng đợi. Ý tưởng là chúng ta cũng có thể tạo hai luồng, trong đó hàng đợi đầu vào và đầu ra được đảo ngược. Trong trường hợp đó, trên luồng đặt đầu ra của nó vào hàng đợi của luồng thứ hai và ngược lại. Điều này sẽ giống như sau

t = Thread[target=modify_variable, args=[my_var, ]]
t2 = Thread[target=modify_variable, args=[my_var, ]]
t.start[]
t2.start[]
2

Trong trường hợp của tôi, đầu ra tôi nhận được là

t = Thread[target=modify_variable, args=[my_var, ]]
t2 = Thread[target=modify_variable, args=[my_var, ]]
t.start[]
t2.start[]
3

Nhỏ hơn nhiều so với mọi thứ khác mà chúng tôi đã thấy cho đến nay, nhưng ít nhất chúng tôi đã quản lý để chia sẻ dữ liệu giữa hai luồng khác nhau mà không có bất kỳ xung đột nào. Tốc độ chậm này đến từ đâu? . Một trong những điều thú vị nhất là chúng tôi đang kiểm tra xem hàng đợi có trống không trước khi thử chạy phần còn lại của mã. Chúng tôi có thể theo dõi lượng thời gian thực sự dành cho việc chạy phần quan trọng của chương trình của chúng tôi

t = Thread[target=modify_variable, args=[my_var, ]]
t2 = Thread[target=modify_variable, args=[my_var, ]]
t.start[]
t2.start[]
4

Những thay đổi duy nhất là việc bổ sung một biến mới trong hàm, được gọi là

my_var = [1, 2, 3]
t = Thread[target=modify_variable, args=[my_var, ]]
t2 = Thread[target=modify_variable, args=[my_var, ]]
t.start[]
t2.start[]
t0 = time[]
while time[]-t0 < 5:
    try:
        print[my_var]
        sleep[1]
    except KeyboardInterrupt:
        event.set[]
        break
event.set[]
t.join[]
t2.join[]
print[my_var]
7. Sau đó, chúng tôi theo dõi thời gian tính toán và đưa vào chuỗi mới. Nếu chúng tôi chạy lại mã, đầu ra bạn sẽ nhận được giống như

t = Thread[target=modify_variable, args=[my_var, ]]
t2 = Thread[target=modify_variable, args=[my_var, ]]
t.start[]
t2.start[]
5

Điều này có nghĩa là trong 5 giây mà chương trình của chúng ta chạy, chỉ trong khoảng. 9 mili giây chúng ta đang thực sự làm điều gì đó. Đây là. 01% thời gian. Hãy nhanh chóng xem điều gì sẽ xảy ra nếu chúng ta thay đổi mã để chỉ sử dụng một hàng đợi thay vì hai hàng đợi, tôi. e. hàng đợi đầu vào và đầu ra sẽ giống nhau

t = Thread[target=modify_variable, args=[my_var, ]]
t2 = Thread[target=modify_variable, args=[my_var, ]]
t.start[]
t2.start[]
6

Chỉ với sự thay đổi đó, tôi đã có đầu ra sau

t = Thread[target=modify_variable, args=[my_var, ]]
t2 = Thread[target=modify_variable, args=[my_var, ]]
t.start[]
t2.start[]
7

Điều đó tốt hơn nhiều. Trong khoảng 5 giây chương trình chạy, các luồng chạy tổng cộng 8 giây. Đó là những gì người ta mong đợi về việc song song hóa. Ngoài ra, đầu ra của các vòng lặp lớn hơn nhiều

t = Thread[target=modify_variable, args=[my_var, ]]
t2 = Thread[target=modify_variable, args=[my_var, ]]
t.start[]
t2.start[]
8

Bạn thử đoán xem điều gì đã khiến chương trình của chúng ta trở nên chậm như vậy nếu chúng ta sử dụng hai hàng đợi nhưng lại khá nhanh nếu chúng ta sử dụng cùng một hàng đợi cho đầu ra và đầu vào?

Chúng tôi không kiểm soát được liệu hệ điều hành có quyết định chuyển từ tác vụ này sang tác vụ khác hay không. Trong đoạn mã trên, chúng tôi kiểm tra xem hàng đợi có trống không. Rất có thể hệ điều hành quyết định ưu tiên cho một tác vụ mà về cơ bản không làm gì cả, nhưng đợi cho đến khi có một phần tử trong hàng đợi. Nếu điều này xảy ra không đồng bộ, phần lớn thời gian chương trình sẽ chỉ chờ có một phần tử trong hàng đợi [nó luôn ưu tiên sai nhiệm vụ]. Mặc dù khi chúng ta sử dụng cùng một tác vụ cho đầu vào và đầu ra, không quan trọng nó chạy tác vụ nào, sẽ luôn có thứ gì đó để tiến hành

Nếu bạn muốn xem suy đoán trước đó có đúng hay không, chúng ta có thể đo lường nó. Chúng tôi chỉ có một câu lệnh

my_var = [1, 2, 3]
t = Thread[target=modify_variable, args=[my_var, ]]
t2 = Thread[target=modify_variable, args=[my_var, ]]
t.start[]
t2.start[]
t0 = time[]
while time[]-t0 < 5:
    try:
        print[my_var]
        sleep[1]
    except KeyboardInterrupt:
        event.set[]
        break
event.set[]
t.join[]
t2.join[]
print[my_var]
8 để kiểm tra
my_var = [1, 2, 3]
t = Thread[target=modify_variable, args=[my_var, ]]
t2 = Thread[target=modify_variable, args=[my_var, ]]
t.start[]
t2.start[]
t0 = time[]
while time[]-t0 < 5:
    try:
        print[my_var]
        sleep[1]
    except KeyboardInterrupt:
        event.set[]
        break
event.set[]
t.join[]
t2.join[]
print[my_var]
9, chúng tôi có thể thêm một
[5738447, 5686971, 5684220]
0 để tích lũy thời gian chương trình thực sự không làm gì cả

t = Thread[target=modify_variable, args=[my_var, ]]
t2 = Thread[target=modify_variable, args=[my_var, ]]
t.start[]
t2.start[]
9

Trong đoạn mã trên, nếu hàng đợi trống, chương trình sẽ ngủ trong 1 mili giây. Tất nhiên, đây không phải là điều tốt nhất, nhưng chúng ta có thể cho rằng 1 mili giây sẽ không có tác động thực sự đến hiệu suất tổng thể của chương trình. Khi tôi chạy chương trình trên, sử dụng hai hàng đợi khác nhau, tôi nhận được kết quả sau

def modify_variable[var]:
    while True:
        for i in range[len[var]]:
            var[i] += 1
        if event.is_set[]:
            break
        # sleep[.5]
    print['Stop printing']
0

Rõ ràng là hầu hết thời gian chương trình chỉ đợi cho đến khi có nhiều dữ liệu hơn trên hàng đợi. Vì chúng tôi đang ngủ trong 1 ms mỗi khi không có dữ liệu, chúng tôi thực sự đang làm cho chương trình chậm hơn nhiều. Nhưng tôi nghĩ đó là một ví dụ tốt. Chúng ta có thể so sánh nó với việc sử dụng cùng một hàng đợi cho đầu vào và đầu ra

def modify_variable[var]:
    while True:
        for i in range[len[var]]:
            var[i] += 1
        if event.is_set[]:
            break
        # sleep[.5]
    print['Stop printing']
1

Bây giờ bạn thấy rằng ngay cả khi chúng ta đang lãng phí thời gian vì giấc ngủ, thì phần lớn thời gian thói quen của chúng ta thực sự là thực hiện một phép tính

Điều duy nhất bạn phải cẩn thận khi sử dụng cùng một hàng đợi cho đầu vào và đầu ra là giữa việc kiểm tra xem hàng đợi có trống hay không và thực sự đọc từ nó, có thể xảy ra trường hợp luồng khác lấy kết quả. Điều này được mô tả trong. Trừ khi chúng tôi bao gồm chính mình một

my_var = [1, 2, 3]
t = Thread[target=modify_variable, args=[my_var, ]]
t2 = Thread[target=modify_variable, args=[my_var, ]]
t.start[]
t2.start[]
t0 = time[]
while time[]-t0 < 5:
    try:
        print[my_var]
        sleep[1]
    except KeyboardInterrupt:
        event.set[]
        break
event.set[]
t.join[]
t2.join[]
print[my_var]
3, Hàng đợi có thể được đọc và viết bởi bất kỳ chủ đề nào. Khóa chỉ có hiệu lực đối với các lệnh
[5738447, 5686971, 5684220]
2 hoặc
[5738447, 5686971, 5684220]
3

Hàng đợi có một số tùy chọn bổ sung, chẳng hạn như số phần tử tối đa mà chúng có thể chứa. Bạn cũng có thể xác định các loại hàng đợi LIFO [vào sau, ra trước] mà bạn có thể đọc về. Điều tôi thấy hữu ích hơn về

[5738447, 5686971, 5684220]
4 là chúng được viết bằng Python thuần túy. Nếu bạn truy cập mã nguồn của họ, bạn có thể tìm hiểu rất nhiều về đồng bộ hóa trong chuỗi, ngoại lệ tùy chỉnh và tài liệu

Điều quan trọng cần lưu ý là khi bạn làm việc với nhiều Chủ đề, đôi khi bạn muốn đợi [i. e. chặn thực thi], đôi khi bạn không. Trong các ví dụ trên, chúng tôi luôn kiểm tra xem Hàng đợi có trống không trước khi đọc từ đó. Nhưng điều gì sẽ xảy ra nếu chúng ta không kiểm tra nó? .

[5738447, 5686971, 5684220]
6 và
[5738447, 5686971, 5684220]
7. Cái đầu tiên được sử dụng để xác định xem chúng ta có muốn chương trình đợi cho đến khi có sẵn một phần tử hay không. Thứ hai là chỉ định số giây chúng tôi muốn nó đợi. Sau khoảng thời gian đó, một ngoại lệ được đưa ra. Nếu chúng tôi đặt
[5738447, 5686971, 5684220]
6 thành false và hàng đợi trống, ngoại lệ sẽ được đưa ra ngay lập tức

Chúng ta có thể thay đổi chức năng

[5738447, 5686971, 5684220]
9 để tận dụng điều này

def modify_variable[var]:
    while True:
        for i in range[len[var]]:
            var[i] += 1
        if event.is_set[]:
            break
        # sleep[.5]
    print['Stop printing']
2

Với mã này, sử dụng các hàng đợi khác nhau cho đầu vào và đầu ra, tôi nhận được thông tin sau

def modify_variable[var]:
    while True:
        for i in range[len[var]]:
            var[i] += 1
        if event.is_set[]:
            break
        # sleep[.5]
    print['Stop printing']
3

Đó là tốt hơn nhiều so với những gì chúng tôi đã nhận được trước đây. Nhưng, điều này không thực sự công bằng. Rất nhiều thời gian dành cho việc chờ đợi trong hàm

[5738447, 5686971, 5684220]
2, nhưng chúng tôi vẫn đang đếm thời gian đó. Nếu chúng ta di chuyển dòng
[-8832, -168606, 2567]
1 ngay bên dưới
[5738447, 5686971, 5684220]
2, thời gian mã thực sự chạy sẽ rất khác nhau

def modify_variable[var]:
    while True:
        for i in range[len[var]]:
            var[i] += 1
        if event.is_set[]:
            break
        # sleep[.5]
    print['Stop printing']
4

Vì vậy, bây giờ bạn thấy đấy, có lẽ chúng ta nên tính toán thời gian theo cách khác trong các ví dụ trước, đặc biệt là khi chúng ta đang sử dụng cùng một hàng đợi cho đầu vào và đầu ra

Nếu chúng ta không muốn lập trình block trong khi chờ get thì có thể làm như sau

def modify_variable[var]:
    while True:
        for i in range[len[var]]:
            var[i] += 1
        if event.is_set[]:
            break
        # sleep[.5]
    print['Stop printing']
5

Hoặc, chúng tôi có thể chỉ định thời gian chờ, như thế này

def modify_variable[var]:
    while True:
        for i in range[len[var]]:
            var[i] += 1
        if event.is_set[]:
            break
        # sleep[.5]
    print['Stop printing']
6

Trong trường hợp đó, chúng tôi không đợi [

[-8832, -168606, 2567]
3] và chúng tôi bắt ngoại lệ hoặc chúng tôi đợi tối đa 1 mili giây [
[-8832, -168606, 2567]
4] và chúng tôi bắt ngoại lệ. Bạn có thể thử với các tùy chọn này để xem liệu hiệu suất mã của bạn có thay đổi theo bất kỳ cách nào không

Cho đến nay, chúng tôi luôn sử dụng khóa để dừng chuỗi, tôi tin rằng đó là một cách rất tao nhã để thực hiện. Tuy nhiên, có một khả năng khác, đó là kiểm soát luồng luồng bằng cách thêm thông tin đặc biệt vào hàng đợi. Một ví dụ rất đơn giản là thêm một phần tử

[-8832, -168606, 2567]
5 vào hàng đợi và khi hàm nhận được phần tử đó, nó sẽ dừng thực thi. Mã sẽ trông như thế này

def modify_variable[var]:
    while True:
        for i in range[len[var]]:
            var[i] += 1
        if event.is_set[]:
            break
        # sleep[.5]
    print['Stop printing']
7

Và sau đó, trong phần chính của tập lệnh, khi chúng ta muốn dừng các luồng, chúng ta làm như sau

def modify_variable[var]:
    while True:
        for i in range[len[var]]:
            var[i] += 1
        if event.is_set[]:
            break
        # sleep[.5]
    print['Stop printing']
8

Nếu bạn đang tự hỏi tại sao bạn lại chọn tùy chọn này hay tùy chọn khác, thì câu trả lời thực sự khá đơn giản. Các ví dụ chúng tôi đang làm việc, luôn có hàng đợi với tối đa 1 phần tử. Khi chúng ta dừng chương trình, chúng ta biết mọi thứ trong hàng đợi đã được xử lý. Tuy nhiên, hãy tưởng tượng rằng chương trình đang xử lý một tập hợp các phần tử, không có mối quan hệ nào với nhau. Đây sẽ là trường hợp nếu bạn tải xuống dữ liệu từ một trang web chẳng hạn hoặc xử lý hình ảnh, v.v. Bạn muốn chắc chắn rằng bạn đã xử lý xong mọi thứ trước khi dừng chuỗi. Trong trường hợp như vậy, việc thêm một giá trị đặc biệt vào hàng đợi đảm bảo rằng tất cả các phần tử sẽ được xử lý

cảnh báo rằng đó là một ý tưởng rất khôn ngoan để đảm bảo hàng đợi trống sau khi bạn ngừng sử dụng nó. Nếu, như trước đây, bạn làm gián đoạn luồng bằng cách xem trạng thái của khóa, thì hàng đợi có thể chứa rất nhiều dữ liệu trong đó và do đó bộ nhớ sẽ không được giải phóng. Một vòng lặp đơn giản lấy tất cả các phần tử của hàng đợi sẽ giải quyết nó

Các ví dụ trong bài viết này chuyên sâu về tính toán và do đó, chúng ở ngay trên ranh giới nơi việc sử dụng đa luồng không thể áp dụng và nơi phát sinh tất cả các vấn đề [chẳng hạn như xử lý đồng thời, v.v. ] Chúng tôi đã tập trung vào các giới hạn của đa luồng vì nếu bạn hiểu chúng, bạn sẽ lập trình tự tin hơn nhiều. Bạn sẽ không kiễng chân lên để hy vọng vấn đề không phát sinh

Một lĩnh vực mà tính năng đa luồng vượt trội trong các tác vụ IO [đầu vào-đầu ra]. Ví dụ: nếu bạn có một chương trình ghi vào ổ cứng trong khi nó đang làm việc khác, thì việc ghi vào ổ cứng có thể được chuyển sang một luồng riêng biệt một cách an toàn, trong khi phần còn lại của chương trình vẫn tiếp tục chạy. Điều này cũng hợp lệ nếu chương trình đợi đầu vào của người dùng hoặc tài nguyên mạng khả dụng, tải xuống dữ liệu từ internet, v.v.

Ví dụ tải trang web

Để kết thúc bài viết này, chúng ta hãy xem một ví dụ về việc tải xuống các trang web bằng luồng, hàng đợi và khóa. Ngay cả khi có thể cải thiện một số hiệu suất, ví dụ này sẽ hiển thị các khối xây dựng cơ bản của hầu hết mọi ứng dụng phân luồng quan tâm

Đầu tiên, hãy thảo luận về những gì chúng ta muốn đạt được. Để giữ cho ví dụ đơn giản, chúng tôi sẽ tải xuống tất cả các trang web trong danh sách và chúng tôi muốn lưu thông tin đã tải xuống vào ổ cứng. Cách tiếp cận đầu tiên là tạo một vòng lặp for đi qua danh sách. Mã này có thể được tìm thấy trên kho lưu trữ Github. Tuy nhiên, chúng tôi muốn làm việc với nhiều chủ đề

Do đó, kiến ​​trúc chúng tôi đề xuất là. Một hàng đợi lưu trữ các trang web chúng tôi muốn tải xuống, một hàng đợi lưu trữ dữ liệu sẽ được lưu. Một số chủ đề đi đến các trang web để tải xuống và mỗi chủ đề xuất dữ liệu sang hàng đợi khác. Một số luồng đọc hàng đợi sau và lưu dữ liệu vào đĩa, chú ý không ghi đè lên tệp. Các mô-đun chúng tôi sẽ sử dụng cho ví dụ này là

def modify_variable[var]:
    while True:
        for i in range[len[var]]:
            var[i] += 1
        if event.is_set[]:
            break
        # sleep[.5]
    print['Stop printing']
9

Lưu ý rằng chúng tôi đang sử dụng urllib để tải xuống dữ liệu. Sau đó, chúng tôi tạo hàng đợi và khóa mà chúng tôi sẽ sử dụng

from time import time
[...]

my_var = [1, 2, 3]
t = Thread[target=modify_variable, args=[my_var, ]]
t.start[]
t0 = time[]
while time[]-t0 < 5:
    print[my_var]
    sleep[1]
event.set[]
t.join[]
print[my_var]
0

Bây giờ chúng ta có thể tiến hành xác định các chức năng sẽ chạy trên các luồng riêng biệt. Để tải dữ liệu

from time import time
[...]

my_var = [1, 2, 3]
t = Thread[target=modify_variable, args=[my_var, ]]
t.start[]
t0 = time[]
while time[]-t0 < 5:
    print[my_var]
    sleep[1]
event.set[]
t.join[]
print[my_var]
1

Ở đây bạn thấy rằng chúng tôi đã sử dụng chiến lược kiểm tra xem hàng đợi có yếu tố đặc biệt hay không, để đảm bảo rằng chúng tôi đã xử lý tất cả các trang web trên hàng đợi trước khi dừng luồng. Chúng tôi tải xuống dữ liệu từ trang web và đặt nó vào một hàng đợi khác để xử lý sau

Việc lưu yêu cầu cẩn thận hơn một chút vì chúng tôi phải chắc chắn rằng không có hai luồng nào cố gắng ghi vào cùng một tệp

from time import time
[...]

my_var = [1, 2, 3]
t = Thread[target=modify_variable, args=[my_var, ]]
t.start[]
t0 = time[]
while time[]-t0 < 5:
    print[my_var]
    sleep[1]
event.set[]
t.join[]
print[my_var]
2

Cách tiếp cận tương tự như tải xuống dữ liệu. Chúng tôi đợi cho đến khi một phần tử đặc biệt xuất hiện để dừng luồng. Sau đó, chúng tôi có một khóa để đảm bảo rằng không có luồng nào khác đang xem các tệp có sẵn để ghi vào. Vòng lặp chỉ kiểm tra số tệp nào có sẵn. Chúng tôi phải sử dụng khóa ở đây vì có một thay đổi là hai luồng chạy cùng một dòng và tìm tệp có sẵn giống nhau

Khi chúng tôi ghi vào tệp, chúng tôi không quan tâm đến khóa, bởi vì chúng tôi biết rằng chỉ có một luồng sẽ ghi vào mỗi tệp. Đó là lý do tại sao chúng tôi tạo tệp trên một dòng, trong khi khóa được lấy

from time import time
[...]

my_var = [1, 2, 3]
t = Thread[target=modify_variable, args=[my_var, ]]
t.start[]
t0 = time[]
while time[]-t0 < 5:
    print[my_var]
    sleep[1]
event.set[]
t.join[]
print[my_var]
3

Nhưng chúng tôi ghi dữ liệu trên một dòng riêng biệt, không có khóa

from time import time
[...]

my_var = [1, 2, 3]
t = Thread[target=modify_variable, args=[my_var, ]]
t.start[]
t0 = time[]
while time[]-t0 < 5:
    print[my_var]
    sleep[1]
event.set[]
t.join[]
print[my_var]
4

Điều này có vẻ quá phức tạp đối với mục đích của chúng tôi và đó là sự thật. Tuy nhiên, nó cho thấy một cách tiếp cận khả thi trong đó một số luồng có thể ghi vào ổ cứng cùng một lúc vì chúng đang ghi vào các tệp khác nhau. Lưu ý rằng chúng tôi đã sử dụng

[-8832, -168606, 2567]
6 để mở tệp.
[-8832, -168606, 2567]
7 là vì chúng tôi muốn ghi vào tệp [không nối thêm] và
[-8832, -168606, 2567]
8 vì kết quả của việc đọc
[-8832, -168606, 2567]
9 là nhị phân và không phải là một chuỗi. Sau đó, chúng tôi cần kích hoạt các chủ đề mà chúng tôi muốn tải xuống và lưu dữ liệu. Đầu tiên, chúng tôi tạo một danh sách các trang web chúng tôi muốn tải xuống. Trong trường hợp này, trang chủ Wikipedia bằng các ngôn ngữ khác nhau

from time import time
[...]

my_var = [1, 2, 3]
t = Thread[target=modify_variable, args=[my_var, ]]
t.start[]
t0 = time[]
while time[]-t0 < 5:
    print[my_var]
    sleep[1]
event.set[]
t.join[]
print[my_var]
5

Và sau đó chúng tôi chuẩn bị hàng đợi và kích hoạt chủ đề

from time import time
[...]

my_var = [1, 2, 3]
t = Thread[target=modify_variable, args=[my_var, ]]
t.start[]
t0 = time[]
while time[]-t0 < 5:
    print[my_var]
    sleep[1]
event.set[]
t.join[]
print[my_var]
6

Với điều này, chúng tôi tạo danh sách với các chủ đề đang chạy để lưu và tải xuống. Tất nhiên, những con số có thể khác nhau. Sau đó, chúng tôi cần chắc chắn rằng chúng tôi dừng các chủ đề tải xuống

from time import time
[...]

my_var = [1, 2, 3]
t = Thread[target=modify_variable, args=[my_var, ]]
t.start[]
t0 = time[]
while time[]-t0 < 5:
    print[my_var]
    sleep[1]
event.set[]
t.join[]
print[my_var]
7

Vì chúng tôi chạy 3 luồng để tải xuống dữ liệu, chúng tôi phải chắc chắn rằng chúng tôi đã thêm 3

[-8832, -168606, 2567]
5 vào Hàng đợi, nếu không một số luồng sẽ không dừng. Sau khi chúng tôi chắc chắn rằng quá trình tải xuống đã hoàn tất, chúng tôi có thể dừng lưu

from time import time
[...]

my_var = [1, 2, 3]
t = Thread[target=modify_variable, args=[my_var, ]]
t.start[]
t0 = time[]
while time[]-t0 < 5:
    print[my_var]
    sleep[1]
event.set[]
t.join[]
print[my_var]
8

Và sau đó chúng tôi đợi quá trình lưu kết thúc

from time import time
[...]

my_var = [1, 2, 3]
t = Thread[target=modify_variable, args=[my_var, ]]
t.start[]
t0 = time[]
while time[]-t0 < 5:
    print[my_var]
    sleep[1]
event.set[]
t.join[]
print[my_var]
9

Bây giờ chúng tôi biết tất cả các chủ đề đã kết thúc và hàng đợi trống. Nếu bạn chạy chương trình, bạn có thể xem danh sách 10 tệp được tạo, với HTML của 10 trang chủ Wikipedia khác nhau

Trong bài viết trước, chúng ta đã thấy cách bạn có thể sử dụng luồng để chạy các chức năng khác nhau cùng một lúc và một số công cụ hữu ích nhất mà bạn có sẵn để kiểm soát luồng của các luồng khác nhau. Trong bài viết này, chúng tôi đã thảo luận về cách bạn có thể chia sẻ dữ liệu giữa các luồng, khai thác cả thực tế là bộ nhớ dùng chung giữa các luồng và bằng cách sử dụng hàng đợi

Có quyền truy cập vào bộ nhớ dùng chung giúp các chương trình phát triển rất nhanh, nhưng chúng có thể gây ra sự cố khi các luồng khác nhau đang đọc/ghi vào cùng một phần tử. Điều này đã được thảo luận ở phần đầu của bài viết, trong đó chúng ta khám phá điều gì sẽ xảy ra khi sử dụng một toán tử đơn giản chẳng hạn như

[97998, 133432, 186591]
1 để tăng giá trị của một mảng lên 1. Sau đó, chúng tôi đã khám phá cách sử dụng Hàng đợi để chia sẻ dữ liệu giữa các luồng, cả giữa luồng chính và luồng con cũng như giữa các luồng con

Để kết thúc, chúng tôi đã trình bày một ví dụ rất đơn giản về cách sử dụng các luồng để tải xuống dữ liệu từ một trang web và lưu nó vào đĩa. Ví dụ này rất cơ bản, nhưng chúng tôi sẽ mở rộng nó trong bài viết sau. Các tác vụ IO [đầu vào-đầu ra] khác có thể được khám phá là thu thập dữ liệu từ các thiết bị như máy ảnh, chờ người dùng nhập dữ liệu, đọc từ đĩa, v.v.

Là chuỗi cấu trúc dữ liệu Python

Chúng an toàn cho chuỗi miễn là bạn không tắt mã GIL trong C cho chuỗi .

Tại sao Python không phải là luồng

Python không an toàn theo luồng và ban đầu được thiết kế với một thứ gọi là GIL hoặc Khóa phiên dịch toàn cầu, đảm bảo các quy trình được thực thi tuần tự trên CPU của máy tính. On the surface, this means Python programs cannot support multiprocessing.

Python có cho phép phân luồng không?

Thư viện chuẩn Python cung cấp luồng , chứa hầu hết các nguyên hàm mà bạn sẽ thấy trong bài viết này. Thread , trong mô-đun này, gói gọn các luồng một cách độc đáo, cung cấp giao diện rõ ràng để làm việc với chúng. Khi bạn tạo một Chủ đề, bạn truyền cho nó một hàm và một danh sách chứa các đối số của hàm đó.

Python đơn luồng hay đa luồng?

Python KHÔNG phải là ngôn ngữ đơn luồng . Các quy trình Python thường sử dụng một luồng đơn vì GIL. Bất chấp 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 các triển khai dựa trên C dưới mui xe, cho phép sử dụng nhiều lõi.

Chủ Đề