Threadsafe vs peserta kembali

89

Baru-baru ini, saya mengajukan pertanyaan, dengan judul "Apakah malloc thread aman?" , dan di dalamnya saya bertanya, "Apakah malloc re-entrant?"

Saya mendapat kesan bahwa semua peserta kembali aman dari benang.

Apakah anggapan ini salah?

Alphaneo
sumber

Jawaban:

42

Fungsi re-entrant tidak bergantung pada variabel global yang diekspos di header C library .. ambil strtok () vs strtok_r () misalnya di C.

Beberapa fungsi memerlukan tempat untuk menyimpan 'pekerjaan yang sedang berlangsung', fungsi re-entrant memungkinkan Anda menentukan penunjuk ini dalam penyimpanan thread itu sendiri, bukan secara global. Karena penyimpanan ini eksklusif untuk fungsi pemanggilan, itu dapat diinterupsi dan dimasukkan kembali (re-entrant) dan karena dalam banyak kasus pengecualian timbal balik di luar apa yang tidak diimplementasikan oleh fungsi yang diperlukan agar ini berfungsi, mereka sering dianggap benang aman . Namun, ini tidak dijamin oleh definisi.

errno, bagaimanapun, adalah kasus yang sedikit berbeda pada sistem POSIX (dan cenderung menjadi aneh dalam penjelasan apa pun tentang bagaimana ini semua bekerja) :)

Singkatnya, reentrant sering kali berarti thread aman (seperti dalam "gunakan versi reentrant dari fungsi itu jika Anda menggunakan thread"), tetapi thread aman tidak selalu berarti peserta kembali (atau sebaliknya). Saat Anda melihat keamanan thread, konkurensi adalah hal yang perlu Anda pikirkan. Jika Anda harus menyediakan alat penguncian dan pengecualian timbal balik untuk menggunakan suatu fungsi, maka fungsi tersebut tidak secara inheren aman untuk thread.

Tapi, tidak semua fungsi perlu diperiksa dengan baik. malloc()tidak perlu reentrant, tidak bergantung pada apa pun yang berada di luar cakupan titik masuk untuk thread tertentu (dan thread itu sendiri aman).

Fungsi yang mengembalikan nilai yang dialokasikan secara statis tidak aman untuk thread tanpa menggunakan mutex, futex, atau mekanisme penguncian atom lainnya. Namun, mereka tidak perlu masuk kembali jika mereka tidak akan diganggu.

yaitu:

static char *foo(unsigned int flags)
{
  static char ret[2] = { 0 };

  if (flags & FOO_BAR)
    ret[0] = 'c';
  else if (flags & BAR_FOO)
    ret[0] = 'd';
  else
    ret[0] = 'e';

  ret[1] = 'A';

  return ret;
}

Jadi, seperti yang Anda lihat, memiliki banyak utas yang menggunakannya tanpa semacam penguncian akan menjadi bencana .. tetapi tidak ada tujuan menjadi peserta kembali. Anda akan mengalami hal itu ketika memori yang dialokasikan secara dinamis adalah tabu pada beberapa platform tertanam.

Dalam pemrograman fungsional murni, reentrant sering tidak menyiratkan thread safe, itu akan bergantung pada perilaku fungsi yang ditentukan atau anonim yang diteruskan ke titik masuk fungsi, rekursi, dll.

Cara yang lebih baik untuk menempatkan 'thread safe' adalah aman untuk akses bersamaan , yang menggambarkan kebutuhan dengan lebih baik.

