Apa kekurangan dari membuat implementasi runtime JavaScript multi-threaded? [Tutup]


51

Saya telah bekerja pada implementasi runtime JavaScript multi-threaded selama seminggu terakhir. Saya memiliki bukti konsep yang dibuat dalam C ++ menggunakan JavaScriptCore dan meningkatkan.

Arsitekturnya sederhana: ketika runtime selesai mengevaluasi skrip utama, ia meluncurkan dan bergabung dengan thread-pool, yang mulai memilih tugas dari antrian prioritas bersama, jika dua tugas mencoba mengakses variabel secara bersamaan, ia ditandai atom dan mereka bersaing untuk akses .

Bekerja runtime node.js multithreaded

Masalahnya adalah ketika saya menunjukkan desain ini ke seorang programmer JavaScript saya mendapatkan umpan balik yang sangat negatif, dan saya tidak tahu mengapa. Bahkan secara pribadi, mereka semua mengatakan bahwa JavaScript dimaksudkan untuk berurutan tunggal, bahwa perpustakaan yang ada harus ditulis ulang, dan bahwa gremlin akan menelurkan dan memakan setiap makhluk hidup jika saya terus mengerjakan ini.

Saya awalnya memiliki implementasi coroutine asli (menggunakan konteks boost) di tempat juga, tetapi saya harus membuangnya (JavaScriptCore sangat bertele-tele tentang stack), dan saya tidak ingin mengambil risiko kemarahan mereka sehingga saya memutuskan untuk tidak menyebutkannya.

Bagaimana menurut anda? Apakah JavaScript harus berurutan tunggal, dan haruskah dibiarkan sendiri? Mengapa semua orang menentang gagasan runtime JavaScript bersamaan?

Sunting: Proyek ini sekarang ada di GitHub , bereksperimenlah sendiri dan beri tahu saya apa yang Anda pikirkan.

Berikut ini adalah gambar janji yang berjalan di semua core CPU secara paralel tanpa pertentangan:

Menjalankan janji secara bersamaan.


7
Ini sepertinya pertanyaan yang sangat keras. Apakah Anda bertanya kepada orang-orang yang tampaknya tidak menyukai ide Anda mengapa mereka pikir itu akan merepotkan?
5gon12eder

26
Menambahkan utas ke sesuatu yang tidak dimaksudkan untuk multithreaded seperti mengubah jalan satu lajur menjadi jalan bebas hambatan tanpa menyediakan ed driver. Ini akan bekerja dengan baik hampir sepanjang waktu, sampai orang-orang mulai crash secara acak. Dengan multithreading, Anda akan memiliki bug timing halus yang tidak dapat Anda tiru atau perilaku yang tidak menentu sebagian besar waktu. Anda harus merancang dengan itu dalam pikiran. Anda perlu sinkronisasi utas. Hanya membuat variabel atom tidak menghilangkan kondisi ras.
mgw854

2
Bagaimana Anda berencana menangani akses multi-utas ke status bersama? "Ditandai sebagai atom dan bersaing untuk akses" tidak menjelaskan bagaimana menurut Anda ini akan bekerja. Saya kira sikap negatif terhadap ide itu karena orang tidak tahu bagaimana Anda benar-benar membuat ini berhasil. Atau, jika Anda meletakkan semua beban pada pengembang seperti di Java atau C ++ untuk menggunakan mutex yang tepat dan semacamnya, maka orang mungkin berpikir mengapa mereka menginginkan komplikasi dan risiko pemrograman dalam lingkungan yang bebas dari itu.
jfriend00

17
Karena secara otomatis mengoordinasikan keadaan acak di antara utas dianggap sebagai masalah yang hampir tidak mungkin sehingga Anda tidak memiliki kredibilitas bahwa Anda dapat menawarkan apa pun yang melakukannya secara otomatis. Dan, jika Anda hanya akan meletakkan beban kembali pada pengembang seperti Java atau C ++ lakukan, maka sebagian besar programmer node.js tidak ingin beban itu - mereka suka itu node.js tidak harus berurusan dengan itu untuk sebagian besar. Jika Anda ingin telinga yang lebih simpatik, Anda harus menjelaskan / menunjukkan bagaimana dan apa yang akan Anda tawarkan dalam hal ini dan mengapa itu akan baik dan bermanfaat.
jfriend00

