Hướng dẫn share data between threads python - chia sẻ dữ liệu giữa các chuỗi python

Hướng dẫn share data between threads python - chia sẻ dữ liệu giữa các chuỗi python

Tìm hiểu cách chia sẻ dữ liệu giữa các luồng

Ngày 6 tháng 8 năm 2019threadingthreadsdatasharing threading threads data sharing

Khi làm việc với các chủ đề trong Python, bạn sẽ thấy rất hữu ích để có thể chia sẻ dữ liệu giữa các tác vụ khác nhau. Một trong những lợi thế của các chủ đề trong Python là chúng chia sẻ cùng một không gian bộ nhớ và do đó trao đổi thông tin là 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 tôi đã đề 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.

Bộ nhớ chia sẻ

Cách tiếp cận đầu tiên và ngây thơ nhất là sử dụng các biến tương tự 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. Hãy xem cách chúng ta có thể sử dụng bộ nhớ chia sẻ 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, đây là danh sách các số. Chủ đề sẽ tăng các giá trị của các số lên một, với một độ 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 chủ đề một cách ân cần, nếu bạn không quen thuộc với chúng, hãy kiểm tra hướng dẫn trước đó.

Phần mã quan trọng trong ví dụ này là dòng

[6563461, 6563462, 6563463]
1. Câu lệnh in đó sống trong chuỗi chính, tuy nhiên, nó có quyền truy cập vào thông tin được tạo trong một luồng con. Hành vi này là có thể nhờ chia sẻ bộ nhớ giữa các chủ đề 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 chủ đề, 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 luồng. Điều này tốt cho các ứng dụng như ở trên, trong đó không quan trọng chủ đề nào thêm một ứng dụng vào biến. Hay nó? Hãy sửa đổi một chút mã chạy trong luồ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ó giấc ngủ giữa một lần lặp và tiếp theo. Hãy chạy nó trong một thời gian ngắn, giả sử 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 đã đàn áp các phần của mã lặp lại. Nếu bạn chạy mã này, bạn sẽ nhận được đầu ra số rất lớn. Trong trường hợp của tôi, tôi đã nhận được:

[6563461, 6563462, 6563463]

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

[6563461, 6563462, 6563463]
4 và chúng tôi đang thêm một 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ó một đầu ra 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, có nghĩa là chạy hai luồng thay vì một luồng thực sự có thể chậm hơn cho hoạt động này. Một điều khác cần lưu ý là các giá trị không liên tiếp cho 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 nghĩ thực sự khó khăn, bạn có thể giải thích vấn đề này đến từ đâu không?

Trong hướng dẫn trước đây, chúng tôi đã thảo luận rằng các luồng được xử lý bởi hệ điều hành, quyết định khi nào nên quay một hoặc tắt. Chúng tôi không kiểm soát được những gì hệ điều hành quyết định làm. 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 và bắt đầu một chủ đề khác. Tuy nhiên, điều đó không giải thích hoàn toàn đầu ra mà chúng ta đang nhận được. Không quan trọng nếu một luồng chạy đầu tiên 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 mã trên là trong dòng

[6563461, 6563462, 6563463]
7, thực sự là hai hoạt động. Đầu tiên, nó sao chép giá trị từ
[6563461, 6563462, 6563463]
8 và ADS
[6563461, 6563462, 6563463]
9. Sau đó, nó lưu trữ giá trị trở lại
[6563461, 6563462, 6563463]
8. Ở giữa hai hoạt động này, hệ điều hành có thể quyết định chuyển từ nhiệm vụ này sang tác vụ khác. Trong trường hợp như vậy, giá trị cả hai nhiệm vụ 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ỉ làm điều đó một lần. Nếu bạn muốn làm điều đó thậm chí còn đáng chú ý hơn, bạn có thể bắt đầu hai luồng, một luồng thêm và một luồng trừ đi từ một danh sách và điều đó sẽ cho bạn một gợi ý nhanh về chủ đề 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ó vào một thời điểm khác, tôi sẽ nhận được:

[97998, 133432, 186591]

!!! !!! Lưu ý Bạn có thể nhận thấy rằng có sự chậm 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 một lợi thế nhất định cho luồng đầu tiên bắt đầu. Tuy nhiên, điều đó một mình không thể giải thích đầu ra được tạo ra.

Cách đồng bộ hóa truy cập dữ liệu

