1. Bagaimana cara didefinisikan dengan aman ?
Secara semantik. Dalam hal ini, ini bukan istilah yang sulit didefinisikan. Itu hanya berarti "Anda bisa melakukan itu, tanpa risiko".
2. Jika suatu program dapat dijalankan dengan aman secara bersamaan, apakah itu selalu berarti bahwa itu reentrant?
Tidak.
Sebagai contoh, mari kita memiliki fungsi C ++ yang mengambil kunci, dan panggilan balik sebagai parameter:
#include <mutex>
typedef void (*callback)();
std::mutex m;
void foo(callback f)
{
m.lock();
// use the resource protected by the mutex
if (f) {
f();
}
// use the resource protected by the mutex
m.unlock();
}
Fungsi lain mungkin perlu mengunci mutex yang sama:
void bar()
{
foo(nullptr);
}
Pada pandangan pertama, semuanya tampak baik-baik saja ... Tapi tunggu:
int main()
{
foo(bar);
return 0;
}
Jika kunci pada mutex tidak rekursif, maka inilah yang akan terjadi, di utas utama:
main
akan menelepon foo
.
foo
akan mendapatkan kunci.
foo
akan memanggil bar
, yang akan memanggil foo
.
- yang ke-2
foo
akan mencoba mendapatkan kunci, gagal dan menunggu sampai dilepaskan.
- Jalan buntu.
- Ups ...
Oke, saya curang, menggunakan panggilan balik. Tetapi mudah untuk membayangkan potongan kode yang lebih kompleks yang memiliki efek serupa.
3. Apa sebenarnya benang merah antara enam poin yang disebutkan yang harus saya ingat ketika memeriksa kode saya untuk kemampuan reentrant?
Anda dapat mencium masalah jika fungsi Anda memiliki / memberikan akses ke sumber daya persisten yang dapat dimodifikasi, atau memiliki / memberikan akses ke fungsi yang berbau .
( Oke, 99% kode kita harus berbau, lalu ... Lihat bagian terakhir untuk mengatasinya ... )
Jadi, mempelajari kode Anda, salah satu poin itu harus mengingatkan Anda:
- Fungsi memiliki status (mis. Mengakses variabel global, atau bahkan variabel anggota kelas)
- Fungsi ini dapat dipanggil oleh banyak utas, atau dapat muncul dua kali dalam tumpukan saat proses sedang dijalankan (yaitu fungsi tersebut dapat memanggil dirinya sendiri, langsung atau tidak langsung). Berfungsi menerima panggilan balik karena parameter sangat berbau .
Perhatikan bahwa non-reentrancy adalah viral: Suatu fungsi yang dapat memanggil fungsi non-reentrant tidak dapat dianggap reentrant.
Perhatikan juga, bahwa metode C ++ berbau karena mereka memiliki aksesthis
, jadi Anda harus mempelajari kode untuk memastikan mereka tidak memiliki interaksi lucu.
4.1. Apakah semua fungsi rekursif reentrant?
Tidak.
Dalam kasus multithreaded, fungsi rekursif mengakses sumber daya bersama dapat dipanggil oleh banyak utas pada saat yang sama, menghasilkan data yang buruk / rusak.
Dalam kasus singlethreaded, fungsi rekursif dapat menggunakan fungsi non-reentrant (seperti yang terkenal strtok
), atau menggunakan data global tanpa menangani fakta data sudah digunakan. Jadi fungsi Anda bersifat rekursif karena ia memanggil dirinya secara langsung atau tidak langsung, tetapi masih bisa bersifat rekursif-tidak aman .
4.2. Apakah semua fungsi thread-safe reentrant?
Dalam contoh di atas, saya menunjukkan bagaimana fungsi threadsafe ternyata tidak reentrant. OK, saya curang karena parameter panggilan balik. Tetapi kemudian, ada beberapa cara untuk mem-deadlock sebuah utas dengan membuatnya memperoleh dua kali kunci non-rekursif.
4.3. Apakah semua fungsi rekursif dan thread-safe reentrant?
Saya akan mengatakan "ya" jika dengan "rekursif" yang Anda maksud adalah "rekursif-aman".
Jika Anda dapat menjamin bahwa suatu fungsi dapat dipanggil secara bersamaan oleh beberapa utas, dan dapat memanggil dirinya sendiri, secara langsung atau tidak langsung, tanpa masalah, maka itu adalah reentrant.
Masalahnya adalah mengevaluasi jaminan ini ... ^ _ ^
5. Apakah istilah seperti reentrance dan keselamatan ulir benar-benar mutlak, yaitu apakah mereka telah menetapkan definisi konkret?
Saya percaya mereka melakukannya, tetapi kemudian, mengevaluasi suatu fungsi adalah thread-safe atau reentrant bisa sulit. Inilah sebabnya saya menggunakan istilah bau di atas: Anda dapat menemukan suatu fungsi bukan reentrant, tetapi bisa jadi sulit untuk memastikan sepotong kode kompleks reentrant
6. Contoh
Katakanlah Anda memiliki objek, dengan satu metode yang perlu menggunakan sumber daya:
struct MyStruct
{
P * p;
void foo()
{
if (this->p == nullptr)
{
this->p = new P();
}
// lots of code, some using this->p
if (this->p != nullptr)
{
delete this->p;
this->p = nullptr;
}
}
};
Masalah pertama adalah bahwa jika entah bagaimana fungsi ini disebut secara rekursif (yaitu fungsi ini memanggil dirinya sendiri, secara langsung atau tidak langsung), kode tersebut mungkin akan macet, karena this->p
akan dihapus pada akhir panggilan terakhir, dan masih mungkin digunakan sebelum akhir panggilan pertama.
Dengan demikian, kode ini tidak aman secara rekursif .
Kami dapat menggunakan penghitung referensi untuk memperbaikinya:
struct MyStruct
{
size_t c;
P * p;
void foo()
{
if (c == 0)
{
this->p = new P();
}
++c;
// lots of code, some using this->p
--c;
if (c == 0)
{
delete this->p;
this->p = nullptr;
}
}
};
Dengan cara ini, kodenya menjadi aman secara rekursif ... Tetapi itu masih belum reentrant karena masalah multithreading: Kita harus yakin modifikasi c
dan p
akan dilakukan secara atomis, menggunakan mutasi rekursif (tidak semua mutex bersifat rekursif):
#include <mutex>
struct MyStruct
{
std::recursive_mutex m;
size_t c;
P * p;
void foo()
{
m.lock();
if (c == 0)
{
this->p = new P();
}
++c;
m.unlock();
// lots of code, some using this->p
m.lock();
--c;
if (c == 0)
{
delete this->p;
this->p = nullptr;
}
m.unlock();
}
};
Dan tentu saja, ini semua menganggap lots of code
itu sendiri reentrant, termasuk penggunaan p
.
Dan kode di atas bahkan tidak aman dari pengecualian , tetapi ini adalah cerita lain ... ^ _ ^
7. Hai 99% dari kode kami tidak reentrant!
Ini cukup benar untuk kode spageti. Tetapi jika Anda mempartisi kode Anda dengan benar, Anda akan menghindari masalah reentrancy.
7.1. Pastikan semua fungsi tidak memiliki status
Mereka hanya harus menggunakan parameter, variabel lokal mereka sendiri, fungsi lain tanpa status, dan mengembalikan salinan data jika mereka kembali sama sekali.
7.2. Pastikan objek Anda "recursive-safe"
Metode objek memiliki akses this
, sehingga ia berbagi keadaan dengan semua metode dengan instance objek yang sama.
Jadi, pastikan objek dapat digunakan pada satu titik di stack (yaitu memanggil metode A), dan kemudian, di titik lain (yaitu memanggil metode B), tanpa merusak keseluruhan objek. Rancang objek Anda untuk memastikan bahwa saat keluar dari suatu metode, objek tersebut stabil dan benar (tidak ada pointer yang menggantung, tidak ada variabel anggota yang bertentangan, dll.).
7.3. Pastikan semua objek Anda dienkapsulasi dengan benar
Tidak ada orang lain yang memiliki akses ke data internal mereka:
// bad
int & MyObject::getCounter()
{
return this->counter;
}
// good
int MyObject::getCounter()
{
return this->counter;
}
// good, too
void MyObject::getCounter(int & p_counter)
{
p_counter = this->counter;
}
Bahkan mengembalikan referensi const bisa berbahaya jika pengguna mengambil alamat data, karena beberapa bagian lain dari kode dapat memodifikasinya tanpa kode yang menahan referensi const diberitahu.
7.4. Pastikan pengguna tahu objek Anda tidak aman
Dengan demikian, pengguna bertanggung jawab untuk menggunakan mutex untuk menggunakan objek yang dibagikan di antara utas.
Objek dari STL dirancang untuk tidak aman-thread (karena masalah kinerja), dan dengan demikian, jika pengguna ingin berbagi std::string
antara dua utas, pengguna harus melindungi aksesnya dengan primitif concurrency;
7.5. Pastikan kode aman thread Anda adalah rekursif-aman
Ini berarti menggunakan mutex rekursif jika Anda yakin sumber daya yang sama dapat digunakan dua kali oleh utas yang sama.