3
Silakan lanjutkan pekerjaan Anda. Saya menganggap bahasa tanpa multi-threading sebagai bahasa mainan. Saya pikir sebagian besar pengembang JavaScript bekerja dengan browser, yang memiliki model single-thread.
Chloe

Jawaban:


70

1) Multithreading sangat sulit, dan sayangnya cara Anda menyajikan ide ini sejauh ini menyiratkan bahwa Anda terlalu meremehkan betapa sulitnya itu.

Saat ini, sepertinya Anda hanya "menambahkan utas" ke bahasa tersebut dan mengkhawatirkan cara memperbaikinya dan performan nanti. Khususnya:

jika dua tugas mencoba mengakses variabel secara bersamaan, itu ditandai atom dan mereka bersaing untuk akses.
...
Saya setuju bahwa variabel atom tidak akan menyelesaikan segalanya, tetapi mengerjakan solusi untuk masalah sinkronisasi adalah tujuan saya berikutnya.

Menambahkan utas ke Javascript tanpa "solusi untuk masalah sinkronisasi" akan seperti menambahkan bilangan bulat ke Javascript tanpa "solusi untuk masalah penambahan". Ini sangat mendasar untuk sifat masalah yang pada dasarnya tidak ada gunanya bahkan membahas apakah multithreading layak ditambahkan tanpa solusi spesifik dalam pikiran, tidak peduli seberapa buruk kita inginkan.

Plus, membuat semua variabel atom adalah hal yang cenderung membuat program multithread berkinerja lebih buruk daripada rekannya yang singlethreaded, yang membuatnya lebih penting untuk benar-benar menguji kinerja pada program yang lebih realistis dan melihat apakah Anda memperoleh sesuatu atau tidak.

Juga tidak jelas bagi saya apakah Anda mencoba menyembunyikan utas dari programmer node.js atau jika Anda berencana untuk mengeksposnya di beberapa titik, secara efektif membuat dialek Javascript baru untuk pemrograman multithreaded. Kedua opsi tersebut berpotensi menarik, tetapi sepertinya Anda belum memutuskan yang mana yang Anda tuju.

Jadi saat ini, Anda meminta programmer untuk mempertimbangkan beralih dari lingkungan singlethreaded ke lingkungan multithread baru yang tidak memiliki solusi untuk masalah sinkronisasi dan tidak ada bukti yang meningkatkan kinerja dunia nyata, dan tampaknya tidak ada rencana untuk menyelesaikan masalah tersebut.

Mungkin itulah sebabnya orang tidak menganggapmu serius.

2) Kesederhanaan dan kekokohan dari loop peristiwa tunggal adalah keuntungan besar .

Programmer Javascript tahu bahwa bahasa Javascript "aman" dari kondisi ras dan bug lain yang sangat berbahaya yang mengganggu semua pemrograman multithreaded. Fakta bahwa mereka membutuhkan argumen yang kuat untuk meyakinkan mereka agar menyerah bahwa keselamatan tidak membuat mereka berpikiran tertutup, itu membuat mereka bertanggung jawab.

Kecuali Anda bisa mempertahankan keamanan itu, siapa pun yang mungkin ingin beralih ke node multithreaded. Mungkin akan lebih baik beralih ke bahasa seperti Go yang dirancang dari bawah ke atas untuk aplikasi multithreaded.

3) Javascript sudah mendukung "latar belakang utas" (WebWorkers) dan pemrograman asinkron tanpa secara langsung memaparkan manajemen utas kepada programmer.

Fitur-fitur tersebut sudah menyelesaikan banyak kasus penggunaan umum yang memengaruhi programmer Javascript di dunia nyata, tanpa meninggalkan keamanan loop peristiwa tunggal.

Apakah Anda memiliki kasus penggunaan tertentu dalam pikiran bahwa fitur-fitur ini tidak menyelesaikan, dan bahwa programmer Javascript menginginkan solusi? Jika demikian, itu akan menjadi ide yang baik untuk menyajikan node.js multithreaded Anda dalam konteks kasus penggunaan tertentu.


PS Apa yang meyakinkan saya untuk mencoba beralih ke implementasi node.js multithreaded?