Tim Post
sumber
2
Reentrant tidak berarti thread-safe. Fungsi murni menyiratkan keamanan benang.
Julio Guerra
Jawaban yang bagus Tim. Hanya untuk memperjelas, pemahaman saya dari "sering" Anda adalah bahwa thread-safe tidak berarti reentrant, tetapi reentrant juga tidak berarti thread-safe. Apakah Anda dapat menemukan contoh fungsi masuk kembali yang tidak aman untuk thread?
Riccardo
@ Tim Post "Singkatnya, reentrant sering berarti thread safe (seperti dalam" gunakan versi reentrant dari fungsi itu jika Anda menggunakan thread "), tetapi thread safe tidak selalu berarti peserta kembali." qt mengatakan sebaliknya: "Oleh karena itu, fungsi thread-safe selalu reentrant, tetapi fungsi reentrant tidak selalu thread-safe."
4pie0
dan wikipedia mengatakan sesuatu yang lain: "Definisi reentrant ini berbeda dari definisi keamanan thread di lingkungan multi-threaded. Subrutin reentrant dapat mencapai keamanan thread, [1] tetapi dengan reentrant saja mungkin tidak cukup untuk menjadi thread-safe dalam semua situasi. Sebaliknya, kode thread-safe tidak harus berupa reentrant (...) "
4pie0
@ Riccardo: Fungsi yang disinkronkan melalui variabel yang mudah menguap tetapi bukan penghalang memori penuh untuk digunakan dengan penangan sinyal / interupsi biasanya masuk kembali tetapi aman untuk thread.
doynax
77

TL; DR: Suatu fungsi dapat berupa reentrant, thread-safe, keduanya atau tidak keduanya.

Artikel Wikipedia untuk keamanan thread dan reentrancy sangat layak untuk dibaca. Berikut beberapa kutipannya:

Suatu fungsi thread-safe jika:

itu hanya memanipulasi struktur data bersama dengan cara yang menjamin eksekusi yang aman oleh banyak utas secara bersamaan.

Suatu fungsi reentrant jika:

itu dapat diinterupsi pada titik mana pun selama eksekusi dan kemudian dipanggil kembali dengan aman ("masuk kembali") sebelum pemanggilan sebelumnya menyelesaikan eksekusi.

Sebagai contoh kemungkinan masuk kembali, Wikipedia memberikan contoh fungsi yang dirancang untuk dipanggil oleh interupsi sistem: anggaplah sudah berjalan ketika interupsi lain terjadi. Tetapi jangan berpikir Anda aman hanya karena Anda tidak membuat kode dengan interupsi sistem: Anda dapat mengalami masalah masuk kembali dalam program single-threaded jika Anda menggunakan callback atau fungsi rekursif.

Kunci untuk menghindari kebingungan adalah reentrant merujuk ke hanya satu thread yang dieksekusi. Ini adalah konsep sejak tidak ada sistem operasi multitasking.

Contoh

(Sedikit diubah dari artikel Wikipedia)

Contoh 1: tidak thread-safe, bukan reentrant

/* As this function uses a non-const global variable without
   any precaution, it is neither reentrant nor thread-safe. */

int t;

void swap(int *x, int *y)
{
    t = *x;
    *x = *y;
    *y = t;
}

Contoh 2: thread-safe, bukan reentrant

/* We use a thread local variable: the function is now
   thread-safe but still not reentrant (within the
   same thread). */

__thread int t;

void swap(int *x, int *y)
{
    t = *x;
    *x = *y;
    *y = t;
}

Contoh 3: tidak thread-safe, reentrant

/* We save the global state in a local variable and we restore
   it at the end of the function.  The function is now reentrant
   but it is not thread safe. */

int t;

void swap(int *x, int *y)
{
    int s;
    s = t;
    t = *x;
    *x = *y;
    *y = t;
    t = s;
}

Contoh 4: thread-safe, reentrant

/* We use a local variable: the function is now
   thread-safe and reentrant, we have ascended to
   higher plane of existence.  */

