Halo semuanya, hari ini saya ingin berbicara tentang beberapa kesulitan dan kesalahpahaman yang dihadapi banyak pencari kerja. Perusahaan kami berkembang secara aktif dan saya sering mengadakan atau berpartisipasi dalam wawancara. Akibatnya, saya mengidentifikasi beberapa masalah yang membuat banyak kandidat berada dalam posisi yang sulit. Mari kita lihat bersama. Saya akan membahas pertanyaan khusus Python, tetapi secara keseluruhan artikel ini akan berfungsi untuk wawancara kerja apa pun. Untuk developer berpengalaman, kebenaran tidak akan terungkap di sini, tetapi bagi mereka yang baru memulai perjalanan, akan lebih mudah untuk memutuskan topik untuk beberapa hari ke depan.
Perbedaan antara proses dan utas di Linux
Nah, Anda tahu, pertanyaan yang khas dan, secara umum, sederhana, murni untuk pemahaman, tanpa menggali detail dan seluk-beluk. Tentu saja, sebagian besar pelamar akan memberi tahu Anda bahwa utas lebih ringan, konteks beralih di antara mereka lebih cepat, dan secara umum mereka hidup di dalam proses. Dan semua ini benar dan luar biasa ketika kita tidak berbicara tentang Linux. Di kernel Linux, utas diimplementasikan dengan cara yang sama seperti proses normal. Untaian hanyalah proses yang berbagi beberapa sumber daya dengan proses lain.
Ada dua panggilan sistem yang dapat digunakan untuk membuat proses di Linux:
Saya akan menunjukkan hal berikut: ketika Anda membuat suatu
fork()
proses, Anda tidak segera mendapatkan salinan dari memori proses induk. Proses Anda akan berjalan dengan satu instance dalam memori. Oleh karena itu, jika secara total Anda seharusnya mengalami kelebihan memori, maka semuanya akan terus berfungsi. Kernel akan menandai deskriptor halaman memori dari proses induk sebagai read-only, dan upaya untuk menulis kepada mereka (oleh proses anak atau induk) akan memunculkan dan menangani pengecualian yang akan menyebabkan salinan lengkap dibuat. Mekanisme ini disebut Copy-on-Write.
Saya pikir Linux adalah buku yang bagus tentang perangkat Linux. Pemrograman Sistem "oleh Robert Love.
Masalah Event Loop
Layanan dan pekerja asinkron di Python atau Go ada di mana-mana di perusahaan kami. Oleh karena itu, kami menganggap penting untuk memiliki pemahaman yang sama tentang asynchrony dan cara kerja Event Loop. Banyak kandidat yang sudah cukup pandai menjawab pertanyaan tentang keuntungan dari pendekatan asinkron dan dengan tepat merepresentasikan Event Loop sebagai semacam loop tanpa akhir yang memungkinkan Anda untuk memahami apakah event tertentu telah datang dari sistem operasi (misalnya, menulis data ke soket). Tetapi perekatnya hilang: bagaimana program mendapatkan informasi ini dari sistem operasi?
Tentu saja, hal paling sederhana untuk diingat adalah
Select
... Dengan bantuannya, daftar deskriptor file terbentuk yang Anda rencanakan untuk dipantau. Kode klien harus memeriksa semua tuas yang diteruskan untuk kejadian (dan jumlahnya dibatasi hingga 1024), yang membuatnya lambat dan tidak nyaman.
Jawaban tentang
Select
lebih dari cukup, tetapi jika Anda mengingat tentang
Poll
atau
Epoll
, dan berbicara tentang masalah yang mereka pecahkan, maka ini akan menjadi nilai tambah yang besar untuk jawaban Anda. Agar tidak menimbulkan kekhawatiran yang tidak perlu: kami tidak dimintai kode C dan spesifikasi terperinci, kami hanya berbicara tentang pemahaman dasar tentang apa yang terjadi. Baca tentang perbedaannya
Select
,
Poll
dan
Epoll
bisa di artikel ini .
Saya juga menyarankan Anda untuk melihat topik asynchrony dengan Python oleh David Beasley .
GIL melindungi, tapi bukan Anda
Kesalahpahaman umum lainnya adalah bahwa GIL dirancang untuk melindungi pengembang dari masalah akses data secara bersamaan. Tapi bukan ini masalahnya. GIL akan, tentu saja, mencegah Anda memparalelkan program Anda dengan utas (tetapi bukan proses). Sederhananya, GIL adalah kunci yang harus diambil sebelum panggilan apa pun ke Python (tidak begitu penting. Kode Python dijalankan atau panggilan Python C API). Oleh karena itu, GIL akan melindungi struktur internal dari keadaan yang tidak konsisten, tetapi Anda, seperti dalam bahasa lain, harus menggunakan primitif sinkronisasi.
Mereka juga mengatakan bahwa GIL hanya diperlukan agar GC berfungsi dengan benar. Baginya, dia, tentu saja, dibutuhkan, tetapi ini bukan akhirnya.
Dari sudut pandang eksekusi, fungsi yang paling sederhana pun akan dipecah menjadi beberapa langkah:
import dis
def sum_2(a, b):
return a + b
dis.dis(sum_2)
4 0 LOAD_FAST 0 (a)
2 LOAD_FAST 1 (b)
4 BINARY_ADD
6 RETURN_VALUE
Dari sudut pandang prosesor, masing-masing operasi ini tidak bersifat atomik. Python akan menjalankan banyak instruksi prosesor untuk setiap baris bytecode. Dalam kasus ini, Anda tidak boleh mengizinkan utas lain untuk mengubah status tumpukan atau membuat modifikasi memori lainnya, ini akan menyebabkan Kesalahan Segmentasi atau perilaku yang salah. Oleh karena itu, interpreter meminta kunci global pada setiap instruksi bytecode. Namun, konteksnya dapat diubah antara instruksi individu, dan di sini GIL tidak menyelamatkan kita dengan cara apapun. Anda dapat membaca lebih lanjut tentang bytecode dan cara bekerja dengannya di dokumentasi .
Tentang topik keamanan GIL, lihat contoh sederhana:
import threading
a = 0
def x():
global a
for i in range(100000):
a += 1
threads = []
for j in range(10):
thread = threading.Thread(target=x)
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
assert a == 1000000
Di komputer saya, kesalahan macet secara stabil. Jika tiba-tiba itu tidak berhasil untuk Anda, jalankan beberapa kali atau tambahkan utas. Dengan jumlah utas yang sedikit, Anda akan mendapatkan masalah mengambang (kesalahan muncul dan tidak muncul). Artinya, selain data yang salah, situasi seperti itu memiliki masalah berupa sifatnya yang mengambang. Ini juga membawa kita ke masalah berikutnya: sinkronisasi primitif.
Dan lagi, saya tidak bisa tidak merujuk pada David Beasley .
Primitif sinkronisasi
Secara umum, primitif sinkronisasi bukanlah pertanyaan terbaik untuk Python, tetapi mereka menunjukkan pemahaman umum tentang masalah dan seberapa dalam Anda menggali ke arah ini. Topik multithreading, setidaknya bersama kami, ditanyakan sebagai bonus, dan hanya akan menjadi nilai plus (jika Anda menjawab). Tapi tidak apa-apa jika Anda belum menemukannya. Kami dapat mengatakan bahwa pertanyaan ini tidak terikat pada bahasa tertentu.
Banyak pythonist pemula, seperti yang saya tulis di atas, berharap untuk kekuatan ajaib dari GIL, sehingga mereka tidak melihat topik sinkronisasi primitif. Namun sia-sia, ini bisa berguna saat melakukan operasi dan tugas latar belakang. Topik primitif sinkronisasi besar dan dipahami dengan baik, khususnya, saya sarankan untuk membacanya di buku "Pemrograman Aplikasi Inti Python" oleh Wesley J. Chun.
Dan karena kita telah melihat contoh di mana GIL tidak membantu kita dalam bekerja dengan utas, kita akan mempertimbangkan contoh paling sederhana tentang bagaimana melindungi diri kita dari masalah seperti itu.
import threading
lock = threading.Lock()
a = 0
def x():
global a
lock.acquire()
try:
for i in range(100000):
a += 1
finally:
lock.release()
threads = []
for j in range(10):
thread = threading.Thread(target=x)
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
assert a == 1000000
Coba lagi di seluruh bagian kepala
Anda tidak pernah dapat mengandalkan fakta bahwa infrastruktur akan selalu bekerja dengan stabil. Dalam wawancara, kami sering meminta untuk merancang layanan mikro sederhana yang berinteraksi dengan orang lain (misalnya, melalui HTTP). Masalah stabilitas layanan terkadang membingungkan kandidat. Saya ingin menunjukkan beberapa masalah yang diabaikan oleh kandidat saat mengusulkan percobaan ulang melalui HTTP.
Masalah pertama: layanan mungkin tidak berfungsi untuk waktu yang lama. Permintaan yang berulang dalam waktu nyata tidak akan ada artinya.
Percobaan ulang yang dilakukan secara kasar dapat menghentikan layanan yang mulai melambat saat dimuat. Setidaknya yang dia butuhkan adalah peningkatan beban, yang dapat meningkat secara signifikan karena permintaan yang berulang. Kami selalu tertarik untuk mendiskusikan metode penyimpanan negara dan menerapkan pengiriman setelah layanan mulai bekerja secara normal.
Atau, Anda dapat mencoba mengubah protokol dari HTTP menjadi sesuatu dengan pengiriman terjamin (AMQP, dll.).
Mesh layanan juga dapat mengambil alih tugas coba lagi. Anda dapat membaca lebih lanjut di artikel ini .
Secara keseluruhan, seperti yang saya katakan, tidak ada kejutan di sini, tetapi artikel ini dapat membantu Anda mengetahui topik mana yang harus diangkat. Tidak hanya untuk wawancara, tetapi juga untuk pemahaman yang lebih dalam tentang esensi dari proses yang sedang berlangsung.