Tulis program non-sepele dalam Javascript / node.js yang menurut Anda akan mendapat manfaat dari multithreading asli. Lakukan tes kinerja pada program sampel ini dalam simpul normal dan simpul multithreaded Anda. Tunjukkan pada saya bahwa versi Anda meningkatkan kinerja runtime, responsif, dan penggunaan beberapa core hingga tingkat yang signifikan, tanpa memperkenalkan bug atau ketidakstabilan apa pun.

Setelah Anda selesai melakukannya, saya pikir Anda akan melihat orang-orang jauh lebih tertarik pada ide ini.


1
1) Oke, saya akui bahwa saya telah menunda masalah sinkronisasi untuk sementara waktu sekarang. Ketika saya mengatakan bahwa 'dua tugas akan diperdebatkan', itu bukan desain saya, ini terutama pengamatan: dl.dropboxusercontent.com/u/27714141/... - Saya tidak yakin apa jenis kinerja yang dilakukan oleh JavaScriptCore sihir di sini, tetapi tidak seharusnya t string ini rusak jika tidak secara inheren atom? 2) Saya sangat tidak setuju. Inilah alasan mengapa JS dipandang sebagai bahasa mainan. 3) Janji ES6 akan jauh lebih berkinerja jika diimplementasikan menggunakan penjadwal thread pool.
voodooattack

13
@voodooattack "Inilah alasan JS dipandang sebagai bahasa mainan." Tidak, ini alasan Anda menganggapnya sebagai bahasa mainan. Jutaan orang menggunakan JS setiap hari dan sangat senang dengan itu, kekurangan dan semua. Pastikan Anda memecahkan masalah yang cukup banyak dimiliki orang lain, yang tidak diselesaikan dengan baik hanya dengan mengubah bahasa.
Chris Hayes

@ ChrisHayes Pertanyaannya adalah, mengapa harus menerima kekurangannya ketika saya bisa memperbaikinya? Akankah konkurensi sebagai fitur meningkatkan JavaScript?
voodooattack

1
@ Voodooattack Itulah pertanyaannya. Apakah concurrency sebagai fitur meningkatkan Javascript? Jika Anda bisa mendapatkan jawaban komunitas untuk itu bukan "tidak", maka mungkin Anda sedang mengerjakan sesuatu. Namun, tampaknya seolah-olah lingkaran acara dan delegasi asli Node ke utas pekerja untuk memblokir acara sudah cukup untuk sebagian besar dari apa yang orang perlu lakukan. Saya pikir jika orang benar-benar membutuhkan Javascript berulir, mereka akan menggunakan pekerja Javascript. Namun, jika Anda dapat menemukan cara untuk membuat pekerja bekerja dengan fungsi kelas satu daripada file JS, maka Anda mungkin benar - benar tertarik pada sesuatu.
lunchmeat317

7
@voodooattack Orang yang menyebut JavaScript sebagai bahasa mainan tidak tahu apa yang mereka bicarakan. Apakah ini bahasa utama untuk semuanya? Tentu saja tidak, tetapi menolaknya seperti itu tentu saja merupakan kesalahan. Jika Anda ingin menghilangkan gagasan bahwa JS adalah bahasa mainan, buat aplikasi produksi yang tidak sepele, atau arahkan ke yang sudah ada. Hanya menambahkan konkurensi tidak akan mengubah pikiran orang-orang itu.
jpmc26

16

Hanya menebak di sini untuk menunjukkan masalah dalam pendekatan Anda. Saya tidak dapat mengujinya terhadap implementasi nyata karena tidak ada tautan di mana pun ...

Saya akan mengatakan itu karena invarian tidak selalu dinyatakan oleh nilai satu variabel, dan 'satu variabel' tidak cukup untuk menjadi lingkup kunci dalam kasus umum. Sebagai contoh, bayangkan kita memiliki invarian yang a+b = 0(saldo bank dengan dua akun). Dua fungsi di bawah ini memastikan bahwa invarian selalu diadakan di akhir setiap fungsi (unit eksekusi di JS single-threaded).

function withdraw(v) {
  a -= v;
  b += v;
}
function deposit(v) {
  b -= v;
  a += v;
}

Sekarang, di dunia multithreaded Anda, apa yang terjadi ketika dua utas mengeksekusi withdrawdan depositpada saat yang sama? Terima kasih, Murphy ...

