Điều gì xảy ra với một luồng tách ra khi main () thoát?


152

Giả sử tôi đang bắt đầu std::threadvà sau detach()đó, vì vậy luồng tiếp tục thực thi mặc dù cái std::threadđã từng đại diện cho nó, đi ra khỏi phạm vi.

Giả sử thêm rằng chương trình không có giao thức đáng tin cậy để tham gia luồng tách rời 1 , do đó, luồng tách rời vẫn chạy khi main()thoát.

Tôi không thể tìm thấy bất cứ điều gì trong tiêu chuẩn (chính xác hơn là trong bản nháp N3797 C ++ 14), trong đó mô tả những gì sẽ xảy ra, cả 1.10 và 30.3 đều không chứa từ ngữ thích hợp.

1 Một câu hỏi khác, có lẽ tương đương, là: "một chuỗi tách rời có thể được nối lại không", bởi vì bất kỳ giao thức nào bạn phát minh ra để tham gia, phần báo hiệu sẽ phải được thực hiện trong khi luồng vẫn đang chạy và bộ lập lịch của hệ điều hành có thể quyết định đặt luồng vào trạng thái ngủ trong một giờ ngay sau khi tín hiệu được thực hiện mà không có cách nào để đầu nhận nhận được phát hiện một cách đáng tin cậy rằng luồng thực sự kết thúc.

Nếu chạy ra main()với các luồng tách rời đang chạy là hành vi không xác định, thì bất kỳ việc sử dụng nàostd::thread::detach() là hành vi không xác định trừ khi luồng chính không bao giờ thoát 2 .

Vì vậy, chạy ra main()với các luồng tách rời đang chạy phải có hiệu ứng xác định . Câu hỏi là: ở đâu (trong tiêu chuẩn C ++ , không phải POSIX, không phải tài liệu hệ điều hành, ...) là những hiệu ứng được xác định.

2 Một sợi chỉ tách rời không thể được nối (theo nghĩa std::thread::join()). Bạn có thể đợi kết quả từ các luồng được tách ra (ví dụ: thông qua một tương lai từ std::packaged_taskhoặc bằng một semaphore đếm hoặc cờ và một biến điều kiện), nhưng điều đó không đảm bảo rằng luồng đã thực hiện xong . Thật vậy, trừ khi bạn đặt phần tín hiệu vào destructor của các đối tượng tự động đầu tiên của chủ đề, có sẽ , nói chung, là mã (destructor) mà chạy sau khi mã tín hiệu. Nếu HĐH lên lịch cho luồng chính để tiêu thụ kết quả và thoát ra trước khi luồng tách rời kết thúc chạy các hàm hủy nói trên, thì ^ Wis sẽ xác định điều gì sẽ xảy ra?


5
Tôi chỉ có thể tìm thấy một ghi chú không bắt buộc rất mơ hồ trong [basic.start.term] / 4: "Chấm dứt mọi luồng trước khi một cuộc gọi đến std::exithoặc thoát khỏi mainlà đủ, nhưng không cần thiết, để đáp ứng các yêu cầu này." (toàn bộ đoạn có thể có liên quan) Cũng xem [support.start.term] / 8 ( std::exitđược gọi khi maintrả về)
dyp

Câu trả lời:


45

Câu trả lời cho câu hỏi ban đầu "điều gì xảy ra với một chuỗi bị tách ra khi main()thoát" là:

Nó tiếp tục chạy (vì tiêu chuẩn không nói là nó bị dừng) và điều đó được xác định rõ, miễn là nó không chạm vào các biến (tự động | thread_local) của các luồng khác cũng như các đối tượng tĩnh.

Điều này dường như được phép cho phép các trình quản lý luồng dưới dạng các đối tượng tĩnh (lưu ý trong [basic.start.term] / 4 nói càng nhiều, nhờ @dyp cho con trỏ).

