thread_local หมายถึงอะไรใน C ++ 11


131

ฉันสับสนกับคำอธิบายthread_localใน C ++ 11 ความเข้าใจของฉันคือแต่ละเธรดมีสำเนาเฉพาะของตัวแปรท้องถิ่นในฟังก์ชัน ตัวแปรส่วนกลาง / คงที่สามารถเข้าถึงได้โดยเธรดทั้งหมด (อาจซิงโครไนซ์การเข้าถึงโดยใช้การล็อก) และthread_localตัวแปรสามารถมองเห็นได้สำหรับเธรดทั้งหมด แต่สามารถแก้ไขได้โดยเธรดที่กำหนดไว้เท่านั้น? ถูกต้องหรือไม่

คำตอบ:


151

ระยะเวลาการจัดเก็บเธรด - โลคัลเป็นคำที่ใช้เพื่ออ้างถึงข้อมูลที่ดูเหมือนเป็นระยะเวลาการจัดเก็บแบบโกลบอลหรือแบบคงที่ (จากมุมมองของฟังก์ชันที่ใช้) แต่ในความเป็นจริงมีหนึ่งสำเนาต่อเธรด

เพิ่มให้กับอัตโนมัติปัจจุบัน (มีอยู่ระหว่างบล็อก / ฟังก์ชัน) แบบคงที่ (มีอยู่สำหรับระยะเวลาของโปรแกรม) และไดนามิก (มีอยู่ในฮีประหว่างการจัดสรรและการจัดสรรตำแหน่ง)

สิ่งที่เป็นเธรดโลคัลจะเกิดขึ้นในการสร้างเธรดและกำจัดทิ้งเมื่อเธรดหยุด

ตัวอย่างบางส่วนทำตาม

ลองนึกถึงตัวสร้างตัวเลขสุ่มที่ต้องดูแลเมล็ดพันธุ์แบบต่อเธรด การใช้ thread-local seed หมายความว่าแต่ละเธรดจะได้รับลำดับหมายเลขสุ่มของตัวเองโดยไม่ขึ้นกับเธรดอื่น ๆ

หากเมล็ดพันธุ์ของคุณเป็นตัวแปรท้องถิ่นภายในฟังก์ชันสุ่มระบบจะเริ่มต้นทุกครั้งที่คุณเรียกมันโดยให้หมายเลขเดียวกันทุกครั้ง หากเป็นทั่วโลกเธรดจะรบกวนลำดับของกันและกัน

อีกตัวอย่างหนึ่งเช่นstrtokตำแหน่งที่จัดเก็บสถานะโทเค็นบนพื้นฐานเฉพาะเธรด ด้วยวิธีนี้เธรดเดียวสามารถมั่นใจได้ว่าเธรดอื่น ๆ จะไม่ทำให้ความพยายามในการสร้างโทเค็นล้มเหลวในขณะที่ยังคงสามารถรักษาสถานะของการเรียกหลายครั้งไปได้strtokซึ่งโดยทั่วไปจะแสดงผลstrtok_rซ้ำซ้อน (เวอร์ชันที่ปลอดภัยของเธรด)

ทั้งสองตัวอย่างนี้อนุญาตให้ตัวแปรโลคัลเธรดมีอยู่ภายในฟังก์ชันที่ใช้ ในโค้ดพรีเธรดมันจะเป็นตัวแปรระยะเวลาการจัดเก็บแบบคงที่ภายในฟังก์ชัน สำหรับเธรดที่ปรับเปลี่ยนเป็นระยะเวลาการจัดเก็บเธรดในเครื่อง

errnoอีกตัวอย่างหนึ่งก็จะเป็นสิ่งที่ชอบ คุณไม่ต้องการให้มีการแก้ไขเธรดแยกต่างหากerrnoหลังจากที่การโทรของคุณล้มเหลว แต่ก่อนที่คุณจะสามารถตรวจสอบตัวแปรได้ แต่คุณต้องการเพียงหนึ่งสำเนาต่อเธรด

ไซต์นี้มีคำอธิบายที่สมเหตุสมผลเกี่ยวกับตัวระบุระยะเวลาการจัดเก็บที่แตกต่างกัน