(Anda mungkin memiliki kode yang memperlakukan + = dan - = secara khusus, tetapi itu tidak membantu. Pada titik tertentu, Anda akan memiliki status lokal dalam suatu fungsi, dan tanpa cara untuk 'mengunci' dua variabel pada saat yang bersamaan invarian Anda akan dilanggar.)


EDIT: Jika kode Anda secara semantik setara dengan kode Go di https://gist.github.com/thriqon/f94c10a45b7e0bf656781b0f4a07292a , komentar saya akurat ;-)


Komentar ini tidak relevan, tetapi para filsuf mampu mengunci banyak objek, tetapi mereka kelaparan karena mereka menguncinya dalam urutan yang tidak konsisten.
Dietrich Epp

3
@ voodooattack "Saya baru saja menguji ..." Apakah Anda tidak pernah menemukan urutan eksekusi yang tidak konsisten dengan penjadwalan thread? Ini bervariasi dari menjalankan ke menjalankan, dari mesin ke mesin. Duduk dan menjalankan satu tes (atau bahkan seratus tes!) Tanpa mengidentifikasi mekanisme bagaimana operasi dijadwalkan tidak berguna.
jpmc26

4
@oodooattack Masalahnya adalah bahwa hanya menguji konkurensi tidak berguna. Anda harus dapat membuktikan bahwa invarian akan bertahan, atau mekanismenya tidak akan pernah bisa dipercaya dalam produksi.
sapi

1
Tetapi Anda belum memberi pengguna alat apa pun untuk "menggunakan sistem secara bertanggung jawab" karena sama sekali tidak ada mekanisme penguncian. Membuat semuanya atom memberikan ilusi keamanan benang (dan Anda mendapatkan performa yang baik dari semuanya menjadi atom, bahkan ketika Anda tidak membutuhkan akses atom), tetapi itu sebenarnya tidak memecahkan sebagian besar masalah konkurensi seperti yang diberikan thriqon di sini. Untuk contoh lain, coba iterasi pada array di satu utas sementara utas lainnya menambah atau menghapus elemen dari array. Untuk itu, apa yang membuat Anda berpikir implementasi mesin dari Array bahkan lebih aman?
Zach Lipton

2
@voodooattack Jadi jika pengguna hanya dapat menggunakan utas Anda untuk fungsi tanpa data bersama atau efek samping (dan mereka sebaiknya tidak menggunakannya untuk hal lain, karena seperti yang telah kita lihat di sini, tidak ada cara untuk memastikan keamanan utas), lalu apa nilai yang Anda berikan di atas Pekerja Web (atau salah satu dari banyak perpustakaan yang menyediakan API yang lebih bermanfaat di seputar Pekerja Web)? Dan Pekerja Web jauh lebih aman, karena mereka membuat mustahil bagi pengguna untuk menggunakan sistem "tidak bertanggung jawab."
Zach Lipton

15

Satu dekade yang lalu, Brendan Eich (penemu JavaScript) menulis sebuah esai berjudul Threads Suck , yang jelas merupakan salah satu dari sedikit dokumen kanonik mitologi desain JavaScript.

Apakah itu benar adalah pertanyaan lain, tapi saya pikir itu memiliki pengaruh besar pada bagaimana komunitas JavaScript berpikir tentang concurrency.


31
Orang terakhir yang saya terima nasihatnya adalah Brendan Eich. Seluruh kariernya didasarkan pada pembuatan JavaScript, yang sangat buruk sehingga kami memiliki banyak sekali alat yang dibuat untuk mencoba dan mengatasi kekurangan bawaannya. Pikirkan berapa jam pengembang yang terbuang sia-sia di dunia karena dia.
Phil Wright

12
Sementara saya mengerti mengapa Anda akan mengabaikan pendapatnya secara umum, dan bahkan penghinaan Anda untuk JavaScript, saya tidak mengerti bagaimana perspektif Anda akan memungkinkan untuk mengabaikan keahliannya dalam domain.
Billy Cravens

4
@BillyCravens haha, bahkan buku kanonik tentang Javascript: "Javascript bagian-bagian yang baik " oleh Crockford memberikan permainan pada judulnya. Perlakukan sikap Eich dengan cara yang sama, hanya berpegang pada hal-hal yang menurutnya bagus :-)
gbjbaanb