Các vấn đề phát sinh khi việc phá hủy các đối tượng tĩnh đã kết thúc, bởi vì sau đó thực thi vào một chế độ trong đó chỉ có mã được cho phép trong trình xử lý tín hiệu mới có thể thực thi ( [basic.start.term] / 1, câu 1 ). Trong thư viện chuẩn C ++, đó chỉ là <atomic>thư viện ( [support.r Yoon] / 9, câu 2 ). Cụ thể, mà nói chung, thì loại trừ condition_variable đó ( loại trừ đó được xác định theo triển khai cho dù đó là tiết kiệm để sử dụng trong trình xử lý tín hiệu, bởi vì nó không phải là một phần của <atomic>).

Trừ khi bạn mở khóa ngăn xếp của mình tại thời điểm này, thật khó để biết cách tránh hành vi không xác định.

Câu trả lời cho câu hỏi thứ hai "có thể tách các chủ đề từng được nối lại" là:

Vâng, với các *_at_thread_exitgia đình của các chức năng ( notify_all_at_thread_exit(), std::promise::set_value_at_thread_exit(), ...).

Như đã lưu ý trong chú thích [2] của câu hỏi, việc báo hiệu một biến điều kiện hoặc một semaphore hoặc bộ đếm nguyên tử là không đủ để tham gia một chuỗi tách rời (theo nghĩa là đảm bảo rằng kết thúc thực hiện của nó đã xảy ra - trước khi nhận được cho biết tín hiệu bởi một luồng chờ), bởi vì, nói chung, sẽ có nhiều mã được thực thi sau ví dụ như một notify_all()biến điều kiện, đặc biệt là các hàm hủy của các đối tượng tự động và luồng cục bộ.

Chạy tín hiệu như là điều cuối cùng mà luồng thực hiện ( sau khi các hàm hủy của đối tượng tự động và luồng - đã xảy ra ) là những gì _at_thread_exithọ chức năng được thiết kế cho.

Vì vậy, để tránh hành vi không xác định trong trường hợp không có bất kỳ đảm bảo triển khai nào vượt quá những gì tiêu chuẩn yêu cầu, bạn cần (thủ công) tham gia một chuỗi tách rời với _at_thread_exitchức năng thực hiện báo hiệu hoặc làm cho luồng tách rời thực thi chỉ mã sẽ an toàn cho một xử lý tín hiệu, quá.


17
Bạn có chắc về điều này? Ở mọi nơi tôi đã kiểm tra (GCC 5, clang 3.5, MSVC 14), tất cả các luồng bị tách ra đều bị giết khi luồng chính thoát ra.
rustyx

3
Tôi tin rằng vấn đề không phải là những gì một triển khai cụ thể làm, mà là làm thế nào để tránh những gì tiêu chuẩn định nghĩa là hành vi không xác định.
Jon Spencer

7
Câu trả lời này dường như ngụ ý rằng sau khi phá hủy các biến tĩnh, quá trình sẽ đi vào một loại trạng thái ngủ chờ đợi cho bất kỳ luồng nào còn lại kết thúc. Điều đó không đúng, sau khi exithoàn thành việc phá hủy các đối tượng tĩnh, chạy atexittrình xử lý, xả luồng, v.v., nó trả lại quyền điều khiển cho môi trường máy chủ, tức là quá trình thoát. Nếu một luồng tách rời vẫn đang chạy (và bằng cách nào đó đã tránh hành vi không xác định bằng cách không chạm vào bất cứ thứ gì bên ngoài luồng của chính nó) thì nó sẽ biến mất trong một làn khói khi quá trình thoát ra.
Jonathan Wakely

3
Nếu bạn ổn khi sử dụng API không phải ISO C ++ thì nếu maincác cuộc gọi pthread_exitthay vì trả lại hoặc gọi exitthì điều đó sẽ khiến quá trình chờ đợi các chuỗi tách rời kết thúc, sau đó gọi exitsau khi kết thúc.
Jonathan Wakely

