Apa arti dari thread_local di C ++ 11?

131

Saya bingung dengan deskripsi thread_localdi C ++ 11. Pemahaman saya adalah, setiap utas memiliki salinan unik variabel lokal dalam suatu fungsi. Variabel global / statis dapat diakses oleh semua utas (kemungkinan akses disinkronkan menggunakan kunci). Dan thread_localvariabel terlihat oleh semua utas tetapi hanya dapat dimodifikasi oleh utas yang ditentukan? Apakah itu benar?

polapts
sumber

Jawaban:

151

Durasi penyimpanan thread-local adalah istilah yang digunakan untuk merujuk ke data yang tampaknya durasi penyimpanan global atau statis (dari sudut pandang fungsi yang menggunakannya) tetapi dalam kenyataannya, ada satu salinan per utas.

Itu menambah otomatis saat ini (ada selama blok / fungsi), statis (ada selama durasi program) dan dinamis (ada di tumpukan antara alokasi dan deallokasi).

Sesuatu yang bersifat lokal-benang muncul saat pembuatan utas dan dibuang saat utas berhenti.

Beberapa contoh mengikuti.

Pikirkan generator nomor acak di mana benih harus dipelihara berdasarkan per-thread. Menggunakan seed-local seed berarti setiap thread mendapatkan urutan nomor acaknya sendiri, terlepas dari utas lainnya.

Jika seed Anda adalah variabel lokal dalam fungsi acak, itu akan diinisialisasi setiap kali Anda menyebutnya, memberikan Anda nomor yang sama setiap kali. Jika itu adalah global, utas akan saling mengganggu urutan masing-masing.

Contoh lain adalah sesuatu seperti di strtokmana status tokenisasi disimpan pada basis spesifik-utas. Dengan begitu, satu utas dapat memastikan bahwa utas lain tidak akan mengacaukan upaya tokenisasinya, sementara masih dapat mempertahankan status melalui beberapa panggilan ke strtok- ini pada dasarnya menjadikan strtok_r(versi aman-utas) berlebihan.

Kedua contoh ini memungkinkan variabel lokal utas ada dalam fungsi yang menggunakannya. Dalam kode pra-utas, itu hanya akan menjadi variabel durasi penyimpanan statis dalam fungsi. Untuk utas, itu diubah agar utas durasi penyimpanan lokal.

Contoh lain adalah sesuatu seperti errno. Anda tidak ingin utas terpisah memodifikasi errnosetelah salah satu panggilan Anda gagal tetapi sebelum Anda dapat memeriksa variabel, namun Anda hanya ingin satu salinan per utas.

Situs ini memiliki deskripsi yang masuk akal tentang berbagai penentu durasi penyimpanan.