4
การใช้เธรดโลคัลไม่สามารถแก้ปัญหาstrtokได้ strtokเสียแม้ในสภาพแวดล้อมแบบเธรดเดียว
James Kanze

11
ขอโทษขอฉันเรียบเรียงใหม่นะ ไม่แนะนำปัญหาใหม่ๆ กับ strtok :-)
paxdiablo

7
จริงๆแล้วrย่อมาจาก "re-entrant" ซึ่งไม่มีส่วนเกี่ยวข้องกับความปลอดภัยของเธรด เป็นเรื่องจริงที่คุณสามารถทำให้บางสิ่งใช้งานเธรดได้อย่างปลอดภัยด้วยพื้นที่จัดเก็บเธรดในเครื่อง แต่คุณไม่สามารถทำให้สิ่งเหล่านั้นกลับเข้ามาใหม่ได้
Kerrek SB

5
ในสภาพแวดล้อมแบบเธรดเดียวฟังก์ชันต่างๆจะต้องเข้ามาใหม่ก็ต่อเมื่อฟังก์ชันเหล่านั้นเป็นส่วนหนึ่งของวงจรในกราฟการโทร ฟังก์ชัน leaf (ฟังก์ชันที่ไม่เรียกฟังก์ชันอื่น) เป็นไปตามคำจำกัดความไม่ได้เป็นส่วนหนึ่งของวัฏจักรและไม่มีเหตุผลที่ดีว่าทำไมจึงstrtokควรเรียกใช้ฟังก์ชันอื่น
MSalters

3
สิ่งนี้จะทำให้สับสน: while (something) { char *next = strtok(whatever); someFunction(next); // someFunction calls strtok }
japreiss

135

เมื่อคุณประกาศตัวแปรthread_localแต่ละเธรดจะมีสำเนาของตัวเอง เมื่อคุณอ้างถึงตามชื่อระบบจะใช้สำเนาที่เกี่ยวข้องกับเธรดปัจจุบัน เช่น

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;
}

รหัสนี้จะแสดงผลเป็น "2349", "3249", "4239", "4329", "2439" หรือ "3429" แต่จะไม่เป็นอย่างอื่น แต่ละเธรดมีสำเนาของตัวเองiซึ่งกำหนดให้เพิ่มขึ้นแล้วพิมพ์ เธรดที่รันmainยังมีสำเนาของตัวเองซึ่งถูกกำหนดให้ในตอนเริ่มต้นจากนั้นปล่อยให้ไม่มีการเปลี่ยนแปลง สำเนาเหล่านี้เป็นเอกเทศโดยสิ้นเชิงและแต่ละฉบับมีที่อยู่ที่แตกต่างกัน

มันเป็นเพียงชื่อที่พิเศษในแง่นั้น --- ถ้าคุณใช้แอดเดรสของthread_localตัวแปรคุณก็แค่มีตัวชี้ปกติไปยังวัตถุปกติซึ่งคุณสามารถส่งผ่านระหว่างเธรดได้อย่างอิสระ เช่น

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;
}

ตั้งแต่ที่อยู่ของผู้iถูกส่งไปยังฟังก์ชั่นด้ายแล้วคัดลอกของที่อยู่ในหัวข้อหลักสามารถกำหนดให้แม้ว่าจะเป็นi thread_localโปรแกรมนี้จะแสดงผลเป็น "42" หากคุณทำเช่นนี้คุณต้องดูแลที่*pไม่สามารถเข้าถึงได้หลังจากเธรดที่เป็นของออกไปมิฉะนั้นคุณจะได้รับตัวชี้ห้อยและพฤติกรรมที่ไม่ได้กำหนดเช่นเดียวกับกรณีอื่น ๆ ที่วัตถุที่ชี้ไปถูกทำลาย

thread_localตัวแปรจะถูกกำหนดค่าเริ่มต้น "ก่อนใช้งานครั้งแรก" ดังนั้นหากไม่เคยสัมผัสกับเธรดที่กำหนดก็ไม่จำเป็นต้องเริ่มต้น นี่คือการอนุญาตให้คอมไพเลอร์หลีกเลี่ยงการสร้างthread_localตัวแปรทุกตัวในโปรแกรมสำหรับเธรดที่มีอยู่ในตัวเองทั้งหมดและไม่สัมผัสกับตัวแปรใด ๆ เช่น

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();
}