3
"Nó tiếp tục chạy (vì tiêu chuẩn không nói là nó đã dừng)" -> Ai đó có thể cho tôi biết làm thế nào một luồng có thể tiếp tục thực hiện trong quá trình chứa nó không?
Gupta

42

Tách chủ đề

Theo std::thread::detach:

Tách luồng thực thi khỏi đối tượng luồng, cho phép thực thi tiếp tục độc lập. Bất kỳ tài nguyên được phân bổ sẽ được giải phóng khi luồng thoát.

Từ pthread_detach:

Hàm pthread_detach () sẽ chỉ ra cho việc thực hiện lưu trữ cho luồng có thể được thu hồi khi luồng đó kết thúc. Nếu luồng chưa kết thúc, pthread_detach () sẽ không khiến nó kết thúc. Hiệu ứng của nhiều lệnh gọi pthread_detach () trên cùng một luồng đích không được chỉ định.

Việc tách các luồng chủ yếu là để tiết kiệm tài nguyên, trong trường hợp ứng dụng không cần đợi một luồng kết thúc (ví dụ: daemon, phải chạy cho đến khi chấm dứt quá trình):

  1. Để giải phóng phần xử lý của ứng dụng: Người ta có thể để một std::threadđối tượng ra khỏi phạm vi mà không cần tham gia, điều thường dẫn đến lời kêu gọi std::terminate()hủy diệt.
  2. Để cho phép HĐH tự động dọn sạch các tài nguyên cụ thể của luồng ( TCB ) ngay khi luồng thoát ra, vì chúng tôi đã chỉ định rõ ràng, rằng chúng tôi không quan tâm đến việc tham gia chuỗi sau này, do đó, người ta không thể tham gia một luồng đã tách ra.

Giết chết chủ đề

Hành vi chấm dứt quá trình giống như hành vi của luồng chính, ít nhất có thể bắt được một số tín hiệu. Việc các luồng khác có thể xử lý tín hiệu hay không không quan trọng, vì người ta có thể tham gia hoặc chấm dứt các luồng khác trong lệnh gọi trình xử lý tín hiệu của luồng chính. (Câu hỏi liên quan )

Như đã nêu, bất kỳ luồng nào, dù tách ra hay không, sẽ chết với quy trình của nó trên hầu hết các hệ điều hành . Quá trình tự nó có thể được chấm dứt bằng cách tăng tín hiệu, bằng cách gọi exit()hoặc bằng cách trở về từ chức năng chính. Tuy nhiên, C ++ 11 không thể và không cố gắng xác định hành vi chính xác của HĐH cơ bản, trong khi các nhà phát triển của máy ảo Java chắc chắn có thể trừu tượng hóa những khác biệt như vậy ở một mức độ nào đó. AFAIK, các mô hình xử lý và luồng xử lý kỳ lạ thường được tìm thấy trên các nền tảng cổ (mà C ++ 11 có thể sẽ không được chuyển) và các hệ thống nhúng khác nhau, có thể có triển khai thư viện ngôn ngữ đặc biệt và / hoặc giới hạn và cũng hỗ trợ ngôn ngữ hạn chế.

Hỗ trợ chủ đề

Nếu các luồng không được hỗ trợ std::thread::get_id()sẽ trả về một id không hợp lệ (được xây dựng mặc định std::thread::id) vì có một quy trình đơn giản, không cần một đối tượng luồng để chạy và hàm tạo của một std::threadnên ném a std::system_error. Đây là cách tôi hiểu C ++ 11 kết hợp với các hệ điều hành ngày nay. Nếu có một hệ điều hành có hỗ trợ luồng, không tạo ra luồng chính trong các quy trình của nó, hãy cho tôi biết.

Kiểm soát chủ đề