void swap(int *x, int *y)
{
    int t;
    t = *x;
    *x = *y;
    *y = t;
}
MiniQuark
sumber
10
Saya tahu saya tidak seharusnya berkomentar hanya untuk mengucapkan terima kasih, tetapi ini adalah salah satu ilustrasi terbaik yang menjelaskan perbedaan antara fungsi re-entrant dan thread safe. Secara khusus, Anda telah menggunakan istilah yang sangat ringkas dan jelas, dan memilih fungsi contoh yang bagus untuk membedakan antara 4 kategori. Jadi terima kasih!
ryyker
11
Bagi saya, contoh 3 bukanlah reentrant: jika penangan sinyal, setelah menginterupsi t = *x, menelepon swap(), maka takan diganti, yang mengarah ke hasil yang tidak diharapkan.
rom1v
1
@ SandBag_1996, mari pertimbangkan panggilan untuk swap(5, 6)diinterupsi oleh swap(1, 2). Setelah t=*x, s=t_originaldan t=5. Sekarang, setelah gangguan, s=5dan t=1. Namun, sebelum swappengembalian kedua itu akan mengembalikan konteks, membuat t=s=5. Sekarang, kita kembali ke yang pertama swapdengan t=5 and s=t_originaldan berlanjut setelah t=*x. Jadi, fungsinya memang tampak sebagai peserta ulang. Ingatlah bahwa setiap panggilan mendapatkan salinannya sendiri dari yang sdialokasikan di tumpukan.
urnonav
4
@ SandBag_1996 Asumsinya adalah bahwa jika fungsi terputus (pada titik mana pun), itu hanya akan dipanggil lagi, dan kita menunggu sampai selesai sebelum melanjutkan panggilan asli. Jika ada hal lain yang terjadi, maka itu pada dasarnya multithreading, dan fungsi ini tidak aman untuk thread. Misalkan fungsi melakukan ABCD, kami hanya menerima hal-hal seperti AB_ABCD_CD, atau A_ABCD_BCD, atau bahkan A__AB_ABCD_CD__BCD. Seperti yang bisa Anda periksa, contoh 3 akan bekerja dengan baik di bawah asumsi ini, jadi ini reentrant. Semoga ini membantu.
MiniQuark
1
@ SandBag_1996, mutex sebenarnya akan membuatnya menjadi non-reentrant. Doa pertama mengunci mutex. Masuk doa kedua - kebuntuan.
urnonav
56

Itu tergantung definisi. Misalnya Qt menggunakan yang berikut:

  • Fungsi thread-safe * dapat dipanggil secara bersamaan dari beberapa utas, meskipun pemanggilan menggunakan data bersama, karena semua referensi ke data bersama dibuat berseri.

  • Sebuah reentrant fungsi juga dapat disebut secara bersamaan dari beberapa thread, tetapi hanya jika setiap permintaan menggunakan data sendiri.

Karenanya, fungsi thread-safe selalu reentrant, tetapi fungsi reentrant tidak selalu thread-safe.

Dengan ekstensi, sebuah kelas dikatakan reentrant jika fungsi anggotanya dapat dipanggil dengan aman dari beberapa thread, selama setiap thread menggunakan instance kelas yang berbeda. Kelas ini aman untuk thread jika fungsi anggotanya dapat dipanggil dengan aman dari beberapa thread, bahkan jika semua thread menggunakan instance kelas yang sama.

tetapi mereka juga memperingatkan:

Catatan: Terminologi di domain multithreading tidak sepenuhnya standar. POSIX menggunakan definisi reentrant dan thread-safe yang agak berbeda untuk C API-nya. Saat menggunakan pustaka kelas C ++ berorientasi objek lainnya dengan Qt, pastikan definisinya dipahami.

Georg Schölly
sumber
2
Definisi reentrant ini terlalu kuat.
qweruiop
Sebuah fungsi reentrant dan thread-safe jika tidak menggunakan variabel global / statis. Thread - safe: ketika banyak thread menjalankan fungsi Anda secara bersamaan, apakah ada balapan ?? Jika Anda menggunakan global var, gunakan kunci untuk melindunginya. jadi aman untuk benang. reentrant: jika sinyal muncul selama eksekusi fungsi Anda, dan memanggil fungsi Anda dalam sinyal lagi, apakah aman ??? dalam kasus seperti itu, tidak ada banyak utas. Sebaiknya Anda tidak menggunakan
variabel