paxdiablo
sumber
4
Menggunakan utas lokal tidak menyelesaikan masalah dengan strtok. strtokrusak bahkan dalam lingkungan berulir tunggal.
James Kanze
11
Maaf, saya ulangi lagi. Itu tidak memperkenalkan masalah baru dengan strtok :-)
paxdiablo
7
Sebenarnya, rsingkatan dari "re-entrant", yang tidak ada hubungannya dengan keamanan benang. Memang benar bahwa Anda dapat membuat beberapa hal bekerja dengan aman dengan penyimpanan lokal-thread, tetapi Anda tidak dapat membuatnya kembali masuk.
Kerrek SB
5
Dalam lingkungan single-threaded, fungsi perlu masuk kembali hanya jika mereka merupakan bagian dari siklus dalam grafik panggilan. Fungsi daun (yang tidak memanggil fungsi lain) menurut definisi bukan bagian dari siklus, dan tidak ada alasan bagus mengapa strtokharus memanggil fungsi lain.
MSalters
3
ini akan mengacaukannya: while (something) { char *next = strtok(whatever); someFunction(next); // someFunction calls strtok }
japreiss
135

Ketika Anda mendeklarasikan variabel thread_localmaka setiap utas memiliki salinannya sendiri. Ketika Anda merujuknya dengan nama, maka salinan yang terkait dengan utas saat ini digunakan. misalnya

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

Kode ini akan menampilkan "2349", "3249", "4239", "4329", "2439" atau "3429", tetapi tidak pernah lainnya. Setiap utas memiliki salinannya sendiri i, yang ditugaskan untuk, bertambah dan kemudian dicetak. Utas yang berjalan mainjuga memiliki salinannya sendiri, yang ditugaskan di awal dan kemudian dibiarkan tidak berubah. Salinan ini sepenuhnya independen, dan masing-masing memiliki alamat yang berbeda.

Hanya nama yang spesial dalam hal itu --- jika Anda mengambil alamat thread_localvariabel maka Anda hanya memiliki pointer normal ke objek normal, yang dapat Anda lewati dengan bebas di antara utas. misalnya

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

Karena alamat idilewatkan ke fungsi utas, maka salinan imilik utas utama dapat ditugaskan meskipun itu thread_local. Dengan demikian program ini akan menghasilkan "42". Jika Anda melakukan ini, maka Anda harus berhati-hati agar *ptidak diakses setelah utasnya keluar, jika tidak Anda mendapatkan pointer menggantung dan perilaku tidak terdefinisi seperti halnya kasus lain di mana objek yang diarahkan ke objek dihancurkan.

thread_localvariabel diinisialisasi "sebelum digunakan pertama kali", jadi jika mereka tidak pernah disentuh oleh utas yang diberikan maka mereka tidak selalu diinisialisasi. Ini untuk memungkinkan kompiler untuk menghindari membangun setiap thread_localvariabel dalam program untuk utas yang sepenuhnya mandiri dan tidak menyentuh salah satu dari mereka. misalnya

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

Dalam program ini ada 2 utas: utas utama dan utas yang dibuat secara manual. Tidak ada utas panggilan f, jadi thread_localobjek tidak pernah digunakan. Oleh karena itu tidak ditentukan apakah kompiler akan membuat contoh 0, 1 atau 2 my_class, dan output mungkin "", "hellohellogoodbyegoodbye" atau "hellogoodbye".

Anthony Williams
sumber
1
Saya pikir penting untuk dicatat bahwa salinan variabel thread-lokal adalah salinan variabel yang baru diinisialisasi. Artinya, jika Anda menambahkan g()panggilan ke awal threadFunc, maka output akan 0304029atau beberapa permutasi lain dari pasangan 02, 03dan 04. Yaitu, meskipun 9 ditugaskan isebelum utas dibuat, utas mendapatkan salinan baru dari itempat i=0. Jika iditugaskan thread_local int i = random_integer(), maka setiap utas mendapat bilangan bulat acak baru.
Mark H
Tidak persis permutasi dari 02, 03, 04, mungkin ada urutan lainnya seperti020043
Hongxu Chen
Berita menarik yang saya temukan: GCC mendukung penggunaan alamat variabel thread_local sebagai argumen templat, tetapi kompiler lain tidak (pada tulisan ini; mencoba dentang, vstudio). Saya tidak yakin apa yang dikatakan standar tentang itu, atau jika ini adalah area yang tidak ditentukan.
jwd
23

Penyimpanan thread-lokal ada di setiap aspek seperti penyimpanan statis (= global), hanya saja setiap utas memiliki salinan objek yang terpisah. Waktu hidup objek dimulai pada saat ulir dimulai (untuk variabel global) atau pada inisialisasi pertama (untuk statika blok-lokal), dan berakhir saat utas berakhir (yaitu saat join()dipanggil).

Akibatnya, hanya variabel yang juga dapat dideklarasikan yang staticdapat dideklarasikan thread_local, yaitu variabel global (lebih tepatnya: variabel "di lingkup namespace"), anggota kelas statis, dan variabel blok-statis (dalam hal staticini tersirat).

Sebagai contoh, misalkan Anda memiliki kumpulan utas dan ingin tahu seberapa baik beban kerja Anda diseimbangkan:

thread_local Counter c;

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

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

Ini akan mencetak statistik penggunaan utas, misalnya dengan implementasi seperti ini:

struct Counter
{
     unsigned int c = 0;
     void increment() { ++c; }
     ~Counter()
     {
         std::cout << "Thread #" << std::this_thread::id() << " was called "
                   << c << " times" << std::endl;
     }
};
Kerrek SB
sumber