Để giải quyết vấn đề chúng tôi tìm thấy trong các ví dụ trước, chúng tôi phải chắc chắn rằng không có hai luồng nào cố gắng viết cùng một lúc cho cùng một biến. Vì vậy, chúng ta có thể sử 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)
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 bạn chạy lại mã, 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 biến tại một thời điểm.

Các ví dụ về việc tăng hoặc giảm giá trị từ danh sách gần như là tầm thường, nhưng chúng chỉ ra hướng hiểu các biến chứng của 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 tốt, nhưng nó cũng đi kèm với rủi ro.

Hàng đợi

Một trong những tình huống phổ biến trong đó các chủ đề được sử dụng là khi bạn có một số nhiệm 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 một thứ khác. Nếu bạn muốn tải xuống toàn bộ một trang web (còn được gọi là cạo), đó sẽ là một giải pháp tốt để tải xuống một số 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, cuối cùng bạn có thể 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ột đối tượng khác có thể rất hữu ích khi làm việc với các chủ đề: hàng đợi. Hàng đợi là một đối tượng chấp nhận dữ liệu theo thứ tự, tức là bạn đặt dữ liệu cho nó một yếu tố tại một thời điểm. Sau đó, dữ liệu có thể được tiêu thụ theo cùng một thứ tự, được gọi là lần đầu tiên (FIFO). Một ví dụ rất đơn giản sẽ là:Queues. A queue is an object which accepts data in order, i.e. you put data to it one element at a time. Then, the data can be consumed in the same order, called First-in-first-out (FIFO). A very simple example would be:

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 ra 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
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 cùng thứ tự mà chúng được thêm vào hàng đợi.

Quay trở lại các ví dụ từ đầu bài viết, chúng ta có thể sử dụng hàng đợi để chia sẻ thông tin giữa các chủ đề. Chúng ta có thể sửa đổi chức năng sao cho thay vì một danh sách là một đối số, nó chấp nhận một hàng đợi mà nó sẽ đọc các yếu tố. Sau đó, nó sẽ xuất kết quả vào 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 ta 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 bị đảo ngược. Trong trường hợp đó, trên chủ đề đặt đầu ra của nó vào hàng đợi của luồng thứ hai và cách khác. Điều này sẽ trông 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? Hãy thử với cách tiếp cận khoa học là phân chia vấn đề và xem xét từng phần. 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 cố gắng chạy phần còn lại của mã. Chúng tôi có thể theo dõi thời gian thực sự dành cho việc điều hành phần quan trọng trong 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 chủ đề mới. Nếu chúng tôi chạy lại mã, đầu ra bạn nên nhận là một cái gì đó 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 số 5 giây trong đó chương trình của chúng tôi chạy, chỉ trong khoảng 0,9 mili giây, chúng tôi thực sự đang làm một cái gì đó. Đây là 0,01% thời gian! Hãy nhanh chóng thấy điều gì xảy ra nếu chúng ta thay đổi mã chỉ sử dụng một hàng đợi thay vì hai, tức là 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

Đó là tốt hơn nhiều! Trong khoảng 5 giây trong đó chương trình chạy, các luồng chạy trong tổng số 8 giây. Đó là những gì người ta sẽ mong đợi về song song. 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 có thể cố gắng đoán những gì đã làm cho chương trình của chúng tôi quá chậm nếu chúng tôi sử dụng hai hàng đợi nhưng nhanh chóng hợp lý nếu chúng tôi sử dụng cùng một hàng đợi cho đầu ra và đầu vào? Bạn phải nhớ rằng khi bạn sử dụng các chủ đề một cách mù quáng như chúng tôi đã làm trong ví dụ trước, chúng tôi để mọi thứ trong tay của hệ điều hành.

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

