“Lưu trữ cục bộ chuỗi” trong Python là gì và tại sao tôi cần nó?


100

Cụ thể trong Python, làm cách nào để các biến được chia sẻ giữa các luồng?

Mặc dù tôi đã sử dụng threading.Threadtrước đây nhưng tôi chưa bao giờ thực sự hiểu hoặc xem các ví dụ về cách các biến được chia sẻ. Chúng được chia sẻ giữa chủ đề chính và con cái hay chỉ giữa các con? Khi nào tôi cần sử dụng lưu trữ cục bộ của chuỗi để tránh chia sẻ này?

Tôi đã thấy nhiều cảnh báo về việc đồng bộ hóa quyền truy cập vào dữ liệu được chia sẻ giữa các luồng bằng cách sử dụng khóa nhưng tôi vẫn chưa thấy một ví dụ thực sự tốt về vấn đề này.

Cảm ơn trước!


2
Tiêu đề không phù hợp với câu hỏi. Câu hỏi đặt ra là phải làm gì với chia sẻ biến giữa các chủ đề, tiêu đề ngụ ý rằng nó được cụ thể về sợi local storage
Casebash

2
@Casebash: từ âm thanh của câu hỏi này, Mike đọc rằng TLS là cần thiết để tránh các vấn đề do dữ liệu được chia sẻ gây ra, nhưng không rõ dữ liệu được chia sẻ theo mặc định, dữ liệu được chia sẻ với cái gì và cách nó được chia sẻ. Tôi đã điều chỉnh tiêu đề để phù hợp hơn với câu hỏi.
Shog9

Câu trả lời:


83

Trong Python, mọi thứ đều được chia sẻ, ngoại trừ các biến cục bộ của hàm (vì mỗi lệnh gọi hàm có một tập hợp cục bộ riêng và các luồng luôn là các lệnh gọi hàm riêng biệt.) Và thậm chí sau đó, chỉ bản thân các biến (tên tham chiếu đến các đối tượng) là địa phương cho chức năng; bản thân các đối tượng luôn mang tính toàn cục và bất cứ thứ gì cũng có thể tham chiếu đến chúng. Đối Threadtượng cho một chủ đề cụ thể không phải là một đối tượng đặc biệt về mặt này. Nếu bạn lưu trữ Threadđối tượng ở một nơi nào đó mà tất cả các luồng có thể truy cập (như một biến toàn cục) thì tất cả các luồng có thể truy cập một Threadđối tượng đó. Nếu bạn muốn sửa đổi nguyên tử bất kỳ thứ gì mà một luồng khác có quyền truy cập, bạn phải bảo vệ nó bằng khóa. Và tất nhiên tất cả các chủ đề phải chia sẻ cùng một khóa này, nếu không nó sẽ không hiệu quả lắm.

Nếu bạn muốn lưu trữ cục bộ luồng thực tế, thì đó là nơi threading.localxuất hiện. Các thuộc tính của threading.localkhông được chia sẻ giữa các luồng; mỗi luồng chỉ nhìn thấy các thuộc tính mà bản thân nó được đặt trong đó. Nếu bạn tò mò về cách triển khai của nó, thì nguồn nằm trong _threading_local.py trong thư viện chuẩn.


1
Bạn có thể cho biết thêm chi tiết về câu sau được không? "Nếu bạn muốn sửa đổi nguyên tử bất kỳ thứ gì mà bạn không chỉ tạo trong chính chuỗi này và không lưu trữ bất kỳ nơi nào mà một chuỗi khác có thể lấy được, bạn phải bảo vệ nó bằng một khóa."
changyuheng

@changyuheng: Đây là lời giải thích về các hành động nguyên tử là gì: cs.nott.ac.uk/~psznza/G52CON/lecture4.pdf
Tom Busby

1
@TomBusby: Nếu không có bất kỳ luồng nào khác có thể xâm nhập vào nó, tại sao chúng ta cần phải bảo vệ nó bằng một khóa, tức là tại sao chúng ta cần thực hiện quy trình nguyên tử?
changyuheng 20/09/18

2
Bạn có thể đưa ra một ví dụ nhanh về: "bản thân các đối tượng luôn mang tính toàn cục và bất cứ thứ gì cũng có thể tham chiếu đến chúng". Bằng cách tham khảo, giả sử bạn có nghĩa là đọc và không gán / nối thêm?
biến

@variable: Tôi nghĩ anh ấy có nghĩa là các giá trị không có phạm vi
dùng1071847

75

Hãy xem xét đoạn mã sau:

#/usr/bin/env python

from time import sleep
from random import random
from threading import Thread, local

data = local()

def bar():
    print("I'm called from", data.v)

def foo():
    bar()