ในโปรแกรมนี้มี 2 เธรด: เธรดหลักและเธรดที่สร้างขึ้นเอง ไม่มีการเรียกเธรดfดังนั้นจึงthread_localไม่ใช้อ็อบเจ็กต์ ดังนั้นจึงไม่มีการระบุว่าคอมไพเลอร์จะสร้างอินสแตนซ์ 0, 1 หรือ 2 ของอินสแตนซ์my_classและเอาต์พุตอาจเป็น "", "hellohellogoodbyegoodbye" หรือ "hellogoodbye"


1
ฉันคิดว่าสิ่งสำคัญคือต้องสังเกตว่าสำเนาเธรดโลคัลของตัวแปรเป็นสำเนาของตัวแปรที่เพิ่งเริ่มต้นใหม่ นั่นคือถ้าคุณเพิ่มg()การเรียกไปยังจุดเริ่มต้นของthreadFuncแล้วออกจะเป็น0304029บางส่วนหรือการเปลี่ยนแปลงอื่น ๆ ของคู่02, และ03 04นั่นคือแม้ว่า 9 ได้รับมอบหมายให้iก่อนหัวข้อที่มีการสร้างหัวข้อได้รับสำเนาสร้างสดใหม่ของที่i i=0หากiกำหนดด้วยthread_local int i = random_integer()เธรดแต่ละเธรดจะได้รับจำนวนเต็มที่สุ่มใหม่
Mark H

ไม่ว่าการเปลี่ยนแปลงของ02, 03, 04อาจจะมีลำดับอื่น ๆ เช่น020043
Hongxu เฉิน

สิ่งที่น่าสนใจที่ฉันเพิ่งพบ: GCC รองรับการใช้ที่อยู่ของตัวแปร thread_local เป็นอาร์กิวเมนต์แม่แบบ แต่คอมไพเลอร์อื่นไม่ทำ (ในขณะที่เขียนนี้พยายามส่งเสียงดัง, vstudio) ฉันไม่แน่ใจว่ามาตรฐานนั้นพูดถึงเรื่องนั้นอย่างไรหรือเป็นพื้นที่ที่ไม่ได้ระบุ
jwd

23

ที่เก็บเธรด - โลคัลอยู่ในทุก ๆ ด้านเช่นหน่วยเก็บข้อมูลแบบคงที่ (= โกลบอล) โดยแต่ละเธรดจะมีสำเนาของอ็อบเจ็กต์แยกต่างหาก เวลาชีวิตของออบเจ็กต์เริ่มต้นที่เธรดเริ่มต้น (สำหรับตัวแปรส่วนกลาง) หรือเมื่อเริ่มต้นครั้งแรก (สำหรับสถิติแบบบล็อกโลคัล) และสิ้นสุดเมื่อเธรดสิ้นสุดลง (กล่าวคือเมื่อjoin()ถูกเรียก)

ดังนั้นตัวแปรที่สามารถประกาศได้เท่านั้นที่สามารถstaticประกาศเป็นthread_localตัวแปรทั่วโลก (อย่างแม่นยำมากขึ้น: ตัวแปร "ที่ขอบเขตเนมสเปซ") สมาชิกคลาสแบบคงที่และตัวแปรบล็อกคงที่ (ในกรณีนี้staticเป็นนัย)

ตัวอย่างเช่นสมมติว่าคุณมีเธรดพูลและต้องการทราบว่าภาระงานของคุณสมดุลดีเพียงใด:

thread_local Counter c;

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

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

สิ่งนี้จะพิมพ์สถิติการใช้เธรดเช่นด้วยการใช้งานดังนี้:

struct Counter
{
     unsigned int c = 0;
     void increment() { ++c; }
     ~Counter()
     {
         std::cout << "Thread #" << std::this_thread::id() << " was called "
                   << c << " times" << std::endl;
     }
};
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.