Nếu bạn muốn xem liệu 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
[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 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à 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 chung 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 đầu ra 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

Trong đó rõ ràng là hầu hết thời gian chương trình chỉ chờ cho đến khi có nhiều dữ liệu trên hàng đợi. Vì chúng tôi đang ngủ 1 ms mỗi khi không có dữ liệu, chúng tôi thực sự 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ủ, hầu hết thời gian thói quen của chúng ta thực sự 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 không và thực sự đọc từ nó, có thể xảy ra rằng luồng khác đã lấy kết quả. Điều này được mô tả trong tài liệu hàng đợi. Trừ khi chúng tôi bao gồm 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 cho các lệnh
[5738447, 5686971, 5684220]
2 hoặc
[5738447, 5686971, 5684220]
3.

Các tùy chọn bổ sung của hàng đợi

Hàng đợi có một số tùy chọn bổ sung, chẳng hạn như số lượng yếu tố tối đa mà họ có thể nắm giữ. Bạn cũng có thể xác định các loại hàng đợi LIFO (lần cuối, đầu tiên) mà bạn có thể đọc trong tài liệu. Đ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 các luồng, ngoại lệ tùy chỉnh và tài liệu.LIFO (last-in, first-out) types of queues, which you can read about in the documentation. What I find more useful about
[5738447, 5686971, 5684220]
4 is that they are written in pure Python. If you visit their source code, you can learn a lot about synchronization in threads, custom exceptions, and documenting.

Điều quan trọng cần lưu ý là khi bạn làm việc với nhiều luồng, đôi khi bạn muốn chờ đợi (tức là 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 hay không trước khi đọc từ nó. Nhưng điều gì xảy ra nếu chúng ta không kiểm tra nó? Phương pháp

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

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

[5738447, 5686971, 5684220]
9 để tận dụng lợi 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')
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 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')
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ì thời gian mã thực sự đang chạy 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, có lẽ chúng ta nên tính thời gian khác nhau 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 để chặn trong khi chờ đợi, chúng ta 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 chờ đợi (

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

Hàng đợi để dừng các chủ đề

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

[-8832, -168606, 2567]
5 vào hàng đợi và khi chức năng nhận được nó, 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 tôi muốn dừng các luồng, chúng tôi 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 sẽ chọn một hoặc tùy chọn khác, 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 1 yếu tố nhiều nhất. Khi chúng tôi dừng chương trình, chúng tôi 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 yếu tố, không có mối quan hệ giữa nhau. Đây sẽ là trường hợp nếu bạn sẽ tải xuống dữ liệu từ một trang web, ví dụ hoặc xử lý hình ảnh, v.v. Bạn muốn chắc chắn rằng bạn hoàn thành việc xử lý 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 yếu tố sẽ được xử lý.

!!! !!! Cảnh báo Đó là một ý tưởng rất khôn ngoan để chắc chắn rằng một 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 nhìn vào trạng thái của khóa, hàng đợi có thể được để lại nhiều dữ liệu trong đó và do đó bộ nhớ sẽ không được giải phóng. Một vòng trong khi có tất cả các yếu tố của hàng đợi giải quyết nó.

Chủ đề IO ràng buộc

Các ví dụ trong bài viết này rất chuyên sâu về mặt tính toán, và do đó chúng nằm ngay bên cạnh nơi sử dụng đa luồng không được áp dụng và nơi tất cả các vấn đề phát sinh (như đồng thời, v.v.) Nếu bạn hiểu chúng, bạn sẽ lập trình với sự tự tin hơn nhiều. Bạn sẽ không ở trên ngón chân của mình với hy vọng một vấn đề không phát sinh.

Một khu vực có các excels đa luồng nằm 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, việc viết vào ổ cứng có thể được giảm tải một cách an toàn vào một luồng riêng biệt, trong khi phần còn lại của chương trình tiếp tục chạy. Điều này cũng hợp lệ nếu chương trình chờ đợi đầu vào của người dùng hoặc tài nguyên mạng có sẵn, tải xuống dữ liệu từ Internet, v.v.

Ví dụ tải xuống các trang web

Để đóng bài viết này, hãy xem một ví dụ về việc tải xuống các trang web bằng cách sử dụng các luồng, hàng đợi và khóa. Ngay cả khi một số cải tiến hiệu suất là có thể, ví dụ 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 quan tâm.

Đầu tiên, chúng ta hãy thảo luận về những gì chúng ta muốn đạt được. Để giữ 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 sẽ là tạo ra một vòng lặp đ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 mà chúng tôi đề xuất là: một hàng đợi lưu trữ các trang web mà 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ủ đề sẽ đến các trang web để tải xuống và mỗi chủ đề sẽ xuất dữ liệu đến hàng đợi khác. Một số chủ đề đọc hàng đợi sau và lưu dữ liệu vào đĩa, chăm sóc không ghi đè các 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 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 xuống 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, để chắc chắn 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à chúng tôi đặt nó vào một hàng đợi khác để được xử lý sau này.

Việc tiết kiệm đòi hỏi phải quan tâm 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ư việc tải xuống dữ liệu. Chúng tôi đợi cho đến khi một yếu tố đặc biệt có mặt để dừng chủ đề. Sau đó, chúng tôi có được một khóa để chắc chắn rằng không có chủ đề 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ó hai thay đổi hai luồng chạy cùng một dòng cùng một lúc và tìm tệp có sẵn là như 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 thu nhận:

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 viết dữ liệu trên một dòng riêng biệt mà không cần 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 cho 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 có thể trong đó một số luồng có thể viết vào ổ cứng cùng một lúc vì chúng đang viết 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à do chúng tôi muốn ghi vào tệp (không nối tiếp) 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 ta cần kích hoạt các luồng chúng ta 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 các 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ủ đề 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 nối 3

[-8832, -168606, 2567]
5 vào hàng đợi hoặc một số chủ đề sẽ không dừng lại. Sau khi chúng tôi chắc chắn rằng việc tải xuống kết thúc, chúng tôi có thể dừng tiết kiệm:

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 chờ đợi để tiết kiệm 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ể thấy danh sách 10 tệp được tạo, với HTML gồm 10 trang chủ Wikipedia khác nhau.

Kết luận

Trong bài viết trước, chúng tôi đã 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 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ế của bộ nhớ chia sẻ 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ớ chia sẻ làm cho các chương trình rất nhanh chóng phát triển, nhưng chúng có thể làm phát sinh các vấn đề khi các chủ đề khác nhau đang đọc/ghi vào cùng một yếu tố. Điều này đã được thảo luận khi bắt đầu bài viết, trong đó chúng tôi đã khám phá những gì xảy ra khi sử dụng một toán tử đơn giản như

[97998, 133432, 186591]
1 để tăng các 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 Chủ đề chính và chủ đề con như giữa các chủ đề con.

Để hoàn thành, chúng tôi đã chỉ ra một ví dụ rất đơn giản về cách sử dụng các chủ đề để tải xuống dữ liệu từ một trang web và lưu nó vào đĩa. Ví dụ là 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 (Input-Output) khác có thể được khám phá là mua lại từ các thiết bị như máy ảnh, chờ đầu vào của người dùng, đọc từ đĩa, v.v.

Bài báo được viết bởi Aquiles Carattino

Minh họa tiêu đề của Tsvetelina Stoynova

Làm thế nào tôi có thể chia sẻ dữ liệu giữa hai luồng?

Tất cả dữ liệu tĩnh và được kiểm soát được chia sẻ giữa các luồng. Tất cả các dữ liệu khác cũng có thể được chia sẻ thông qua các đối số/tham số và thông qua các tham chiếu dựa trên, miễn là dữ liệu được phân bổ và không được giải phóng cho đến khi tất cả các luồng đã hoàn thành bằng cách sử dụng dữ liệu.through arguments/parameters and through based references, as long as the data is allocated and is not freed until all of the threads have finished using the data.

Chủ đề Python có chia sẻ bộ nhớ không?

Các quá trình mỗi người có nhóm bộ nhớ riêng.Điều này có nghĩa là chậm để sao chép một lượng lớn dữ liệu vào chúng hoặc từ chúng.Ví dụ: khi chạy các chức năng trên các mảng đầu vào lớn hoặc khung dữ liệu.Các chủ đề có chung bộ nhớ với phiên Python chính, do đó không cần phải sao chép dữ liệu vào hoặc từ chúng.Threads share the same memory as the main Python session, so there is no need to copy data to or from them.

Chủ đề Python có thể truy cập biến toàn cầu không?

Nhưng để trả lời câu hỏi của bạn, bất kỳ chủ đề nào cũng có thể truy cập bất kỳ biến toàn cầu nào hiện tại trong phạm vi.Không có khái niệm về việc chuyển các biến cho một chủ đề.Nó có một biến toàn cầu duy nhất với chức năng getter và setter, bất kỳ luồng nào cũng có thể gọi là getter hoặc setter bất cứ lúc nào.any thread can access any global variable currently in scope. There is no notion of passing variables to a thread. It has a single global variable with a getter and setter function, any thread can call either the getter or setter at any time.

Các biến toàn cầu có được chia sẻ giữa các chủ đề không?

Chủ đề chia sẻ tất cả các biến toàn cầu;Không gian bộ nhớ nơi các biến toàn cầu được lưu trữ được chia sẻ bởi tất cả các luồng (mặc dù, như chúng ta sẽ thấy, bạn phải rất cẩn thận về việc truy cập một biến toàn cầu từ nhiều luồng).Điều này bao gồm các thành viên Static lớp!; the memory space where global variables are stored is shared by all threads (though, as we will see, you have to be very careful about accessing a global variable from multiple threads). This includes class-static members!