Nếu một người cần giữ quyền kiểm soát một luồng để tắt máy đúng cách, người ta có thể làm điều đó bằng cách sử dụng các nguyên hàm đồng bộ hóa và / hoặc một số loại cờ. Tuy nhiên, trong trường hợp này, đặt cờ tắt theo sau là nối là cách tôi thích, vì không có điểm nào tăng độ phức tạp bằng cách tách các luồng, vì dù sao tài nguyên sẽ được giải phóng cùng lúc, trong đó vài byte của std::threadđối tượng so với độ phức tạp cao hơn và có thể nhiều nguyên thủy đồng bộ hơn nên được chấp nhận.


3
Vì mỗi luồng có ngăn xếp riêng (nằm trong phạm vi megabyte trên Linux), tôi sẽ chọn tách luồng (vì vậy ngăn xếp của nó sẽ được giải phóng ngay khi thoát) và sử dụng một số nguyên hàm đồng bộ hóa nếu luồng chính cần thoát (và để tắt máy đúng cách, nó cần tham gia các luồng vẫn đang chạy thay vì chấm dứt chúng khi quay lại / thoát).
Norbert Bérci

8
Tôi thực sự không thấy cách này trả lời câu hỏi
MikeMB

18

Hãy xem xét các mã sau đây:

#include <iostream>
#include <string>
#include <thread>
#include <chrono>

void thread_fn() {
  std::this_thread::sleep_for (std::chrono::seconds(1)); 
  std::cout << "Inside thread function\n";   
}

int main()
{
    std::thread t1(thread_fn);
    t1.detach();

    return 0; 
}

Chạy nó trên hệ thống Linux, thông báo từ thread_fn không bao giờ được in. Hệ điều hành thực sự dọn sạch thread_fn()ngay khi main()thoát ra. Thay thế t1.detach()bằng t1.join()luôn in thông điệp như mong đợi.


Hành vi này xảy ra chính xác trên Windows. Vì vậy, có vẻ như Windows sẽ giết các luồng bị tách ra khi chương trình kết thúc.
Gupta

17

Số phận của luồng sau khi thoát khỏi chương trình là hành vi không xác định. Nhưng một hệ điều hành hiện đại sẽ dọn sạch tất cả các luồng được tạo bởi quá trình đóng nó.

Khi tách một std::thread, ba điều kiện này sẽ tiếp tục giữ:

  1. *this không còn sở hữu bất kỳ chủ đề
  2. joinable() sẽ luôn luôn bằng false
  3. get_id() sẽ bằng std::thread::id()

1
Tại sao không xác định? Bởi vì tiêu chuẩn không định nghĩa gì? Theo chú thích của tôi, sẽ không thực hiện bất kỳ cuộc gọi nào để detach()có hành vi không xác định? Thật khó tin ...
Marc Mutz - mmutz

2
@ MarcMutz-mmutz Không xác định theo nghĩa là nếu quá trình thoát ra, số phận của luồng không được xác định.
Caesar

2
@Caesar và làm cách nào để đảm bảo không thoát trước khi kết thúc chuỗi?
MichalH


0

Để cho phép các luồng khác tiếp tục thực thi, luồng chính sẽ chấm dứt bằng cách gọi pthread_exit () thay vì thoát (3). Sử dụng pthread_exit trong main là tốt. Khi pthread_exit được sử dụng, luồng chính sẽ dừng thực thi và sẽ ở trạng thái zombie (không còn tồn tại) cho đến khi tất cả các luồng khác thoát. Nếu bạn đang sử dụng pthread_exit trong luồng chính, không thể lấy trạng thái trả về của các luồng khác và không thể dọn sạch các luồng khác (có thể được thực hiện bằng pthread_join (3)). Ngoài ra, tốt hơn là tách các luồng (pthread_detach (3)) để tài nguyên luồng được tự động giải phóng khi chấm dứt luồng. Các tài nguyên được chia sẻ sẽ không được phát hành cho đến khi tất cả các luồng thoát.


@kgvinod, tại sao không thêm "pthread_exit (0);" sau "ti.detach ()";
yshi

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.