class T(Thread):
    def run(self):
        sleep(random())
        data.v = self.getName()   # Thread-1 and Thread-2 accordingly
        sleep(1)
        foo()
>> T (). Start (); T (). Start ()
Tôi được gọi từ Thread-2
Tôi được gọi từ Thread-1 

Ở đây threading.local () được sử dụng như một cách nhanh chóng và dễ dàng để chuyển một số dữ liệu từ run () sang bar () mà không làm thay đổi giao diện của foo ().

Lưu ý rằng việc sử dụng các biến toàn cục sẽ không thực hiện được mẹo:

#/usr/bin/env python

from time import sleep
from random import random
from threading import Thread

def bar():
    global v
    print("I'm called from", v)

def foo():
    bar()

class T(Thread):
    def run(self):
        global v
        sleep(random())
        v = self.getName()   # Thread-1 and Thread-2 accordingly
        sleep(1)
        foo()
>> T (). Start (); T (). Start ()
Tôi được gọi từ Thread-2
Tôi được gọi từ Thread-2 

Trong khi đó, nếu bạn có đủ khả năng chuyển dữ liệu này qua như một đối số của foo () - thì đó sẽ là một cách thanh lịch và được thiết kế tốt hơn:

from threading import Thread

def bar(v):
    print("I'm called from", v)

def foo(v):
    bar(v)

class T(Thread):
    def run(self):
        foo(self.getName())

Nhưng điều này không phải lúc nào cũng khả thi khi sử dụng mã của bên thứ ba hoặc được thiết kế kém.


18

Bạn có thể tạo lưu trữ cục bộ chuỗi bằng cách sử dụng threading.local().

>>> tls = threading.local()
>>> tls.x = 4 
>>> tls.x
4

Dữ liệu được lưu trữ trong tls sẽ là duy nhất cho mỗi luồng sẽ giúp đảm bảo rằng việc chia sẻ không chủ ý không xảy ra.


2

Cũng giống như mọi ngôn ngữ khác, mọi luồng trong Python đều có quyền truy cập vào các biến giống nhau. Không có sự phân biệt giữa 'luồng chính' và luồng con.

Một điểm khác biệt với Python là Global Interpreter Lock có nghĩa là chỉ một luồng có thể chạy mã Python tại một thời điểm. Tuy nhiên, điều này không giúp ích nhiều khi nói đến việc đồng bộ hóa quyền truy cập, vì tất cả các vấn đề về tính trước thông thường vẫn áp dụng và bạn phải sử dụng nguyên thủy phân luồng giống như trong các ngôn ngữ khác. Tuy nhiên, điều đó có nghĩa là bạn cần phải xem xét lại nếu bạn đang sử dụng các luồng cho hiệu suất.


0

Tôi có thể sai ở đây. Nếu bạn biết cách khác, vui lòng giải thích vì điều này sẽ giúp giải thích lý do tại sao một người cần sử dụng luồng cục bộ ().

Câu nói này có vẻ sai, không sai: "Nếu bạn muốn sửa đổi nguyên tử bất kỳ thứ gì mà một luồng khác có quyền truy cập, bạn phải bảo vệ nó bằng một khóa." Tôi nghĩ câu nói này -> hiệu quả <- đúng nhưng không hoàn toàn chính xác. Tôi nghĩ rằng thuật ngữ "nguyên tử" có nghĩa là trình thông dịch Python đã tạo ra một đoạn mã byte không còn chỗ cho tín hiệu ngắt đến CPU.

Tôi nghĩ rằng các hoạt động nguyên tử là các đoạn mã byte Python không cho phép truy cập vào các ngắt. Các câu lệnh Python như "running = True" là nguyên tử. Bạn không cần phải khóa CPU khỏi ngắt trong trường hợp này (tôi tin rằng). Phân tích mã byte Python an toàn không bị gián đoạn luồng.

Mã Python như "thread_running [5] = True" không phải là nguyên tử. Có hai đoạn mã byte Python ở đây; một để hủy tham chiếu danh sách () cho một đối tượng và một đoạn mã byte khác để gán giá trị cho một đối tượng, trong trường hợp này là "vị trí" trong danh sách. Một ngắt có thể được nâng lên -> giữa <- hai đoạn mã byte -> <-. Đó là điều tồi tệ xảy ra.

Làm thế nào để thread local () liên quan đến "nguyên tử"? Đây là lý do tại sao tuyên bố dường như đi sai hướng đối với tôi. Nếu không bạn có thể giải thích?


1
Điều này trông giống như một câu trả lời, nhưng được báo cáo là có vấn đề, tôi cho rằng do các câu hỏi được đặt ra. Tôi sẽ tránh yêu cầu làm rõ trong câu trả lời. Đây là những gì bình luận dành cho.
Dharman
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.