Thread_local có nghĩa là gì trong C ++ 11?


131

Tôi bối rối với mô tả thread_localtrong C ++ 11. Hiểu biết của tôi là, mỗi luồng có bản sao duy nhất của các biến cục bộ trong một hàm. Các biến toàn cục / tĩnh có thể được truy cập bởi tất cả các luồng (có thể truy cập được đồng bộ hóa bằng cách sử dụng khóa). Và các thread_localbiến được hiển thị cho tất cả các luồng nhưng chỉ có thể sửa đổi bởi các luồng mà chúng được xác định? Nó có đúng không?

Câu trả lời:


150

Thời lượng lưu trữ cục bộ là một thuật ngữ được sử dụng để chỉ dữ liệu dường như là thời lượng lưu trữ toàn cầu hoặc tĩnh (theo quan điểm của các chức năng sử dụng nó) nhưng trên thực tế, có một bản sao cho mỗi luồng.

Nó thêm vào tự động hiện tại (tồn tại trong một khối / chức năng), tĩnh (tồn tại trong thời lượng chương trình) và động (tồn tại trên heap giữa phân bổ và phân bổ).

Một cái gì đó là luồng cục bộ được đưa vào sự tồn tại khi tạo luồng và xử lý khi luồng dừng lại.

Một số ví dụ sau đây.

Hãy nghĩ về một trình tạo số ngẫu nhiên trong đó hạt giống phải được duy trì trên cơ sở mỗi luồng. Sử dụng một hạt giống cục bộ có nghĩa là mỗi luồng có chuỗi số ngẫu nhiên riêng, độc lập với các luồng khác.

Nếu hạt giống của bạn là một biến cục bộ trong hàm ngẫu nhiên, nó sẽ được khởi tạo mỗi khi bạn gọi nó, cho bạn cùng một số mỗi lần. Nếu nó là một toàn cầu, các chủ đề sẽ can thiệp vào trình tự của nhau.

Một ví dụ khác là một strtoknơi như trạng thái mã thông báo được lưu trữ trên cơ sở cụ thể của luồng. Theo cách đó, một luồng duy nhất có thể chắc chắn rằng các luồng khác sẽ không làm hỏng các nỗ lực mã thông báo của nó, trong khi vẫn có thể duy trì trạng thái qua nhiều cuộc gọi đến strtok- điều này về cơ bản là kết xuất lại strtok_r(phiên bản an toàn của luồng).

Cả hai ví dụ này đều cho phép biến cục bộ của luồng tồn tại trong hàm sử dụng nó. Trong mã tiền luồng, nó chỉ đơn giản là một biến thời lượng lưu trữ tĩnh trong hàm. Đối với các luồng, điều đó được sửa đổi theo thời gian lưu trữ cục bộ.

Một ví dụ khác sẽ là một cái gì đó như errno. Bạn không muốn các luồng riêng biệt sửa đổi errnosau khi một trong các cuộc gọi của bạn thất bại nhưng trước khi bạn có thể kiểm tra biến và bạn chỉ muốn một bản sao cho mỗi luồng.

Trang web này có một mô tả hợp lý của các chỉ định thời lượng lưu trữ khác nhau.


4
Sử dụng chủ đề cục bộ không giải quyết vấn đề với strtok. strtokbị hỏng ngay cả trong một môi trường luồng đơn.
James Kanze

11
Xin lỗi, hãy để tôi nói lại điều đó. Nó không giới thiệu bất kỳ vấn đề mới nào với strtok :-)
paxdiablo

7
Trên thực tế, rviết tắt của "re-entrant", không liên quan gì đến an toàn luồng. Đúng là bạn có thể làm cho một số thứ hoạt động an toàn với luồng lưu trữ cục bộ, nhưng bạn không thể làm cho chúng được đăng ký lại.
Kerrek SB

5
Trong một môi trường đơn luồng, các hàm chỉ cần được đăng ký lại nếu chúng là một phần của chu trình trong biểu đồ cuộc gọi. Một hàm lá (một hàm không gọi các hàm khác) theo định nghĩa không phải là một phần của chu trình và không có lý do chính đáng tại sao strtoknên gọi các hàm khác.
MSalters