13
@ PhilWright: Implementasi Javascript pertamanya adalah varian Lisp. Yang bagi saya membuatnya sangat dihormati. Keputusan bosnya untuk memaksanya mengganti sintaks Lisp dengan sintaks mirip C bukanlah kesalahannya. Javascript pada intinya masih merupakan runtime Lisp.
slebetman

7
@ PhilWright: Anda seharusnya tidak menyalahkan Eich atas keburukan bahasa tersebut. Ini bug dalam implementasi awal, dan kebutuhan kompatibilitas ke belakang untuk bahasa web yang mencegah JS jatuh tempo. Konsep inti masih membentuk bahasa yang indah.
Bergi

8

Akses atom tidak diterjemahkan ke dalam perilaku thread-safe.

Salah satu contoh adalah ketika struktur data global perlu tidak valid selama pembaruan seperti mengulang hashmap (ketika menambahkan properti ke objek misalnya) atau menyortir array global. Selama waktu itu Anda tidak dapat mengizinkan utas lain mengakses variabel. Ini pada dasarnya berarti bahwa Anda perlu mendeteksi seluruh siklus baca-perbarui-tulis dan menguncinya. Jika pembaruan itu tidak sepele, itu akan berakhir pada penghentian wilayah masalah.

Javascript telah berurutan tunggal dan kotak pasir dari awal dan semua kode ditulis dengan asumsi ini.

Ini memiliki keuntungan besar sehubungan dengan konteks yang terisolasi dan membiarkan 2 konteks terpisah berjalan di utas yang berbeda. Saya juga berarti bahwa orang yang menulis javascript tidak perlu tahu bagaimana menghadapi kondisi ras dan berbagai pitfals multi-thread lainnya.


Inilah sebabnya saya pikir API scheduler harus lebih pada sisi lanjutan, dan digunakan secara internal untuk janji dan fungsi yang tidak memiliki efek samping.
voodooattack

6

Apakah pendekatan Anda akan secara signifikan meningkatkan kinerja?

Diragukan. Anda benar-benar perlu membuktikan ini.

Apakah pendekatan Anda akan membuatnya lebih mudah / lebih cepat untuk menulis kode?

Jelas tidak, kode multithreaded berkali-kali lebih sulit untuk mendapatkan yang benar daripada kode single threaded.

Apakah pendekatan Anda akan lebih kuat?

Kebuntuan, kondisi balapan dll. Adalah mimpi buruk untuk diperbaiki.


2
Coba buat raytracer menggunakan node.js, sekarang coba lagi dengan utas. Multi-proses bukanlah segalanya.
voodooattack

8
@voodooattack, tidak ada orang waras yang akan menulis ray-tracer menggunakan javascript, dengan atau tanpa threading, karena ini adalah algoritma yang relatif sederhana, tetapi komputasi yang lebih baik ditulis dalam bahasa yang dikompilasi penuh, lebih disukai yang dengan dukungan SIMD . Untuk jenis masalah yang digunakan untuk javascript, multi-proses lebih dari cukup.
Jan Hudec

@JanHudec: Heh, JS juga akan mendapatkan dukungan SIMD :-) hacks.mozilla.org/2014/10/introducing-simd-js
Bergi

2
@voodooattack Jika Anda tidak menyadarinya, lihat SharedArrayBuffers . JavaScript mendapatkan konstruksi konkurensi yang lebih banyak, tetapi mereka ditambahkan secara ekstrem dengan hati-hati, menangani titik nyeri tertentu, mencoba meminimalkan membuat keputusan desain buruk yang harus kita jalani selama bertahun-tahun.
REINSTATE MONICA -Jeremy Banks

2

Implementasi Anda bukan hanya tentang memperkenalkan konkurensi, melainkan tentang memperkenalkan cara spesifik untuk mengimplementasikan konkurensi yaitu konkurensi dengan status yang dapat ditukar pakai bersama. Sepanjang sejarah orang telah menggunakan jenis konkurensi dan ini telah menyebabkan banyak jenis masalah. Ofcourse Anda dapat membuat program sederhana yang bekerja sempurna dengan menggunakan konkurensi keadaan bersama yang dapat ditukar, tetapi ujian sebenarnya dari mekanisme apa pun bukanlah apa yang dapat ia lakukan tetapi dapat mengukurnya saat program menjadi kompleks dan semakin banyak fitur ditambahkan ke program. Ingat perangkat lunak bukanlah hal statis yang Anda bangun sekali dan selesai dengan itu, melainkan terus berkembang dari waktu ke waktu dan jika ada mekanisme atau konsep yang bisa '