3
điều này sẽ làm nó rối tung lên: while (something) { char *next = strtok(whatever); someFunction(next); // someFunction calls strtok }
japreiss

135

Khi bạn khai báo một biến thread_localthì mỗi luồng có bản sao riêng. Khi bạn gọi nó theo tên, thì bản sao được liên kết với chuỗi hiện tại sẽ được sử dụng. ví dụ

thread_local int i=0;

void f(int newval){
    i=newval;
}

void g(){
    std::cout<<i;
}

void threadfunc(int id){
    f(id);
    ++i;
    g();
}

int main(){
    i=9;
    std::thread t1(threadfunc,1);
    std::thread t2(threadfunc,2);
    std::thread t3(threadfunc,3);

    t1.join();
    t2.join();
    t3.join();
    std::cout<<i<<std::endl;
}

Mã này sẽ xuất ra "2349", "3249", "4239", "4329", "2439" hoặc "3429", nhưng không bao giờ có gì khác. Mỗi luồng có bản sao riêng của nó i, được gán cho, tăng lên và sau đó được in. Các luồng đang chạy maincũng có bản sao riêng của nó, được gán ở đầu và sau đó không thay đổi. Các bản sao này là hoàn toàn độc lập, và mỗi bản có một địa chỉ khác nhau.

Đó chỉ là cái tên đặc biệt trong khía cạnh đó --- nếu bạn lấy địa chỉ của một thread_localbiến thì bạn chỉ cần có một con trỏ bình thường đến một đối tượng bình thường, mà bạn có thể tự do chuyển qua giữa các luồng. ví dụ

thread_local int i=0;

void thread_func(int*p){
    *p=42;
}

int main(){
    i=9;
    std::thread t(thread_func,&i);
    t.join();
    std::cout<<i<<std::endl;
}

Vì địa chỉ của iđược truyền cho hàm luồng, nên bản sao ithuộc về luồng chính có thể được gán cho mặc dù vậy thread_local. Chương trình này do đó sẽ xuất "42". Nếu bạn làm điều này, thì bạn cần phải cẩn thận *pkhông truy cập được sau khi luồng mà nó thuộc về đã thoát, nếu không bạn sẽ có một con trỏ lơ lửng và hành vi không xác định giống như bất kỳ trường hợp nào khác khi đối tượng trỏ bị phá hủy.

thread_localcác biến được khởi tạo "trước khi sử dụng lần đầu", vì vậy nếu chúng không bao giờ được chạm bởi một luồng nhất định thì chúng không nhất thiết phải được khởi tạo. Điều này là để cho phép trình biên dịch tránh xây dựng mọi thread_localbiến trong chương trình cho một luồng hoàn toàn độc lập và không chạm vào bất kỳ biến nào trong số chúng. ví dụ

struct my_class{
    my_class(){
        std::cout<<"hello";
    }
    ~my_class(){
        std::cout<<"goodbye";
    }
};

void f(){
    thread_local my_class unused;
}

void do_nothing(){}

int main(){
    std::thread t1(do_nothing);
    t1.join();
}

Trong chương trình này có 2 luồng: luồng chính và luồng được tạo thủ công. Không có cuộc gọi chủ đề f, vì vậy thread_localđối tượng không bao giờ được sử dụng. Do đó, không xác định liệu trình biên dịch sẽ xây dựng 0, 1 hoặc 2 trường hợp my_classvà đầu ra có thể là "", "hellohellogoodbyegoodbye" hoặc "hellogoodbye".


1
Tôi nghĩ điều quan trọng cần lưu ý là bản sao luồng của biến là một bản sao mới được khởi tạo của biến. Nghĩa là, nếu bạn thêm một g()cuộc gọi đến đầu threadFunc, sau đó sản lượng sẽ được 0304029hoặc một số hoán vị khác của các cặp 02, 0304. Đó là, mặc dù 9 được gán cho itrước khi các luồng được tạo, các luồng có được một bản sao được xây dựng mới iở đâu i=0. Nếu iđược gán với thread_local int i = random_integer(), thì mỗi luồng sẽ nhận được một số nguyên ngẫu nhiên mới.
Đánh dấu H

Không hẳn là một hoán vị của 02, 03, 04, có thể có trình tự khác như020043
Hongxu Chen

Tidbit thú vị tôi vừa tìm thấy: GCC hỗ trợ sử dụng địa chỉ của biến thread_local làm đối số mẫu, nhưng các trình biên dịch khác thì không (như cách viết này; đã thử clang, vstudio). Tôi không chắc tiêu chuẩn nói gì về điều đó, hoặc nếu đây là một khu vực không xác định.
JWD

23

Lưu trữ luồng cục bộ nằm trong mọi khía cạnh như lưu trữ tĩnh (= toàn cầu), chỉ có mỗi luồng có một bản sao riêng của đối tượng. Thời gian sống của đối tượng bắt đầu khi bắt đầu luồng (đối với biến toàn cục) hoặc ở lần khởi tạo đầu tiên (đối với thống kê khối cục bộ) và kết thúc khi luồng kết thúc (tức là khi join()được gọi).

Do đó, chỉ các biến cũng staticcó thể được khai báo thread_locallà biến toàn cục (chính xác hơn là: biến "ở phạm vi không gian tên"), thành viên lớp tĩnh và biến tĩnh khối (trong trường hợp staticnày được ngụ ý).

Ví dụ: giả sử bạn có một nhóm luồng và muốn biết mức độ cân bằng công việc của bạn được cân bằng:

thread_local Counter c;

void do_work()
{
    c.increment();
    // ...
}

int main()
{
    std::thread t(do_work);   // your thread-pool would go here
    t.join();
}

Điều này sẽ in số liệu thống kê sử dụng luồng, ví dụ như với một triển khai như thế này:

struct Counter
{
     unsigned int c = 0;
     void increment() { ++c; }
     ~Counter()
     {
         std::cout << "Thread #" << std::this_thread::id() << " was called "
                   << c << " times" << std::endl;
     }
};
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.