Anda dapat melihat model konkurensi lainnya (misal: pesan yang lewat) untuk membantu Anda mengetahui manfaat apa yang diberikan oleh model-model itu.


1
Saya pikir janji ES6 akan mendapat manfaat dari model implementasi saya, karena mereka tidak bersaing untuk akses.
voodooattack

Lihat spesifikasi WebWorkers. Ada paket-paket npm yang menyediakan implementasinya tetapi Anda dapat mengimplementasikannya sebagai inti dari mesin Anda alih-alih sebagai paket
Ankur

JavaScriptCore (implementasi JS yang saya gunakan di webkit) sudah mengimplementasikannya, itu hanya flag kompilasi.
voodooattack

Baik. WebWorkers adalah konkurensi dengan pengiriman pesan. Anda dapat mencoba contoh raytracer Anda dengan mereka dan membandingkannya dengan pendekatan keadaan bisa berubah.
Ankur

4
Pesan yang lewat selalu diimplementasikan di atas status yang bisa berubah, yang berarti akan lebih lambat. Saya gagal melihat maksud Anda di sini. : - /
voodooattack

1

Ini dibutuhkan. Kurangnya mekanisme konkurensi tingkat rendah di simpul js membatasi aplikasinya dalam bidang seperti matematika dan bioinformatika, dll. Selain itu, konkurensi dengan utas tidak selalu bertentangan dengan model konsurensi default yang digunakan dalam simpul. Ada semantik terkenal untuk threading dalam lingkungan dengan loop peristiwa utama, seperti kerangka ui (dan nodejs), dan mereka pasti terlalu rumit untuk sebagian besar situasi mereka masih memiliki kegunaan yang valid.

Tentu, aplikasi web rata-rata Anda tidak akan memerlukan utas, tetapi cobalah melakukan sesuatu yang sedikit kurang konvensional, dan kurangnya primitif konkurensi tingkat rendah suara dengan cepat akan menggerakkan Anda ke sesuatu yang menawarkannya.


4
Tetapi "matematika dan bioinformatika" akan lebih baik ditulis dalam C #, JAVA, atau C ++
Ian

Sebenarnya Python dan Perl adalah bahasa yang dominan di wilayah tersebut.
dryajov

1
Sebenarnya, alasan utama saya menerapkan ini adalah karena aplikasi pembelajaran mesin. Jadi, Anda benar juga.
voodooattack

@dryajov Bersyukurlah bahwa Anda tidak tahu seberapa besar FORTRAN di beberapa bidang ilmu komputasi ...
cmaster

@dryajov Karena mereka lebih mudah diakses oleh Manusia yang Mungkin Bukan Pemrogram Penuh Waktu, bukan karena mereka secara inheren lebih baik dalam sekuensing genom - kami memiliki bahasa yang dibuat khusus seperti R dan menyusun bahasa ilmiah seperti Fortran untuk itu.
kucing

0

Saya sangat percaya itu karena itu ide yang berbeda dan kuat. Anda menentang sistem kepercayaan. Barang-barang menjadi diterima atau populer melalui jaringan tidak memengaruhi berdasarkan prestasi. Juga tidak ada yang mau beradaptasi dengan tumpukan baru. Orang secara otomatis menolak hal-hal yang terlalu berbeda.

Jika Anda dapat menemukan cara untuk membuatnya menjadi modul npm reguler yang sepertinya tidak mungkin, maka Anda mungkin membuat beberapa orang menggunakannya.


Apakah maksud Anda bahwa pemrogram JS terkunci oleh vendor ke npm?
voodooattack

3
Pertanyaannya adalah tentang sistem runtime baru dan bukan beberapa modul npm dan juga konkurensi keadaan bersama yang bisa ditukar adalah ide lama.
Ankur

1
@ Voodooattack Maksud saya mereka di-brainlock untuk pendekatan yang sudah populer dan itu akan hampir mustahil untuk mengatasi bias status quo.
Jason Livesay
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.