Untuk tujuan apa sistem multithreaded digunakan. Delapan Aturan Sederhana untuk Mengembangkan Aplikasi Multithreaded

Topik apa yang paling banyak menimbulkan pertanyaan dan kesulitan bagi pemula? Ketika saya bertanya kepada guru saya dan programmer Java Alexander Pryakhin tentang hal ini, dia langsung menjawab: “Multithreading”. Terima kasih kepadanya atas ide dan bantuannya dalam mempersiapkan artikel ini!

Kami akan melihat ke dalam dunia aplikasi dan prosesnya, mencari tahu apa inti dari multithreading, kapan itu berguna, dan bagaimana mengimplementasikannya - menggunakan Java sebagai contoh. Jika Anda mempelajari bahasa OOP yang berbeda, jangan khawatir: prinsip dasarnya sama.

Tentang aliran dan asal-usulnya

Untuk memahami multithreading, mari kita pahami dulu apa itu proses. Proses adalah bagian dari memori virtual dan sumber daya yang dialokasikan OS untuk menjalankan program. Jika Anda membuka beberapa contoh aplikasi yang sama, sistem akan mengalokasikan proses untuk masing-masing. Di browser modern, proses terpisah dapat bertanggung jawab untuk setiap tab.

Anda mungkin telah menemukan "Task Manager" Windows (di Linux ini adalah "System Monitor") dan Anda tahu bahwa proses yang berjalan tidak perlu memuat sistem, dan yang paling "berat" sering membeku, sehingga harus dihentikan secara paksa .

Tetapi pengguna menyukai multitasking: jangan memberi mereka roti - biarkan mereka membuka selusin jendela dan melompat-lompat. Ada dilema: Anda perlu memastikan operasi aplikasi secara simultan dan pada saat yang sama mengurangi beban pada sistem sehingga tidak melambat. Katakanlah perangkat keras tidak dapat memenuhi kebutuhan pemilik - Anda perlu menyelesaikan masalah di tingkat perangkat lunak.

Kami ingin prosesor mengeksekusi lebih banyak instruksi dan memproses lebih banyak data per unit waktu. Artinya, kita perlu memasukkan lebih banyak kode yang dieksekusi di setiap irisan waktu. Pikirkan unit eksekusi kode sebagai objek — itu adalah utas.

Kasus yang kompleks lebih mudah untuk didekati jika Anda memecahnya menjadi beberapa kasus sederhana. Begitu juga ketika bekerja dengan memori: proses "berat" dibagi menjadi beberapa utas, yang menggunakan lebih sedikit sumber daya dan lebih cenderung mengirimkan kode ke kalkulator (bagaimana tepatnya - lihat di bawah).

Setiap aplikasi memiliki setidaknya satu proses, dan setiap proses memiliki setidaknya satu utas, yang disebut utas utama dan dari mana yang baru diluncurkan, jika perlu.

Perbedaan antara utas dan proses

    Utas menggunakan memori yang dialokasikan untuk proses, dan proses memerlukan ruang memorinya sendiri. Oleh karena itu, utas dibuat dan diselesaikan lebih cepat: sistem tidak perlu mengalokasikan ruang alamat baru setiap kali, dan kemudian melepaskannya.

    Proses masing-masing bekerja dengan data mereka sendiri - mereka dapat bertukar sesuatu hanya melalui mekanisme komunikasi antarproses. Utas mengakses data dan sumber daya satu sama lain secara langsung: apa yang telah diubah segera tersedia untuk semua orang. Utas dapat mengontrol "sesama" dalam proses, sedangkan proses mengontrol secara eksklusif "anak perempuannya". Oleh karena itu, beralih antar aliran lebih cepat dan komunikasi di antara mereka lebih mudah.

Apa kesimpulan dari ini? Jika Anda perlu memproses data dalam jumlah besar secepat mungkin, bagi menjadi beberapa bagian yang dapat diproses oleh utas terpisah, lalu kumpulkan hasilnya. Ini lebih baik daripada memunculkan proses yang haus sumber daya.

Tetapi mengapa aplikasi populer seperti Firefox menempuh rute menciptakan banyak proses? Karena untuk browser itulah pekerjaan tab yang terisolasi dapat diandalkan dan fleksibel. Jika ada yang salah dengan satu proses, tidak perlu menghentikan seluruh program - Anda dapat menyimpan setidaknya sebagian data.

Apa itu multithreading?

Jadi kita sampai pada poin utama. Multithreading adalah ketika proses aplikasi dipecah menjadi utas yang diproses secara paralel - pada satu unit waktu - oleh prosesor.

Beban komputasi didistribusikan antara dua atau lebih inti, sehingga antarmuka dan komponen program lainnya tidak memperlambat pekerjaan satu sama lain.

Aplikasi multi-utas juga dapat dijalankan pada prosesor inti tunggal, tetapi kemudian utas dieksekusi secara bergantian: yang pertama berfungsi, statusnya disimpan - yang kedua diizinkan untuk bekerja, disimpan - dikembalikan ke yang pertama atau meluncurkan yang ketiga, dll.

Orang sibuk mengeluh bahwa mereka hanya memiliki dua tangan. Proses dan program dapat memiliki tangan sebanyak mungkin untuk menyelesaikan tugas secepat mungkin.

Tunggu sinyal: sinkronisasi dalam aplikasi multi-utas

Bayangkan beberapa utas mencoba mengubah area data yang sama secara bersamaan. Perubahan siapa yang pada akhirnya akan diterima dan perubahan siapa yang akan dibatalkan? Untuk menghindari kebingungan saat berurusan dengan sumber daya bersama, utas perlu mengoordinasikan tindakannya. Untuk melakukan ini, mereka bertukar informasi menggunakan sinyal. Setiap utas memberi tahu yang lain apa yang dilakukannya dan perubahan apa yang diharapkan. Jadi data semua utas tentang status sumber daya saat ini disinkronkan.

Alat Sinkronisasi Dasar

Pengecualian bersama (pengecualian bersama, disingkat - mutex) - "bendera" menuju utas yang saat ini diizinkan untuk bekerja dengan sumber daya bersama. Menghilangkan akses oleh utas lain ke area memori yang ditempati. Mungkin ada beberapa mutex dalam sebuah aplikasi, dan mereka dapat dibagi antar proses. Ada masalah: mutex memaksa aplikasi untuk mengakses kernel sistem operasi setiap saat, yang mahal.

Tiang sinyal - memungkinkan Anda membatasi jumlah utas yang dapat mengakses sumber daya pada saat tertentu. Ini akan mengurangi beban pada prosesor saat mengeksekusi kode di mana ada kemacetan. Masalahnya adalah jumlah utas yang optimal tergantung pada mesin pengguna.

Peristiwa - Anda menentukan kondisi saat terjadinya kontrol mana yang ditransfer ke utas yang diinginkan. Aliran bertukar data peristiwa untuk mengembangkan dan secara logis melanjutkan tindakan satu sama lain. Satu menerima data, yang lain memeriksa kebenarannya, yang ketiga menyimpannya ke hard disk. Acara berbeda dalam cara pembatalannya. Jika Anda perlu memberi tahu beberapa utas tentang suatu peristiwa, Anda harus mengatur fungsi pembatalan secara manual untuk menghentikan sinyal. Jika hanya ada satu utas target, Anda dapat membuat acara setel ulang otomatis. Ini akan menghentikan sinyal itu sendiri setelah mencapai aliran. Acara dapat diantrekan untuk kontrol aliran yang fleksibel.

Bagian penting - mekanisme yang lebih kompleks yang menggabungkan penghitung loop dan semaphore. Penghitung memungkinkan Anda untuk menunda awal semafor untuk waktu yang diinginkan. Keuntungannya adalah kernel hanya diaktifkan jika bagian sedang sibuk dan semaphore perlu dihidupkan. Sisa waktu, utas berjalan dalam mode pengguna. Sayangnya, bagian hanya dapat digunakan dalam satu proses.

Bagaimana menerapkan multithreading di Java

Kelas Thread bertanggung jawab untuk bekerja dengan utas di Jawa. Membuat utas baru untuk menjalankan tugas berarti membuat turunan dari kelas Utas dan mengaitkannya dengan kode yang Anda inginkan. Ini dapat dilakukan dengan dua cara:

    subkelas Benang;

    mengimplementasikan antarmuka Runnable di kelas Anda, lalu meneruskan instance kelas ke konstruktor Thread.

Meskipun kami tidak akan menyentuh topik kebuntuan, ketika utas saling memblokir pekerjaan dan membeku, kami akan meninggalkannya untuk artikel berikutnya.

Contoh multithreading Java: ping-pong dengan mutex

Jika Anda berpikir sesuatu yang buruk akan terjadi, hembuskan napas. Kami akan mempertimbangkan untuk bekerja dengan objek sinkronisasi hampir dengan cara yang menyenangkan: dua utas akan dilemparkan oleh mutex. Tetapi kenyataannya, Anda akan melihat aplikasi nyata di mana hanya satu utas yang dapat memproses data yang tersedia untuk umum pada satu waktu.

Pertama, mari buat kelas yang mewarisi properti dari Thread yang sudah kita ketahui, dan tulis metode kickBall:

PingPongThread kelas publik memperluas Thread (PingPongThread (Nama string) (this.setName (nama); // menimpa nama thread) @Override public void run () (Bola bola = Ball.getBall (); while (ball.isInGame () ) (kickBall (bola);)) private void kickBall (Bola bola) (jika (! ball.getSide (). sama dengan (getName ())) (ball.kick (getName ());)))

Sekarang mari kita jaga bola. Dia tidak akan sederhana dengan kita, tetapi berkesan: sehingga dia bisa tahu siapa yang memukulnya, dari sisi mana dan berapa kali. Untuk melakukan ini, kami menggunakan mutex: itu akan mengumpulkan informasi tentang pekerjaan masing-masing utas - ini akan memungkinkan utas yang terisolasi untuk berkomunikasi satu sama lain. Setelah pukulan ke-15, kami akan mengeluarkan bola dari permainan, agar tidak melukainya secara serius.

Bola kelas publik (tendangan int pribadi = 0; instance Bola statis pribadi = Bola baru (); sisi String pribadi = ""; Bola pribadi () () Bola statis getBall () (contoh kembali;) tendangan batal yang disinkronkan (Nama pemain string) (tendangan ++; sisi = nama pemain; System.out.println (tendangan + "" + sisi);) String getSide () (sisi balik;) boolean isInGame () (kembali (tendangan< 15); } }

Dan sekarang dua utas pemain memasuki tempat kejadian. Mari kita panggil mereka, tanpa basa-basi lagi, Ping dan Pong:

PingPongGame kelas publik (PingPongThread player1 = PingPongThread baru ("Ping"); PingPongThread player2 = PingPongThread baru ("Pong"); Bola bola; PingPongGame () (ball = Ball.getBall ();) void startGame () melempar InterruptedException (player1 .start(); player2.start();))

"Stadion penuh orang - waktu untuk memulai pertandingan." Kami akan secara resmi mengumumkan pembukaan pertemuan - di kelas utama aplikasi:

PingPong kelas publik (public static void main (String args) melempar InterruptedException (Game PingPongGame = PingPongGame baru (); game.startGame ();))

Seperti yang Anda lihat, tidak ada yang marah di sini. Ini hanyalah pengantar multithreading untuk saat ini, tetapi Anda sudah tahu cara kerjanya, dan Anda dapat bereksperimen - membatasi durasi permainan bukan dengan jumlah pukulan, tetapi berdasarkan waktu, misalnya. Kita akan kembali ke topik multithreading nanti - kita akan melihat paket java.util.concurrent, perpustakaan Akka dan mekanisme volatil. Mari kita juga berbicara tentang mengimplementasikan multithreading dengan Python.

Pemrograman multithreaded pada dasarnya tidak berbeda dari menulis antarmuka pengguna grafis yang digerakkan oleh peristiwa atau bahkan menulis aplikasi sekuensial sederhana. Semua aturan penting yang mengatur enkapsulasi, pemisahan kekhawatiran, kopling longgar, dll. berlaku di sini. Tetapi banyak pengembang merasa sulit untuk menulis program multithreaded justru karena mereka mengabaikan aturan ini. Sebaliknya, mereka mencoba untuk mempraktekkan pengetahuan yang kurang penting tentang utas dan primitif sinkronisasi, yang diperoleh dari teks tentang pemrograman multithreading untuk pemula.

Jadi apa aturan ini?

Programmer lain, menghadapi masalah, berpikir: "Oh, tepatnya, kita perlu menerapkan ekspresi reguler." Dan sekarang dia sudah memiliki dua masalah - Jamie Zawinski.

Pemrogram lain, menghadapi masalah, berpikir: "Oh, benar, saya akan menggunakan aliran di sini." Dan sekarang dia memiliki sepuluh masalah - Bill Schindler.

Terlalu banyak programmer yang berusaha untuk menulis kode multi-utas jatuh ke dalam perangkap, seperti pahlawan balada Goethe " Murid Sang Penyihir". Pemrogram akan belajar cara membuat sekelompok utas yang, pada prinsipnya, berfungsi, tetapi cepat atau lambat mereka lepas kendali, dan pemrogram tidak tahu apa yang harus dilakukan.

Namun tidak seperti seorang penyihir yang putus sekolah, programmer yang malang tidak bisa mengharapkan kedatangan seorang penyihir yang kuat yang akan mengayunkan tongkatnya dan memulihkan ketertiban. Sebaliknya, pemrogram menggunakan trik yang paling tidak sedap dipandang, mencoba mengatasi masalah yang terus muncul. Hasilnya selalu sama: aplikasi yang terlalu rumit, terbatas, rapuh, dan tidak dapat diandalkan diperoleh. Ini memiliki ancaman kebuntuan terus-menerus dan bahaya lain yang melekat dalam kode multithreaded yang buruk. Saya bahkan tidak berbicara tentang crash yang tidak dapat dijelaskan, kinerja yang buruk, hasil kerja yang tidak lengkap atau salah.

Anda mungkin bertanya-tanya: mengapa ini terjadi? Kesalahpahaman yang umum adalah: "Pemrograman multi-utas sangat sulit." Tapi ini tidak terjadi. Jika program multi-utas tidak dapat diandalkan, maka biasanya gagal karena alasan yang sama seperti program berutas tunggal berkualitas rendah. Hanya saja programmer tidak mengikuti metode pengembangan yang mendasar, terkenal dan terbukti. Program multithreaded hanya tampaknya lebih kompleks, karena semakin banyak utas paralel yang salah, semakin banyak kekacauan yang mereka buat - dan jauh lebih cepat daripada satu utas.

Kesalahpahaman tentang "kompleksitas pemrograman multi-threaded" telah menyebar luas karena para pengembang yang telah berkembang secara profesional dalam menulis kode single-threaded, pertama kali menemukan multithreading dan tidak mengatasinya. Tetapi alih-alih memikirkan kembali bias dan kebiasaan kerja mereka, mereka dengan keras kepala memperbaiki fakta bahwa mereka tidak ingin bekerja dengan cara apa pun. Membuat alasan untuk perangkat lunak yang tidak dapat diandalkan dan tenggat waktu yang terlewat, orang-orang ini mengulangi hal yang sama: "pemrograman multithread sangat sulit."

Harap dicatat bahwa di atas saya berbicara tentang program khas yang menggunakan multithreading. Memang, ada skenario multi-utas yang kompleks - serta skenario utas tunggal yang kompleks. Tapi mereka tidak umum. Sebagai aturan, dalam praktiknya tidak ada hal supernatural yang diperlukan dari programmer. Kami memindahkan data, mengubahnya, melakukan beberapa perhitungan dari waktu ke waktu dan, akhirnya, menyimpan informasi dalam database atau menampilkannya di layar.

Tidak ada yang sulit untuk meningkatkan program single-threaded rata-rata dan mengubahnya menjadi multi-threaded. Setidaknya tidak seharusnya begitu. Kesulitan muncul karena dua alasan:

  • pemrogram tidak tahu bagaimana menerapkan metode pengembangan yang sederhana dan sudah terbukti;
  • sebagian besar informasi yang disajikan dalam buku tentang pemrograman multi-utas secara teknis benar, tetapi sama sekali tidak dapat diterapkan untuk memecahkan masalah yang diterapkan.

Konsep pemrograman yang paling penting bersifat universal. Mereka sama-sama berlaku untuk program single-threaded dan multi-threaded. Pemrogram yang tenggelam dalam pusaran arus sama sekali tidak mempelajari pelajaran penting ketika mereka menguasai kode utas tunggal. Saya dapat mengatakan ini karena pengembang semacam itu membuat kesalahan mendasar yang sama dalam program multi-utas dan satu-utas.

Mungkin pelajaran yang paling penting untuk dipelajari dalam enam puluh tahun sejarah pemrograman adalah: keadaan global yang bisa berubah- kejahatan... Benar-benar jahat. Program yang bergantung pada keadaan yang dapat berubah secara global relatif sulit untuk dipikirkan, dan umumnya tidak dapat diandalkan karena terlalu banyak cara untuk mengubah keadaan. Ada banyak penelitian yang mengkonfirmasi prinsip umum ini, ada pola desain yang tak terhitung jumlahnya, tujuan utamanya adalah menerapkan satu atau lain cara menyembunyikan data. Untuk membuat program Anda lebih dapat diprediksi, cobalah untuk menghilangkan keadaan yang dapat berubah sebanyak mungkin.

Dalam program serial single-threaded, kemungkinan korupsi data berbanding lurus dengan jumlah komponen yang dapat memodifikasi data.

Sebagai aturan, tidak mungkin untuk sepenuhnya menghilangkan keadaan global, tetapi pengembang memiliki alat yang sangat efektif di gudang senjatanya yang memungkinkan Anda untuk secara ketat mengontrol komponen program mana yang dapat mengubah keadaan. Selain itu, kami mempelajari cara membuat lapisan API restriktif di sekitar struktur data primitif. Oleh karena itu, kami memiliki kontrol yang baik atas bagaimana struktur data ini berubah.

Masalah keadaan yang dapat berubah secara global secara bertahap menjadi jelas pada akhir 80-an dan awal 90-an, dengan menjamurnya program yang digerakkan oleh peristiwa. Program tidak lagi dimulai "dari awal" atau mengikuti satu jalur eksekusi yang dapat diprediksi "sampai akhir". Program modern memiliki status awal, setelah keluar dari peristiwa yang terjadi di dalamnya - dalam urutan yang tidak dapat diprediksi, dengan interval waktu yang bervariasi. Kode tetap single-threaded, tetapi sudah menjadi asynchronous. Kemungkinan korupsi data meningkat justru karena urutan kejadian sangat penting. Situasi semacam ini cukup umum: jika peristiwa B terjadi setelah peristiwa A, maka semuanya bekerja dengan baik. Tetapi jika peristiwa A terjadi setelah peristiwa B, dan peristiwa C memiliki waktu untuk campur tangan di antara mereka, maka data mungkin terdistorsi tanpa dapat dikenali.

Jika aliran paralel terlibat, masalah ini semakin diperburuk, karena beberapa metode dapat secara bersamaan beroperasi pada keadaan global. Menjadi tidak mungkin untuk menilai bagaimana tepatnya keadaan global berubah. Kami sudah berbicara tidak hanya tentang fakta bahwa peristiwa dapat terjadi dalam urutan yang tidak dapat diprediksi, tetapi juga tentang fakta bahwa status beberapa utas eksekusi dapat diperbarui. serentak... Dengan pemrograman asinkron, Anda setidaknya dapat memastikan bahwa peristiwa tertentu tidak dapat terjadi sebelum peristiwa lain selesai diproses. Artinya, adalah mungkin untuk mengatakan dengan pasti seperti apa keadaan global pada akhir pemrosesan suatu peristiwa tertentu. Dalam kode multi-utas, sebagai suatu peraturan, tidak mungkin untuk mengatakan peristiwa mana yang akan terjadi secara paralel, sehingga tidak mungkin untuk menggambarkan dengan pasti keadaan global setiap saat.

Program multithreaded dengan keadaan yang dapat berubah secara global yang luas adalah salah satu contoh paling fasih dari prinsip ketidakpastian Heisenberg yang saya ketahui. Tidak mungkin memeriksa status program tanpa mengubah perilakunya.

Ketika saya memulai philippic lain tentang keadaan yang dapat berubah secara global (intinya diuraikan dalam beberapa paragraf sebelumnya), programmer memutar mata mereka dan meyakinkan saya bahwa mereka telah mengetahui semua ini sejak lama. Tetapi jika Anda mengetahui hal ini, mengapa Anda tidak dapat mengetahuinya dari kode Anda? Program dijejali dengan keadaan global yang bisa berubah, dan pemrogram bertanya-tanya mengapa kodenya tidak berfungsi.

Tidak mengherankan, pekerjaan yang paling penting dalam pemrograman multithreaded terjadi selama fase desain. Diperlukan untuk mendefinisikan dengan jelas apa yang harus dilakukan program, mengembangkan modul independen untuk melakukan semua fungsi, menjelaskan secara rinci data apa yang diperlukan untuk modul mana, dan menentukan cara pertukaran informasi antar modul ( Ya, jangan lupa untuk menyiapkan T-shirt bagus untuk semua orang yang terlibat dalam proyek ini. Hal pertama.- kira-kira ed. aslinya). Proses ini pada dasarnya tidak berbeda dengan merancang program single-threaded. Kunci keberhasilan, seperti halnya kode utas tunggal, adalah membatasi interaksi antar modul. Jika Anda dapat menghilangkan status berbagi yang dapat diubah, masalah berbagi data tidak akan muncul.

Seseorang mungkin berpendapat bahwa terkadang tidak ada waktu untuk desain program yang begitu rumit, yang akan memungkinkan untuk dilakukan tanpa negara global. Saya percaya bahwa adalah mungkin dan perlu untuk menghabiskan waktu untuk ini. Tidak ada yang mempengaruhi program multithreaded secara destruktif seperti mencoba mengatasi keadaan global yang bisa berubah. Semakin banyak detail yang harus Anda kelola, semakin besar kemungkinan program Anda akan mencapai puncak dan macet.

Dalam aplikasi realistis, harus ada keadaan bersama tertentu yang dapat berubah. Dan di sinilah kebanyakan programmer mulai mengalami masalah. Pemrogram melihat bahwa status bersama diperlukan di sini, beralih ke gudang senjata multithreaded dan mengambil dari sana alat paling sederhana: kunci universal (bagian kritis, mutex, atau apa pun namanya). Mereka tampaknya percaya bahwa pengecualian bersama akan menyelesaikan semua masalah berbagi data.

Jumlah masalah yang dapat muncul dengan kunci tunggal seperti itu sangat mengejutkan. Pertimbangan harus diberikan pada kondisi balapan, masalah gating dengan pemblokiran yang terlalu luas, dan masalah keadilan alokasi hanyalah beberapa contoh. Jika Anda memiliki banyak kunci, terutama jika kunci itu bersarang, Anda juga perlu mengambil tindakan terhadap kebuntuan, kebuntuan dinamis, pemblokiran antrian, dan ancaman lain yang terkait dengan konkurensi. Selain itu, ada masalah pemblokiran tunggal yang melekat.
Ketika saya menulis atau meninjau kode, saya memiliki aturan besi yang hampir sempurna: jika Anda membuat kunci, maka Anda tampaknya telah membuat kesalahan di suatu tempat.

Pernyataan ini dapat dikomentari dengan dua cara:

  1. Jika Anda perlu mengunci, maka Anda mungkin memiliki status global yang dapat diubah yang ingin Anda lindungi dari pembaruan bersamaan. Kehadiran keadaan yang dapat berubah secara global adalah cacat dalam fase desain aplikasi. Tinjau dan desain ulang.
  2. Menggunakan kunci dengan benar tidak mudah, dan bisa sangat sulit untuk melokalisasi bug yang terkait dengan penguncian. Kemungkinan besar Anda akan salah menggunakan kunci. Jika saya melihat kunci, dan program berperilaku tidak biasa, maka hal pertama yang saya lakukan adalah memeriksa kode yang bergantung pada kunci tersebut. Dan saya biasanya menemukan masalah di dalamnya.

Kedua interpretasi ini benar.

Menulis kode multithreaded itu mudah. Tetapi sangat, sangat sulit untuk menggunakan primitif sinkronisasi dengan benar. Mungkin Anda tidak memenuhi syarat untuk menggunakan bahkan satu kunci dengan benar. Bagaimanapun, kunci dan primitif sinkronisasi lainnya adalah konstruksi yang didirikan pada tingkat seluruh sistem. Orang yang memahami pemrograman paralel jauh lebih baik daripada Anda menggunakan primitif ini untuk membangun struktur data bersamaan dan konstruksi sinkronisasi tingkat tinggi. Dan Anda dan saya, programmer biasa, hanya mengambil konstruksi seperti itu dan menggunakannya dalam kode kita. Seorang programmer aplikasi tidak boleh menggunakan primitif sinkronisasi tingkat rendah lebih sering daripada melakukan panggilan langsung ke driver perangkat. Artinya, hampir tidak pernah.

Mencoba menggunakan kunci untuk memecahkan masalah berbagi data seperti memadamkan api dengan oksigen cair. Seperti api, masalah seperti itu lebih mudah dicegah daripada diperbaiki. Jika Anda menyingkirkan status bersama, Anda juga tidak perlu menyalahgunakan primitif sinkronisasi.

Sebagian besar dari apa yang Anda ketahui tentang multithreading tidak relevan

Dalam tutorial multithreading untuk pemula, Anda akan mempelajari apa itu thread. Kemudian penulis akan mulai mempertimbangkan berbagai cara di mana utas ini dapat bekerja secara paralel - misalnya, berbicara tentang mengontrol akses ke data bersama menggunakan kunci dan semafor, memikirkan hal-hal apa yang dapat terjadi ketika bekerja dengan acara. Akan melihat dari dekat variabel kondisi, hambatan memori, bagian kritis, mutex, bidang volatil, dan operasi atom. Contoh bagaimana menggunakan konstruksi tingkat rendah ini untuk melakukan segala macam operasi sistem akan dibahas. Setelah membaca materi ini sampai setengah, programmer memutuskan bahwa dia sudah cukup tahu tentang semua primitif ini dan penggunaannya. Lagi pula, jika saya tahu cara kerjanya di tingkat sistem, saya bisa menerapkannya dengan cara yang sama di tingkat aplikasi. Ya?

Bayangkan memberi tahu seorang remaja cara merakit mesin pembakaran internal sendiri. Kemudian, tanpa pelatihan mengemudi, Anda meletakkannya di belakang kemudi mobil dan berkata, "Pergi!" Remaja itu memahami cara kerja sebuah mobil, tetapi tidak tahu bagaimana berpindah dari titik A ke titik B di atasnya.

Memahami cara kerja utas di tingkat sistem biasanya tidak membantu sama sekali di tingkat aplikasi. Saya tidak menyarankan bahwa programmer tidak perlu mempelajari semua detail tingkat rendah ini. Hanya saja, jangan berharap bisa langsung menerapkan pengetahuan ini saat merancang atau mengembangkan aplikasi bisnis.

Literatur threading pengantar (dan kursus akademik terkait) tidak boleh mengeksplorasi konstruksi tingkat rendah seperti itu. Anda perlu fokus pada pemecahan kelas masalah yang paling umum dan menunjukkan kepada pengembang bagaimana masalah ini diselesaikan menggunakan kemampuan tingkat tinggi. Pada prinsipnya, sebagian besar aplikasi bisnis adalah program yang sangat sederhana. Mereka membaca data dari satu atau lebih perangkat input, melakukan beberapa pemrosesan kompleks pada data ini (misalnya, dalam proses, mereka meminta beberapa data lagi), dan kemudian menampilkan hasilnya.

Seringkali, program semacam itu sangat cocok dengan model penyedia-konsumen, yang hanya membutuhkan tiga utas:

  • aliran input membaca data dan meletakkannya di antrian input;
  • utas pekerja membaca catatan dari antrian input, memprosesnya, dan memasukkan hasilnya ke dalam antrian keluaran;
  • aliran keluaran membaca entri dari antrian keluaran dan menyimpannya.

Ketiga utas ini bekerja secara independen, komunikasi di antara mereka terjadi di tingkat antrian.

Meskipun secara teknis antrian ini dapat dianggap sebagai zona keadaan bersama, dalam praktiknya antrian ini hanya saluran komunikasi di mana sinkronisasi internal mereka sendiri beroperasi. Dukungan antrian bekerja dengan banyak produsen dan konsumen sekaligus, Anda dapat menambah dan menghapus item di dalamnya secara paralel.

Karena tahap input, pemrosesan, dan output terisolasi satu sama lain, implementasinya dapat dengan mudah diubah tanpa mempengaruhi program lainnya. Selama jenis data dalam antrian tidak berubah, Anda dapat memfaktorkan ulang komponen program individual sesuai kebijaksanaan Anda. Selain itu, karena jumlah pemasok dan konsumen yang sewenang-wenang ikut dalam antrian, tidak sulit untuk menambah produsen/konsumen lain. Kami dapat memiliki lusinan aliran input yang menulis informasi ke antrean yang sama, atau lusinan utas pekerja yang mengambil informasi dari antrean input dan mencerna data. Dalam kerangka satu komputer, model seperti itu berskala dengan baik.

Yang terpenting, bahasa pemrograman dan perpustakaan modern sangat memudahkan pembuatan aplikasi produsen-konsumen. Di .NET, Anda akan menemukan Koleksi Paralel dan Perpustakaan Aliran Data TPL. Java memiliki layanan Executor serta BlockingQueue dan kelas lain dari namespace java.util.concurrent. C ++ memiliki perpustakaan threading Boost dan perpustakaan Thread Building Blocks Intel. Microsoft Visual Studio 2013 memperkenalkan agen asinkron. Pustaka serupa juga tersedia dalam Python, JavaScript, Ruby, PHP dan, sejauh yang saya tahu, banyak bahasa lainnya. Anda dapat membuat aplikasi produsen-konsumen menggunakan salah satu paket ini, tanpa harus menggunakan kunci, semafor, variabel kondisi, atau primitif sinkronisasi lainnya.

Berbagai macam primitif sinkronisasi digunakan secara bebas di perpustakaan ini. Ini baik-baik saja. Semua perpustakaan ini ditulis oleh orang-orang yang memahami multithreading jauh lebih baik daripada rata-rata programmer. Bekerja dengan perpustakaan seperti itu praktis sama dengan menggunakan perpustakaan bahasa runtime. Ini dapat dibandingkan dengan pemrograman dalam bahasa tingkat tinggi daripada bahasa rakitan.

Model pemasok-konsumen hanyalah salah satu dari banyak contoh. Pustaka di atas berisi kelas yang dapat digunakan untuk mengimplementasikan banyak pola desain threading umum tanpa masuk ke detail tingkat rendah. Dimungkinkan untuk membuat aplikasi multithread skala besar tanpa khawatir tentang bagaimana utas dikoordinasikan dan disinkronkan.

Bekerja dengan perpustakaan

Jadi, membuat program multi-utas pada dasarnya tidak berbeda dengan menulis program sinkron berutas tunggal. Prinsip-prinsip penting enkapsulasi dan penyembunyian data bersifat universal dan hanya bertambah penting ketika beberapa utas bersamaan terlibat. Jika Anda mengabaikan aspek-aspek penting ini, maka bahkan pengetahuan paling komprehensif tentang threading tingkat rendah tidak akan menyelamatkan Anda.

Pengembang modern harus menyelesaikan banyak masalah di tingkat pemrograman aplikasi, kebetulan tidak ada waktu untuk memikirkan apa yang terjadi di tingkat sistem. Semakin rumit aplikasi, semakin rumit detail yang harus disembunyikan di antara level API. Kami telah melakukan ini selama lebih dari belasan tahun. Dapat dikatakan bahwa penyembunyian kualitatif kompleksitas sistem dari programmer adalah alasan utama mengapa programmer mampu menulis aplikasi modern. Dalam hal ini, bukankah kita menyembunyikan kerumitan sistem dengan menerapkan loop pesan UI, membangun protokol komunikasi tingkat rendah, dll.?

Situasinya mirip dengan multithreading. Sebagian besar skenario multi-utas yang mungkin dihadapi oleh programmer aplikasi bisnis rata-rata sudah dikenal dan diimplementasikan dengan baik di perpustakaan. Fungsi perpustakaan melakukan pekerjaan yang baik untuk menyembunyikan kompleksitas paralelisme yang luar biasa. Anda perlu mempelajari cara menggunakan pustaka ini dengan cara yang sama seperti Anda menggunakan pustaka elemen antarmuka pengguna, protokol komunikasi, dan banyak alat lain yang berfungsi. Serahkan multithreading tingkat rendah ke spesialis - penulis perpustakaan yang digunakan dalam pembuatan aplikasi.

NS Artikel ini bukan untuk penjinak Python berpengalaman, yang mengungkap bola ular ini adalah permainan anak-anak, melainkan gambaran dangkal tentang kemampuan multithreading untuk python yang baru kecanduan.

Sayangnya, tidak ada begitu banyak materi dalam bahasa Rusia tentang topik multithreading dengan Python, dan pythoner yang belum pernah mendengar apa pun, misalnya, tentang GIL, mulai menemukan saya dengan keteraturan yang patut ditiru. Pada artikel ini saya akan mencoba menjelaskan fitur paling dasar dari python multithreaded, memberi tahu Anda apa itu GIL dan bagaimana cara hidup dengannya (atau tanpanya), dan banyak lagi.


Python adalah bahasa pemrograman yang menawan. Ini dengan sempurna menggabungkan banyak paradigma pemrograman. Sebagian besar tugas yang dapat dipenuhi oleh seorang programmer diselesaikan di sini dengan mudah, elegan, dan ringkas. Tetapi untuk semua masalah ini, solusi single-threaded seringkali cukup, dan program single-threaded biasanya dapat diprediksi dan mudah di-debug. Hal yang sama tidak dapat dikatakan tentang program multithreaded dan multiprocessing.

Aplikasi multithread


Python memiliki modul threading , dan memiliki semua yang Anda butuhkan untuk pemrograman multi-utas: ada berbagai jenis kunci, dan semafor, dan mekanisme acara. Dalam satu kata - semua yang diperlukan untuk sebagian besar program multithreaded. Selain itu, menggunakan semua alat ini cukup sederhana. Mari kita pertimbangkan contoh program yang memulai 2 utas. Satu utas menulis sepuluh "0", yang lain - sepuluh "1", dan ketat secara bergantian.

benang impor

penulis def

untuk saya di xrange (10):

cetak x

Acara_untuk_set.set ()

# init acara

e1 = threading.Event()

e2 = threading.Event()

# init utas

0, e1, e2))

1, e2, e1))

#mulai utas

t1.mulai ()

t2.mulai ()

t1.bergabung ()

t2.bergabung ()


Tidak ada kode sihir atau voodoo. Kodenya jelas dan konsisten. Selain itu, seperti yang Anda lihat, kami telah membuat aliran dari suatu fungsi. Ini sangat nyaman untuk tugas-tugas kecil. Kode ini juga cukup fleksibel. Misalkan kita memiliki proses ketiga yang menulis “2”, maka kodenya akan terlihat seperti ini:

benang impor

penulis def (x, event_for_wait, event_for_set):

untuk saya di xrange (10):

Event_for_wait.wait () # tunggu acara

Event_for_wait.clear () # acara bersih untuk masa depan

cetak x

Acara_untuk_set.set () # atur acara untuk utas tetangga

# init acara

e1 = threading.Event()

e2 = threading.Event()

e3 = threading.Event()

# init utas

t1 = threading.Thread (target = penulis, args = ( 0, e1, e2))

t2 = threading.Thread (target = penulis, args = ( 1, e2, e3))

t3 = threading.Thread (target = penulis, args = ( 2, e3, e1))

#mulai utas

t1.mulai ()

t2.mulai ()

t3.mulai ()

e1.set () # memulai acara pertama

# gabungkan utas ke utas utama

t1.bergabung ()

t2.bergabung ()

t3.bergabung ()


Kami menambahkan acara baru, utas baru dan sedikit mengubah parameter yang
stream dimulai (tentu saja, Anda dapat menulis solusi yang lebih umum menggunakan, misalnya, MapReduce, tetapi ini di luar cakupan artikel ini).
Seperti yang Anda lihat, masih belum ada keajaiban. Semuanya sederhana dan lugas. Mari kita pergi lebih jauh.

Kunci Penerjemah Global


Ada dua alasan paling umum untuk menggunakan utas: pertama, untuk meningkatkan efisiensi penggunaan arsitektur multicore dari prosesor modern, dan karenanya kinerja program;
kedua, jika kita perlu membagi logika program menjadi bagian paralel, sebagian atau seluruhnya asinkron (misalnya, untuk dapat melakukan ping ke beberapa server secara bersamaan).

Dalam kasus pertama, kita dihadapkan pada keterbatasan Python (atau lebih tepatnya implementasi CPython utamanya) sebagai Global Interpreter Lock (atau disingkat GIL). Konsep GIL adalah bahwa hanya satu utas yang dapat dieksekusi oleh prosesor pada satu waktu. Hal ini dilakukan agar tidak ada perebutan antara utas untuk variabel yang terpisah. Utas yang dapat dieksekusi memperoleh akses ke seluruh lingkungan. Fitur implementasi utas di Python ini sangat menyederhanakan pekerjaan dengan utas dan memberikan keamanan utas tertentu.

Tetapi ada poin yang halus: tampaknya aplikasi multi-utas akan berjalan dengan jumlah waktu yang persis sama dengan aplikasi berutas tunggal yang melakukan hal yang sama, atau jumlah waktu eksekusi setiap utas pada CPU. Tapi di sini satu efek yang tidak menyenangkan menanti kita. Pertimbangkan programnya:

dengan open ("test1.txt", "w") sebagai fout:

untuk saya di xrange (1000000):

cetak >> lecek, 1


Program ini hanya menulis satu juta baris "1" ke file dan melakukannya dalam ~ 0,35 detik di komputer saya.

Pertimbangkan program lain:

dari threading impor Thread

penulis def (nama file, n):

dengan open (nama file, "w") sebagai fout:

untuk i di xrange (n):

cetak >> lecek, 1

t1 = Utas (target = penulis, argumen = ("test2.txt", 500.000,))

t2 = Utas (target = penulis, argumen = ("test3.txt", 500.000,))

t1.mulai ()

t2.mulai ()

t1.bergabung ()

t2.bergabung ()


Program ini membuat 2 utas. Di setiap utas, ia menulis ke file terpisah, setengah juta baris "1". Padahal, jumlah pekerjaan sama seperti pada program sebelumnya. Namun seiring berjalannya waktu, efek menarik didapat di sini. Program ini dapat berjalan dari 0,7 detik hingga 7 detik. Mengapa ini terjadi?

Ini disebabkan oleh fakta bahwa ketika sebuah utas tidak memerlukan sumber daya CPU, ia melepaskan GIL, dan pada saat ini ia dapat mencoba mendapatkannya, dan utas lain, serta utas utama. Pada saat yang sama, sistem operasi, mengetahui bahwa ada banyak inti, dapat memperburuk segalanya dengan mencoba mendistribusikan utas di antara inti.

UPD: saat ini, di Python 3.2, ada peningkatan implementasi GIL, di mana masalah ini diselesaikan sebagian, khususnya, karena fakta bahwa setiap utas, setelah kehilangan kontrol, menunggu beberapa saat sebelum dapat kembali menangkap GIL (ada presentasi yang bagus dalam bahasa Inggris)

“Jadi, Anda tidak dapat menulis program multithread yang efisien dengan Python?” Anda bertanya. Tidak, tentu saja, ada jalan keluar, dan bahkan beberapa.

Aplikasi multiproses


Untuk memecahkan masalah yang dijelaskan dalam paragraf sebelumnya, Python memiliki modul subproses ... Kita dapat menulis program yang ingin kita jalankan dalam utas paralel (sebenarnya, sudah menjadi proses). Dan jalankan di satu atau lebih utas di program lain. Ini akan sangat mempercepat program kita, karena thread yang dibuat di GIL launcher tidak mengambil, tetapi hanya menunggu proses running selesai. Namun, metode ini memiliki banyak masalah. Masalah utamanya adalah menjadi sulit untuk mentransfer data antar proses. Anda harus entah bagaimana membuat serial objek, membangun komunikasi melalui PIPA atau alat lain, tetapi semua ini pasti membawa overhead dan kode menjadi sulit untuk dipahami.

Pendekatan lain dapat membantu kami di sini. Python memiliki modul multiprosesor ... Dalam hal fungsionalitas, modul ini menyerupai threading ... Misalnya, proses dapat dibuat dengan cara yang sama dari fungsi biasa. Metode untuk bekerja dengan proses hampir sama dengan utas dari modul threading. Tetapi untuk sinkronisasi proses dan pertukaran data, biasanya menggunakan alat lain. Kita berbicara tentang antrian (Queue) dan pipa (Pipe). Namun, analog dari kunci, peristiwa, dan semaphore yang ada di threading juga ada di sini.

Selain itu, modul multiprosesor memiliki mekanisme untuk bekerja dengan memori bersama. Untuk ini, modul memiliki kelas variabel (Nilai) dan array (Array), yang dapat "dibagi" antara proses. Untuk kenyamanan bekerja dengan variabel bersama, Anda dapat menggunakan kelas manajer. Mereka lebih fleksibel dan lebih mudah digunakan, tetapi lebih lambat. Perlu dicatat bahwa ada peluang bagus untuk membuat tipe umum dari modul ctypes menggunakan modul multiprocessing.sharedctypes.

Juga dalam modul multiprosesor ada mekanisme untuk membuat kumpulan proses. Mekanisme ini sangat nyaman digunakan untuk menerapkan pola Master-Worker atau untuk menerapkan Peta paralel (yang dalam arti tertentu merupakan kasus khusus Master-Worker).

Dari masalah utama dengan bekerja dengan modul multiprosesor, perlu dicatat ketergantungan platform relatif dari modul ini. Karena bekerja dengan proses diatur secara berbeda dalam sistem operasi yang berbeda, beberapa batasan dikenakan pada kode. Misalnya, Windows tidak memiliki mekanisme fork, sehingga titik pemisahan proses harus dibungkus dengan:

jika __name__ == "__main__":


Namun, desain ini sudah merupakan bentuk yang bagus.

Apa lagi...


Ada perpustakaan dan pendekatan lain untuk menulis aplikasi paralel dengan Python. Misalnya, Anda dapat menggunakan Hadoop + Python atau berbagai implementasi Python MPI (pyMPI, mpi4py). Anda bahkan dapat menggunakan pembungkus pustaka C ++ atau Fortran yang ada. Di sini orang dapat menyebutkan kerangka kerja / perpustakaan seperti Pyro, Twisted, Tornado dan banyak lainnya. Tetapi semua ini sudah di luar cakupan artikel ini.

Jika Anda menyukai gaya saya, maka di artikel berikutnya saya akan mencoba memberi tahu Anda cara menulis juru bahasa sederhana di PLY dan untuk apa mereka dapat digunakan.

Bab 10.

Aplikasi multithread

Multitasking dalam sistem operasi modern diterima begitu saja [ Sebelum munculnya Apple OS X, tidak ada sistem operasi multitasking modern di komputer Macintosh. Sangat sulit untuk merancang sistem operasi dengan multitasking penuh, sehingga OS X harus berbasis Unix.]. Pengguna mengharapkan bahwa ketika editor teks dan klien email diluncurkan pada saat yang sama, program ini tidak akan bertentangan, dan ketika menerima email, editor tidak akan berhenti bekerja. Ketika beberapa program diluncurkan pada saat yang sama, sistem operasi dengan cepat beralih di antara program, memberi mereka prosesor secara bergantian (kecuali, tentu saja, beberapa prosesor diinstal di komputer). Hasil dari, ilusi menjalankan beberapa program secara bersamaan, karena bahkan juru ketik terbaik (dan koneksi internet tercepat) tidak dapat mengikuti prosesor modern.

Multithreading, dalam arti tertentu, dapat dilihat sebagai tingkat multitasking berikutnya: alih-alih beralih di antara yang berbeda program, sistem operasi beralih di antara bagian-bagian berbeda dari program yang sama. Misalnya, klien email multi-utas memungkinkan Anda menerima pesan email baru saat membaca atau menulis pesan baru. Saat ini, multithreading juga diterima begitu saja oleh banyak pengguna.

VB tidak pernah memiliki dukungan multithreading normal. Benar, salah satu varietasnya muncul di VB5 - model streaming kolaboratif(pengikatan apartemen). Seperti yang akan segera Anda lihat, model kolaboratif memberi programmer beberapa manfaat multithreading, tetapi tidak memanfaatkan semua fitur sepenuhnya. Cepat atau lambat, Anda harus beralih dari mesin pelatihan ke mesin nyata, dan VB .NET menjadi versi pertama VB dengan dukungan untuk model multithreaded gratis.

Namun, multithreading bukanlah salah satu fitur yang mudah diimplementasikan dalam bahasa pemrograman dan mudah dikuasai oleh programmer. Mengapa?

Karena dalam aplikasi multithreaded, kesalahan yang sangat rumit dapat terjadi yang muncul dan menghilang secara tidak terduga (dan kesalahan seperti itu adalah yang paling sulit untuk di-debug).

Peringatan jujur: multithreading adalah salah satu area pemrograman yang paling sulit. Sedikit perhatian mengarah pada munculnya kesalahan yang sulit dipahami, koreksi yang membutuhkan jumlah astronomi. Untuk alasan ini, bab ini berisi banyak buruk contoh - kami sengaja menulisnya sedemikian rupa untuk menunjukkan kesalahan umum. Ini adalah pendekatan teraman untuk mempelajari pemrograman multithread: Anda harus dapat menemukan masalah potensial ketika semuanya tampak berjalan dengan baik pada pandangan pertama, dan tahu bagaimana menyelesaikannya. Jika Anda ingin menggunakan teknik pemrograman multi-utas, Anda tidak dapat melakukannya tanpanya.

Bab ini akan meletakkan dasar yang kuat untuk pekerjaan independen lebih lanjut, tetapi kami tidak akan dapat menjelaskan pemrograman multithreaded di semua seluk-beluk - hanya dokumentasi tercetak pada kelas-kelas dari namespace Threading yang membutuhkan lebih dari 100 halaman. Jika Anda ingin menguasai pemrograman multithreaded di tingkat yang lebih tinggi, lihat buku-buku khusus.

Tapi tidak peduli seberapa berbahayanya pemrograman multithreaded, itu sangat diperlukan untuk solusi profesional dari beberapa masalah. Jika program Anda tidak menggunakan multithreading jika sesuai, pengguna akan menjadi sangat frustrasi dan memilih produk lain. Misalnya, hanya dalam versi keempat dari program email populer Eudora muncul kemampuan multithreaded, yang tanpanya tidak mungkin membayangkan program modern apa pun untuk bekerja dengan email. Pada saat Eudora memperkenalkan dukungan multithreading, banyak pengguna (termasuk salah satu penulis buku ini) telah beralih ke produk lain.

Akhirnya, di .NET, program single-threaded sama sekali tidak ada. Semuanya Program .NET multithreaded karena pengumpul sampah berjalan sebagai proses latar belakang dengan prioritas rendah. Seperti yang ditunjukkan di bawah ini, untuk pemrograman grafis yang serius di .NET, threading yang tepat dapat membantu mencegah antarmuka grafis dari pemblokiran saat program menjalankan operasi yang panjang.

Memperkenalkan multithreading

Setiap program bekerja secara spesifik konteks, menggambarkan distribusi kode dan data dalam memori. Dengan menyimpan konteks, status aliran program sebenarnya disimpan, yang memungkinkan Anda untuk memulihkannya di masa mendatang dan melanjutkan eksekusi program.

Menyimpan konteks datang dengan biaya dalam waktu dan memori. Sistem operasi mengingat status utas program dan mentransfer kontrol ke utas lain. Ketika program ingin melanjutkan eksekusi utas yang ditangguhkan, konteks yang disimpan harus dipulihkan, yang membutuhkan waktu lebih lama. Oleh karena itu, multithreading hanya boleh digunakan ketika manfaat mengimbangi semua biaya. Beberapa contoh tipikal tercantum di bawah ini.

  • Fungsionalitas program secara jelas dan alami dibagi menjadi beberapa operasi heterogen, seperti dalam contoh menerima email dan menyiapkan pesan baru.
  • Program melakukan perhitungan yang panjang dan rumit, dan Anda tidak ingin antarmuka grafis diblokir selama perhitungan berlangsung.
  • Program berjalan pada komputer multiprosesor dengan sistem operasi yang mendukung penggunaan beberapa prosesor (selama jumlah utas aktif tidak melebihi jumlah prosesor, eksekusi paralel praktis bebas dari biaya yang terkait dengan pemindahan utas).

Sebelum beralih ke mekanisme program multithreaded, perlu untuk menunjukkan satu keadaan yang sering menyebabkan kebingungan di kalangan pemula di bidang pemrograman multithreaded.

Prosedur, bukan objek, dieksekusi dalam aliran program.

Sulit untuk mengatakan apa yang dimaksud dengan ungkapan "objek sedang mengeksekusi", tetapi salah satu penulis sering mengajar seminar tentang pemrograman multithreading dan pertanyaan ini diajukan lebih sering daripada yang lain. Mungkin seseorang berpikir bahwa pekerjaan utas program dimulai dengan panggilan ke metode kelas Baru, setelah itu utas memproses semua pesan yang diteruskan ke objek yang sesuai. Representasi seperti itu sangat salah. Satu objek dapat berisi beberapa utas yang menjalankan metode yang berbeda (dan kadang-kadang bahkan sama), sedangkan pesan dari objek ditransmisikan dan diterima oleh beberapa utas yang berbeda (omong-omong, ini adalah salah satu alasan yang memperumit pemrograman multi-utas: untuk men-debug program, Anda perlu mencari tahu utas mana pada saat tertentu yang melakukan prosedur ini atau itu!).

Karena utas dibuat dari metode objek, objek itu sendiri biasanya dibuat sebelum utas. Setelah berhasil membuat objek, program membuat utas, meneruskannya ke alamat metode objek, dan hanya setelah itu memberikan perintah untuk memulai eksekusi thread. Prosedur yang membuat utas, seperti semua prosedur, dapat membuat objek baru, melakukan operasi pada objek yang ada, dan memanggil prosedur dan fungsi lain yang ada dalam cakupannya.

Metode umum kelas juga dapat dieksekusi di utas program. Dalam hal ini, ingat juga keadaan penting lainnya: utas diakhiri dengan keluar dari prosedur pembuatannya. Penyelesaian normal dari alur program tidak mungkin sampai prosedur keluar.

Utas dapat berakhir tidak hanya secara alami, tetapi juga secara tidak normal. Hal ini umumnya tidak dianjurkan. Lihat Mengakhiri dan Mengganggu Aliran untuk informasi selengkapnya.

Alat .NET inti yang terkait dengan penggunaan utas program terkonsentrasi di namespace Threading. Oleh karena itu, sebagian besar program multithreaded harus dimulai dengan baris berikut:

Sistem Impor.Threading

Mengimpor namespace membuat program Anda lebih mudah diketik dan mengaktifkan teknologi IntelliSense.

Hubungan langsung arus dengan prosedur menunjukkan bahwa dalam gambar ini, delegasi(lihat bab 6). Secara khusus, ruang nama Threading menyertakan delegasi ThreadStart, yang biasanya digunakan saat memulai utas program. Sintaks untuk menggunakan delegasi ini terlihat seperti ini:

Delegasi Publik Sub ThreadStart ()

Kode yang dipanggil dengan delegasi ThreadStart tidak boleh memiliki parameter atau nilai kembalian, sehingga utas tidak dapat dibuat untuk fungsi (yang mengembalikan nilai) dan untuk prosedur dengan parameter. Untuk mentransfer informasi dari aliran, Anda juga harus mencari cara alternatif, karena metode yang dieksekusi tidak mengembalikan nilai dan tidak dapat menggunakan transfer dengan referensi. Misalnya, jika ThreadMethod berada di kelas WilluseThread, maka ThreadMethod dapat mengomunikasikan informasi dengan memodifikasi properti instance kelas WillUseThread.

Domain aplikasi

Utas .NET berjalan dalam apa yang disebut domain aplikasi, yang didefinisikan dalam dokumentasi sebagai "kotak pasir tempat aplikasi berjalan." Domain aplikasi dapat dianggap sebagai versi ringan dari proses Win32; proses Win32 tunggal dapat berisi beberapa domain aplikasi. Perbedaan utama antara domain aplikasi dan proses adalah bahwa proses Win32 memiliki ruang alamatnya sendiri (dalam dokumentasi, domain aplikasi juga dibandingkan dengan proses logis yang berjalan di dalam proses fisik). Di NET, semua manajemen memori ditangani oleh runtime, sehingga beberapa domain aplikasi dapat berjalan dalam satu proses Win32. Salah satu manfaat skema ini adalah peningkatan kemampuan penskalaan aplikasi. Alat untuk bekerja dengan domain aplikasi ada di kelas AppDomain. Kami menyarankan Anda mempelajari dokumentasi untuk kelas ini. Dengan bantuannya, Anda bisa mendapatkan informasi tentang lingkungan di mana program Anda berjalan. Secara khusus, kelas AppDomain digunakan saat melakukan refleksi pada kelas sistem .NET. Program berikut mencantumkan rakitan yang dimuat.

Sistem Impor.Refleksi

Modul Modul

Sub utama ()

RedupkanDomain Sebagai AppDomain

theDomain = AppDomain.CurrentDomain

Rakitan Redup () As

Majelis = theDomain.GetAssemblies

RedupkanAssemblyxAs

Untuk Setiap Majelis Dalam Rakitan

Console.WriteLinetanAssembly.Nama Lengkap) Selanjutnya

Console.ReadLine ()

Akhir Sub

Modul Akhir

Membuat aliran

Mari kita mulai dengan contoh yang belum sempurna. Katakanlah Anda ingin menjalankan prosedur di utas terpisah yang mengurangi nilai penghitung dalam loop tak terbatas. Prosedur didefinisikan sebagai bagian dari kelas:

Kelas Publik Akan Menggunakan Benang

KurangiDariCounter Publik ()

Hitung redup Sebagai Integer

Do While True count - = 1

Console.WriteLlne ("Saya di utas dan penghitung lain ="

& menghitung)

Lingkaran

Akhir Sub

Kelas Akhir

Karena kondisi loop Do selalu benar, Anda mungkin berpikir bahwa tidak ada yang akan mengganggu prosedur SubtractFromCounter. Namun, dalam aplikasi multithread, hal ini tidak selalu terjadi.

Cuplikan berikut menunjukkan prosedur Sub Utama yang memulai utas dan perintah Impor:

Opsi Ketat Pada Sistem Impor. Modul Modul Threading

Sub utama ()

1 Redupkan myTest Sebagai WillUseThreads Baru ()

2 Redupkan bThreadStart Sebagai ThreadStart Baru (AddressOf _

myTest.SubtractFromCounter)

3 Redupkan bThread Sebagai Thread Baru (bThreadStart)

4 "bThread.Start ()

Redupkan saya Sebagai Integer

5 Lakukan Sementara Benar

Console.WriteLine ("Dalam utas dan hitungan utama adalah" & i) i + = 1

Lingkaran

Akhir Sub

Modul Akhir

Mari kita lihat poin terpenting secara berurutan. Pertama-tama, prosedur Sub Man n selalu berhasil di arus utama(utas utama). Dalam program .NET, selalu ada setidaknya dua utas yang berjalan: utas utama dan utas pengumpulan sampah. Baris 1 membuat instance baru dari kelas pengujian. Pada baris 2, kita membuat delegasi ThreadStart dan meneruskan alamat prosedur SubtractFromCounter ke instance kelas pengujian yang dibuat pada baris 1 (prosedur ini dipanggil tanpa parameter). BagusDengan mengimpor namespace Threading, nama panjang dapat dihilangkan. Objek thread baru dibuat pada baris 3. Perhatikan lewatnya delegasi ThreadStart saat memanggil konstruktor kelas Thread. Beberapa programmer lebih suka menggabungkan dua baris ini menjadi satu baris logis:

Redupkan bThread Sebagai Thread Baru (New ThreadStarttAddressOf _

myTest.SubtractFromCounter))

Terakhir, baris 4 "memulai" thread dengan memanggil metode Start dari instance Thread yang dibuat untuk delegasi ThreadStart. Dengan memanggil metode ini, kami memberi tahu sistem operasi bahwa prosedur Kurangi harus dijalankan di utas terpisah.

Kata "mulai" di paragraf sebelumnya diapit tanda kutip, karena ini adalah salah satu dari banyak keanehan pemrograman multithreaded: Memanggil Start sebenarnya tidak memulai thread! Itu hanya memberi tahu sistem operasi untuk menjadwalkan utas yang ditentukan untuk dijalankan, tetapi di luar kendali program untuk memulai secara langsung. Anda tidak akan dapat mulai menjalankan utas sendiri, karena sistem operasi selalu mengontrol eksekusi utas. Di bagian selanjutnya, Anda akan mempelajari cara menggunakan prioritas untuk membuat sistem operasi memulai utas Anda lebih cepat.

dalam gambar. 10.1 menunjukkan contoh apa yang dapat terjadi setelah memulai program dan kemudian menginterupsinya dengan tombol Ctrl + Break. Dalam kasus kami, utas baru dimulai hanya setelah penghitung di utas utama meningkat menjadi 341!

Beras. 10.1. Runtime perangkat lunak multi-utas sederhana

Jika program berjalan untuk jangka waktu yang lebih lama, hasilnya akan terlihat seperti yang ditunjukkan pada Gambar. 10.2. Kami melihat bahwa Andapenyelesaian utas yang sedang berjalan ditangguhkan dan kontrol ditransfer ke utas utama lagi. Dalam hal ini, ada manifestasi multithreading preemptive melalui time slicing. Arti dari istilah menakutkan ini dijelaskan di bawah ini.

Beras. 10.2. Beralih antar utas dalam program multi-utas sederhana

Saat menginterupsi utas dan mentransfer kontrol ke utas lain, sistem operasi menggunakan prinsip multithreading preemptive melalui pemotongan waktu. Kuantisasi waktu juga memecahkan salah satu masalah umum yang muncul sebelumnya dalam program multithread - satu utas menghabiskan semua waktu CPU dan tidak kalah dengan kontrol utas lainnya (sebagai aturan, ini terjadi dalam siklus intensif seperti di atas). Untuk mencegah pembajakan CPU eksklusif, utas Anda harus mentransfer kontrol ke utas lain dari waktu ke waktu. Jika program ternyata "tidak sadar", ada solusi lain yang sedikit kurang diinginkan: sistem operasi selalu mendahului utas yang sedang berjalan, terlepas dari tingkat prioritasnya, sehingga akses ke prosesor diberikan ke setiap utas dalam sistem.

Karena skema kuantisasi semua versi Windows yang menjalankan .NET memiliki irisan waktu minimum untuk setiap utas, dalam pemrograman .NET, masalah dengan perebutan eksklusif CPU tidak begitu serius. Di sisi lain, jika kerangka .NET pernah diadaptasi untuk sistem lain, ini mungkin berubah.

Jika kita menyertakan baris berikut dalam program kita sebelum memanggil Start, maka bahkan thread dengan prioritas terendah akan mendapatkan sebagian kecil dari waktu CPU:

bThread.Priority = ThreadPriority.Highest

Beras. 10.3. Utas dengan prioritas tertinggi biasanya dimulai lebih cepat

Beras. 10.4. Prosesor juga disediakan untuk utas dengan prioritas lebih rendah

Perintah memberikan prioritas maksimum ke utas baru dan mengurangi prioritas utas utama. Ara. 10.3 dapat dilihat bahwa utas baru mulai bekerja lebih cepat dari sebelumnya, tetapi, seperti Gambar. 10.4, utas utama juga menerima kontrolkemalasan (walaupun untuk waktu yang sangat singkat dan hanya setelah pekerjaan aliran yang berkepanjangan dengan pengurangan). Ketika Anda menjalankan program di komputer Anda, Anda akan mendapatkan hasil yang serupa dengan yang ditunjukkan pada Gambar. 10.3 dan 10.4, tetapi karena perbedaan antara sistem kami, tidak akan ada pencocokan persis.

Jenis enumerasi ThreadPrlority mencakup nilai untuk lima tingkat prioritas:

Prioritas Utas. Tertinggi

Prioritas Thread.AboveNormal

ThreadPrlority.Normal

Prioritas Utas.Di BawahNormal

Prioritas Utas. Terendah

Metode bergabung

Terkadang utas program perlu dijeda sampai utas lainnya selesai. Katakanlah Anda ingin menjeda utas 1 hingga utas 2 menyelesaikan perhitungannya. Untuk ini dari aliran 1 metode Gabung dipanggil untuk aliran 2. Dengan kata lain, perintah

utas2.Gabung ()

menangguhkan utas saat ini dan menunggu utas 2 selesai. Utas 1 pergi ke keadaan terkunci.

Jika Anda bergabung dengan streaming 1 ke streaming 2 menggunakan metode Gabung, sistem operasi akan secara otomatis memulai streaming 1 setelah streaming 2. Perhatikan bahwa proses startup adalah non-deterministik: tidak mungkin untuk mengatakan dengan tepat berapa lama setelah akhir dari utas 2, utas 1 akan mulai bekerja. Ada versi lain dari Gabung yang mengembalikan nilai boolean:

thread2.Gabung (Bilangan Bulat)

Metode ini menunggu utas 2 selesai, atau membuka blokir utas 1 setelah interval waktu yang ditentukan berlalu, menyebabkan penjadwal sistem operasi mengalokasikan waktu CPU ke utas lagi. Metode mengembalikan True jika utas 2 berakhir sebelum interval waktu habis yang ditentukan, dan False sebaliknya.

Ingat aturan dasarnya: apakah utas 2 telah selesai atau habis, Anda tidak memiliki kendali atas kapan utas 1 diaktifkan.

Nama utas, CurrentThread dan ThreadState

Properti Thread.CurrentThread mengembalikan referensi ke objek thread yang sedang dijalankan.

Meskipun ada jendela utas yang luar biasa untuk men-debug aplikasi multithreaded di VB .NET, yang dijelaskan di bawah ini, kami sangat sering dibantu oleh perintah

MsgBox (Thread.CurrentThread.Name)

Seringkali ternyata kode tersebut dieksekusi di utas yang sama sekali berbeda dari yang seharusnya dieksekusi.

Ingatlah bahwa istilah "penjadwalan aliran program non-deterministik" berarti hal yang sangat sederhana: pemrogram praktis tidak memiliki sarana untuk memengaruhi pekerjaan penjadwal. Untuk alasan ini, program sering menggunakan properti ThreadState, yang mengembalikan informasi tentang status thread saat ini.

Jendela aliran

Jendela Threads Visual Studio .NET sangat berharga dalam debugging program multithreaded. Ini diaktifkan oleh perintah submenu Debug> Windows dalam mode interupsi. Katakanlah Anda menetapkan nama ke utas bThread dengan perintah berikut:

bThread.Name = "Mengurangi utas"

Tampilan perkiraan jendela aliran setelah menginterupsi program dengan kombinasi tombol Ctrl + Break (atau dengan cara lain) ditunjukkan pada Gambar. 10.5.

Beras. 10.5. Jendela aliran

Panah di kolom pertama menandai utas aktif yang dikembalikan oleh properti Thread.CurrentThread. Kolom ID berisi ID utas numerik. Kolom berikutnya mencantumkan nama aliran (jika ditetapkan). Kolom Lokasi menunjukkan prosedur yang akan dijalankan (misalnya, prosedur WriteLine dari kelas Konsol pada Gambar 10.5). Kolom yang tersisa berisi informasi tentang prioritas dan utas yang ditangguhkan (lihat bagian berikutnya).

Jendela utas (bukan sistem operasi!) Memungkinkan Anda untuk mengontrol utas program Anda menggunakan menu konteks. Misalnya, Anda dapat menghentikan utas saat ini dengan mengklik kanan pada baris yang sesuai dan memilih perintah Bekukan (nanti, utas yang dihentikan dapat dilanjutkan). Menghentikan utas sering digunakan saat debugging untuk mencegah utas yang tidak berfungsi mengganggu aplikasi. Selain itu, jendela aliran memungkinkan Anda untuk mengaktifkan aliran lain (tidak dihentikan); untuk melakukan ini, klik kanan pada baris yang diperlukan dan pilih perintah Switch To Thread dari menu konteks (atau cukup klik dua kali pada baris thread). Seperti yang akan ditunjukkan di bawah, ini sangat berguna dalam mendiagnosis potensi kebuntuan.

Menangguhkan streaming

Aliran yang tidak digunakan untuk sementara dapat ditransfer ke status pasif menggunakan metode Slеer. Aliran pasif juga dianggap diblokir. Tentu saja, ketika sebuah utas diletakkan dalam keadaan pasif, utas lainnya akan memiliki lebih banyak sumber daya prosesor. Sintaks standar metode Slеer adalah sebagai berikut: Thread.Sleep (interval_in_milliseconds)

Sebagai hasil dari panggilan Tidur, utas aktif menjadi pasif setidaknya selama beberapa milidetik tertentu (namun, aktivasi segera setelah interval yang ditentukan kedaluwarsa tidak dijamin). Harap dicatat: saat memanggil metode, referensi ke utas tertentu tidak diteruskan - metode Tidur dipanggil hanya untuk utas aktif.

Versi lain dari Sleep membuat utas saat ini menyerahkan sisa waktu CPU yang dialokasikan:

Benang.Tidur (0)

Opsi berikutnya menempatkan utas saat ini dalam keadaan pasif untuk waktu yang tidak terbatas (aktivasi hanya terjadi ketika Anda memanggil Interrupt):

Thread.Slеer (Waktu Habis. Tak Terbatas)

Karena utas pasif (bahkan dengan batas waktu tidak terbatas) dapat diinterupsi oleh metode Interupsi, yang mengarah pada inisiasi pengecualian ThreadlnterruptExcepti, panggilan Slayer selalu diapit dalam blok Try-Catch, seperti dalam cuplikan berikut:

Mencoba

Benang.Tidur (200)

"Keadaan pasif utas telah terputus

Tangkap e Sebagai Pengecualian

"Pengecualian lainnya

Akhiri Coba

Setiap program .NET berjalan pada utas program, jadi metode Tidur juga digunakan untuk menangguhkan program (jika ruang nama Threadipg tidak diimpor oleh program, Anda harus menggunakan nama Threading.Thread. Tidur yang sepenuhnya memenuhi syarat).

Mengakhiri atau menginterupsi utas program

Sebuah utas akan secara otomatis berakhir ketika metode yang ditentukan ketika delegasi ThreadStart dibuat, tetapi kadang-kadang perlu untuk menghentikan metode (dan karenanya utas) ketika faktor-faktor tertentu terjadi. Dalam kasus seperti itu, aliran biasanya memeriksa variabel bersyarat, tergantung keadaannyakeputusan dibuat tentang pintu keluar darurat dari sungai. Biasanya, loop Do-While disertakan dalam prosedur untuk ini:

Sub ThreadedMetode ()

"Program harus menyediakan sarana untuk survei

"variabel bersyarat.

"Misalnya, variabel bersyarat dapat ditata sebagai properti

Do While conditionVariable = False And MoreWorkToDo

"Kode utama

Loop End Sub

Dibutuhkan beberapa waktu untuk polling variabel kondisional. Anda hanya boleh menggunakan polling persisten dalam kondisi loop jika Anda menunggu utas berakhir sebelum waktunya.

Jika variabel kondisi harus diperiksa di lokasi tertentu, gunakan perintah If-then dalam hubungannya dengan Exit Sub di dalam infinite loop.

Akses ke variabel bersyarat harus disinkronkan sehingga eksposur dari utas lain tidak mengganggu penggunaan normalnya. Topik penting ini dibahas di bagian "Pemecahan Masalah: Sinkronisasi".

Sayangnya, kode utas pasif (atau diblokir) tidak dieksekusi, jadi opsi dengan polling variabel bersyarat tidak cocok untuk mereka. Dalam hal ini, panggil metode Interrupt pada variabel objek yang berisi referensi ke utas yang diinginkan.

Metode Interupsi hanya dapat dipanggil pada utas dalam status Tunggu, Tidur, atau Gabung. Jika Anda memanggil Interrupt untuk utas yang berada di salah satu status yang terdaftar, maka setelah beberapa saat utas akan mulai bekerja kembali, dan lingkungan eksekusi akan memulai ThreadlnterruptedExcepti pada pengecualian di utas. Ini terjadi bahkan jika utas telah dibuat pasif tanpa batas waktu dengan memanggil Thread.Sleepdimeout. Tak terbatas). Kami mengatakan "setelah beberapa saat" karena penjadwalan utas non-deterministik. ThreadlnterruptedExcepti pada pengecualian ditangkap oleh bagian Catch, yang berisi kode keluar dari status menunggu. Namun, bagian Tangkap tidak harus mengakhiri utas pada panggilan Interupsi - utas menangani pengecualian sesuai keinginan.

Dalam .NET, metode Interupsi dapat dipanggil bahkan untuk utas yang tidak diblokir. Dalam hal ini, utas terputus pada pemblokiran terdekat.

Menangguhkan dan mematikan utas

Namespace Threading berisi metode lain yang mengganggu threading normal:

  • Menskors;
  • Menggugurkan.

Sulit untuk mengatakan mengapa .NET menyertakan dukungan untuk metode ini - memanggil Suspend dan Abort kemungkinan akan menyebabkan program menjadi tidak stabil. Tak satu pun dari metode memungkinkan deinisialisasi normal aliran. Selain itu, saat memanggil Suspend atau Abort, tidak mungkin untuk memprediksi status apa yang akan ditinggalkan oleh objek setelah ditangguhkan atau dibatalkan.

Memanggil Abort melempar ThreadAbortException. Untuk membantu Anda memahami mengapa pengecualian aneh ini tidak boleh ditangani dalam program, berikut adalah kutipan dari dokumentasi .NET SDK:

“... Ketika sebuah utas dihancurkan dengan memanggil Abort, runtime melempar ThreadAbortException. Ini adalah jenis pengecualian khusus yang tidak dapat ditangkap oleh program. Ketika pengecualian ini dilemparkan, runtime menjalankan semua blok Akhirnya sebelum mengakhiri utas. Karena tindakan apa pun dapat terjadi di blok Akhirnya, panggil Gabung untuk memastikan alirannya dihancurkan.

Moral: Abort dan Suspend tidak disarankan (dan jika Anda masih tidak dapat melakukannya tanpa Suspend, lanjutkan thread yang ditangguhkan menggunakan metode Resume). Anda dapat dengan aman mengakhiri utas hanya dengan mengumpulkan variabel kondisi yang disinkronkan atau dengan memanggil metode Interupsi yang dibahas di atas.

Utas latar belakang (daemon)

Beberapa utas yang berjalan di latar belakang secara otomatis berhenti berjalan ketika komponen program lain berhenti. Secara khusus, pengumpul sampah berjalan di salah satu utas latar belakang. Utas latar belakang biasanya dibuat untuk menerima data, tetapi ini dilakukan hanya jika utas lain menjalankan kode yang dapat memproses data yang diterima. Sintaks: nama aliran. IsBackGround = True

Jika hanya ada utas latar belakang yang tersisa di aplikasi, aplikasi akan berhenti secara otomatis.

Contoh yang lebih serius: mengekstrak data dari kode HTML

Kami merekomendasikan penggunaan aliran hanya ketika fungsionalitas program secara jelas dibagi menjadi beberapa operasi. Contoh yang baik adalah program ekstraksi HTML di Bab 9. Kelas kita melakukan dua hal: mengambil data dari Amazon dan memprosesnya. Ini adalah contoh sempurna dari situasi di mana pemrograman multithreaded benar-benar tepat. Kami membuat kelas untuk beberapa buku yang berbeda dan kemudian mengurai data dalam aliran yang berbeda. Membuat utas baru untuk setiap buku meningkatkan efisiensi program, karena ketika satu utas menerima data (yang mungkin memerlukan menunggu di server Amazon), utas lain akan sibuk memproses data yang telah diterima.

Versi multi-threaded dari program ini bekerja lebih efisien daripada versi single-threaded hanya pada komputer dengan beberapa prosesor atau jika penerimaan data tambahan dapat digabungkan secara efektif dengan analisisnya.

Seperti disebutkan di atas, hanya prosedur yang tidak memiliki parameter yang dapat dijalankan dalam utas, jadi Anda harus membuat sedikit perubahan pada program. Di bawah ini adalah prosedur dasar, ditulis ulang untuk mengecualikan parameter:

Sub FindRank Publik ()

m_Rank = MengikisAmazon ()

Console.WriteLine ("peringkat" & m_Name & "Is" & GetRank)

Akhir Sub

Karena kita tidak akan dapat menggunakan bidang gabungan untuk menyimpan dan mengambil informasi (menulis program multi-utas dengan antarmuka grafis dibahas di bagian terakhir bab ini), program menyimpan data empat buku dalam larik, definisi yang dimulai seperti ini:

Dim theBook (3.1) As String theBook (0.0) = "1893115992"

theBook (0.l) = "Pemrograman VB .NET" "Dll.

Empat aliran dibuat dalam siklus yang sama di mana objek AmazonRanker dibuat:

Untuk i = 0 Sampai 3

Mencoba

theRanker = AmazonRanker Baru (Buku (i.0).Buku.1))

aThreadStart = ThreadStar Baru (AlamatRanker.FindRan ()

aThread = Utas Baru (aThreadStart)

aThread.Name = Buku (i.l)

aThread.Start () Tangkap e Sebagai Pengecualian

Console.WriteLine (e.Pesan)

Akhiri Coba

Lanjut

Di bawah ini adalah teks lengkap dari program:

Opsi Ketat Pada Impor System.IO Imports System.Net

Sistem Impor.Threading

Modul Modul

Sub utama ()

Redupkan Buku (3.1) Sebagai String

theBook (0.0) = "1893115992"

theBook (0.l) = "Pemrograman VB .NET"

Buku (l.0) = "1893115291"

theBook (l.l) = "Pemrograman Basis Data VB .NET"

Buku (2,0) = "1893115623"

theBook (2.1) = "Programmer" s Pengantar C #. "

theBook (3.0) = "1893115593"

theBook (3.1) = "Gland the .Net Platform"

Redupkan saya Sebagai Integer

Redupkan Ranker As = AmazonRanker

Redupkan ThreadMulai Sebagai Threading.ThreadStart

Redupkan Thread Sebagai Threading.Thread

Untuk i = 0 Sampai 3

Mencoba

theRanker = AmazonRankerttheBook Baru (i.0). Buku (i.1))

aThreadStart = ThreadStart Baru (AddressOf theRanker. FindRank)

aThread = Utas Baru (aThreadStart)

aThread.Name = Buku (i.l)

aThread.Mulai ()

Tangkap e Sebagai Pengecualian

Console.WriteLlnete.Pesan)

Akhiri Coba Berikutnya

Console.ReadLine ()

Akhir Sub

Modul Akhir

AmazonRanker Kelas Publik

m_URL Pribadi Sebagai String

m_Rank Pribadi Sebagai Integer

m_Name Pribadi Sebagai String

Sub Publik Baru (ByVal ISBN As String. ByVal theName As String)

m_URL = "http://www.amazon.com/exec/obidos/ASIN/" & ISBN

m_Name = Nama Akhir Sub

Sub FindRank Publik () m_Rank = ScrapeAmazon ()

Console.Writeline ("peringkat" & m_Name & "adalah"

& GetRank) Akhir Sub

GetRank Properti Hanya-Baca Publik () Sebagai String Get

Jika m_Rank<>0 Lalu

Kembalikan CStr (m_Rank) Lain

"Masalah

Berakhir jika

Dapatkan Akhir

Akhiri Properti

Properti Hanya Baca Publik GetName () Sebagai String Get

Kembalikan m_Name

Dapatkan Akhir

Akhiri Properti

Fungsi Pribadi ScrapeAmazon () Sebagai Integer Coba

Redupkan URL Sebagai Uri Baru (m_URL)

Redupkan Permintaan Sebagai Permintaan Web

theRequest = WebRequest.Create (URL)

Redupkan Respon Sebagai WebResponse

theResponse = theRequest.GetResponse

Redupkan aReader Sebagai StreamReader Baru (theResponse.GetResponseStream ())

RedupkanData Sebagai String

theData = aReader.ReadToEnd

Analisis Kembali (Data)

Tangkap E Sebagai Pengecualian

Console.WriteLine (E.Pesan)

Console.WriteLine (E.StackTrace)

Menghibur. Baris Baca ()

Akhiri Coba Akhiri Fungsi

Analisis Fungsi Pribadi (ByVal theData As String) Sebagai Integer

Lokasi Dim As.Integer Location = theData.IndexOf (" Amazon.com

Peringkat Penjualan:") _

+ "Peringkat Penjualan Amazon.com:".Panjang

Redupkan suhu As String

Lakukan Hingga theData.Substring (Location.l) = "<" temp = temp

& theData.Substring (Location.l) Lokasi + = 1 Loop

Kembali Clnt (temp)

Fungsi Akhir

Kelas Akhir

Operasi multithreaded biasanya digunakan di ruang nama .NET dan I / O, sehingga perpustakaan .NET Framework menyediakan metode asinkron khusus untuk mereka. Untuk informasi selengkapnya tentang menggunakan metode asinkron saat menulis program multithread, lihat metode BeginGetResponse dan EndGetResponse dari kelas HTTPWebRequest.

Bahaya utama (data umum)

Sejauh ini, satu-satunya kasus penggunaan yang aman untuk utas telah dipertimbangkan - aliran kami tidak mengubah data umum. Jika Anda mengizinkan perubahan dalam data umum, potensi kesalahan mulai berlipat ganda secara eksponensial dan menjadi jauh lebih sulit untuk menghilangkannya untuk program. Di sisi lain, jika Anda melarang modifikasi data bersama oleh utas yang berbeda, pemrograman .NET multithreading hampir tidak akan berbeda dari kemampuan terbatas VB6.

Kami ingin menarik perhatian Anda ke program kecil yang menunjukkan masalah yang muncul tanpa masuk ke detail yang tidak perlu. Program ini mensimulasikan rumah dengan termostat di setiap ruangan. Jika suhu 5 derajat Fahrenheit atau lebih (sekitar 2,77 derajat Celcius) kurang dari suhu target, kami memerintahkan sistem pemanas untuk meningkatkan suhu sebesar 5 derajat; jika tidak, suhu naik hanya 1 derajat. Jika suhu saat ini lebih besar dari atau sama dengan yang ditetapkan, tidak ada perubahan yang dibuat. Kontrol suhu di setiap ruangan dilakukan dengan aliran terpisah dengan penundaan 200 milidetik. Pekerjaan utama dilakukan dengan cuplikan berikut:

Jika mHouse.HouseTemp< mHouse.MAX_TEMP = 5 Then Try

Benang.Tidur (200)

Tangkap dasi Sebagai ThreadlnterruptedException

"Menunggu pasif telah terputus

Tangkap e Sebagai Pengecualian

"Pengecualian Percobaan Akhir Lainnya

mHouse.HouseTemp + - 5" Dll.

Di bawah ini adalah kode sumber lengkap program. Hasilnya ditunjukkan pada Gambar. 10.6: Suhu di rumah telah mencapai 105 derajat Fahrenheit (40,5 derajat Celcius)!

1 Opsi Ketat Aktif

2 Sistem Impor.Threading

3 Modul Modul

4 Sub Utama ()

5 Redupkan Rumahku Sebagai Rumah Baru (l0)

6 Konsol. Baris Baca ()

7 Akhir Sub

8 Akhir Modul

9 Rumah Kelas Umum

10 Public Const MAX_TEMP Sebagai Integer = 75

11 mCurTemp Pribadi Sebagai Integer = 55

12 Kamar Pribadi () Sebagai Kamar

13 Sub Publik Baru (ByVal numOfRooms As Integer)

14 RedDim mRooms (numOfRooms = 1)

15 Dim i As Integer

16 Redupkan aThreadMulai Sebagai Threading.ThreadStart

17 Redupkan Benang Sebagai Benang

18 Untuk i = 0 Ke numOfRooms -1

19 Coba

20 mRooms (i) = NewRoom (Saya, mCurTemp, CStr (i) & "throom")

21 aThreadStart - ThreadStart Baru (AddressOf _

mRooms (i) .CheckTempInRoom)

22 aThread = Utas Baru (aThreadStart)

23 aThread.Start ()

24 Tangkap E Sebagai Pengecualian

25 Console.WriteLine (E.StackTrace)

26 Akhir Coba

27 Selanjutnya

28 Akhir Sub

29 Properti Publik HouseTemp () Sebagai Integer

tiga puluh. Mendapatkan

31 Kembalikan mCurTemp

32 Akhir Dapatkan

33 Set (Nilai ByVal Sebagai Integer)

34 mCurTemp = Nilai 35 Set Akhir

36 Akhir Properti

37 Akhir Kelas

Ruang Kelas Umum 38

39 mCurTemp Pribadi Sebagai Integer

40 Nama Pribadi Sebagai String

41 mRumah Pribadi Sebagai Rumah

42 Public Sub Baru (ByVal TheHouse As House,

ByVal temp As Integer, ByVal roomName As String)

43 mRumah = Rumah

44 mCurTemp = suhu

45 mNama = Nama Kamar

46 Akhir Sub

47 Sub Publik CheckTempInRoom ()

48 UbahSuhu ()

49 Akhir Sub

50 Sub Perubahan Pribadi Suhu ()

51 Coba

52 Jika mHouse.HouseTemp< mHouse.MAX_TEMP - 5 Then

53 Benang.Tidur (200)

54 mRumah.RumahSuhu + - 5

55 Console.WriteLine ("Am in" & Me.mName & _

56 ".Suhu saat ini" & mHouse.HouseTemp)

57. Elself mHouse.HouseTemp< mHouse.MAX_TEMP Then

58 Benang.Tidur (200)

59 mHouse.HouseTemp + = 1

60 Console.WriteLine ("Am in" & Me.mName & _

61 ".Suhu saat ini" & mHouse.HouseTemp)

62 Lainnya

63 Console.WriteLine ("Am in" & Me.mName & _

64 ".Suhu saat ini" & mHouse.HouseTemp)

65 "Tidak melakukan apa-apa, suhu normal

66 Akhir Jika

67 Catch tae As ThreadlnterruptedException

68 "Penantian pasif telah terputus

69 Tangkap e Sebagai Pengecualian

70 "Pengecualian lainnya

71 Akhir Coba

72 Akhir Sub

73 Kelas Akhir

Beras. 10.6. Masalah multithreading

Prosedur Sub Utama (baris 4-7) membuat "rumah" dengan sepuluh "kamar". Kelas House menetapkan suhu maksimum 75 derajat Fahrenheit (sekitar 24 derajat Celcius). Baris 13-28 mendefinisikan konstruktor rumah yang agak rumit. Kunci untuk memahami program ini adalah baris 18-27. Baris 20 membuat objek ruangan lain, dan referensi ke objek rumah diteruskan ke konstruktor sehingga objek ruangan dapat merujuknya jika perlu. Baris 21-23 memulai sepuluh aliran untuk menyesuaikan suhu di setiap ruangan. Kelas Kamar didefinisikan pada baris 38-73. Referensi coxpa rumahdisimpan dalam variabel mHouse dalam konstruktor kelas Room (baris 43). Kode untuk memeriksa dan menyesuaikan suhu (baris 50-66) terlihat sederhana dan alami, tetapi seperti yang akan segera Anda lihat, kesan ini menipu! Perhatikan bahwa kode ini dibungkus dalam blok Try-Catch karena program menggunakan metode Sleep.

Hampir tidak ada orang yang setuju untuk hidup dalam suhu 105 derajat Fahrenheit (40,5 hingga 24 derajat Celcius). Apa yang terjadi? Masalahnya terkait dengan baris berikut:

Jika mHouse.HouseTemp< mHouse.MAX_TEMP - 5 Then

Dan hal berikut terjadi: pertama, suhu diperiksa oleh aliran 1. Dia melihat bahwa suhu terlalu rendah, dan menaikkannya sebesar 5 derajat. Sayangnya, sebelum suhu naik, aliran 1 terganggu dan kontrol ditransfer ke aliran 2. Aliran 2 memeriksa variabel yang sama yang belum berubah aliran 1. Dengan demikian, aliran 2 juga bersiap untuk menaikkan suhu sebesar 5 derajat, tetapi tidak punya waktu untuk melakukan ini dan juga masuk ke keadaan menunggu. Proses berlanjut hingga aliran 1 diaktifkan dan melanjutkan ke perintah berikutnya - meningkatkan suhu sebesar 5 derajat. Peningkatan diulang ketika semua 10 aliran diaktifkan, dan penghuni rumah akan mengalami waktu yang buruk.

Solusi untuk masalah: sinkronisasi

Pada program sebelumnya, situasi muncul ketika output program tergantung pada urutan eksekusi utas. Untuk menghilangkannya, Anda perlu memastikan bahwa perintah seperti

Jika mHouse.HouseTemp< mHouse.MAX_TEMP - 5 Then...

sepenuhnya diproses oleh utas aktif sebelum terputus. Properti ini disebut malu atom - blok kode harus dieksekusi oleh setiap utas tanpa gangguan, sebagai unit atom. Sekelompok perintah, digabungkan menjadi blok atom, tidak dapat diinterupsi oleh penjadwal utas sampai selesai. Setiap bahasa pemrograman multithreaded memiliki caranya sendiri untuk memastikan atomisitas. Di VB .NET, cara termudah untuk menggunakan perintah SyncLock adalah dengan memasukkan variabel objek saat dipanggil. Buat perubahan kecil pada prosedur ChangeTemperature dari contoh sebelumnya, dan program akan bekerja dengan baik:

Sub Perubahan Pribadi Suhu () SyncLock (mHouse)

Mencoba

Jika mHouse.HouseTemp< mHouse.MAXJTEMP -5 Then

Benang.Tidur (200)

mHouse.HouseTemp + = 5

Console.WriteLine ("Am in" & Me.mName & _

".Suhu saat ini" & mHouse.HouseTemp)

diri sendiri

mHouse.HouseTemp< mHouse. MAX_TEMP Then

Thread.Sleep (200) mHouse.HouseTemp + = 1

Console.WriteLine ("Am in" & Me.mName & _ ".Suhu saat ini" & mHouse.HomeTemp) Lain

Console.WriteLineC "Am in" & Me.mName & _ ".Suhu saat ini" & mHouse.HouseTemp)

"Jangan lakukan apa-apa, suhunya normal

Akhiri Jika Tangkap dasi Sebagai ThreadlnterruptedException

"Penantian pasif terganggu oleh Catch e As Exception

"Pengecualian lainnya

Akhiri Coba

Akhiri Kunci Sinkronisasi

Akhir Sub

Kode blok SyncLock dijalankan secara atom. Akses ke sana dari semua utas lainnya akan ditutup hingga utas pertama melepaskan kunci dengan perintah Akhiri SyncLock. Jika utas di blok yang disinkronkan masuk ke status tunggu pasif, kunci tetap ada sampai utas terputus atau dilanjutkan.

Penggunaan perintah SyncLock yang benar membuat utas program Anda tetap aman. Sayangnya, penggunaan SyncLock yang berlebihan berdampak negatif pada kinerja. Menyinkronkan kode dalam program multithread mengurangi kecepatan kerjanya beberapa kali. Sinkronkan hanya kode yang paling dibutuhkan dan lepaskan kunci sesegera mungkin.

Kelas koleksi dasar tidak aman dalam aplikasi multi-utas, tetapi .NET Framework menyertakan versi aman-utas dari sebagian besar kelas koleksi. Di kelas ini, kode metode yang berpotensi berbahaya disertakan dalam blok SyncLock. Versi kelas koleksi yang aman untuk thread harus digunakan dalam program multithread di mana pun integritas data dikompromikan.

Masih disebutkan bahwa variabel kondisional mudah diimplementasikan menggunakan perintah SyncLock. Untuk melakukan ini, Anda hanya perlu menyinkronkan penulisan ke properti boolean umum, yang tersedia untuk membaca dan menulis, seperti yang dilakukan dalam fragmen berikut:

Variabel Kondisi Kelas Publik

Loker Bersama Pribadi Sebagai Objek = Objek Baru ()

Pribadi Dibagikan mOK Sebagai Boolean Dibagikan

Properti TheConditionVariable () Sebagai Boolean

Mendapatkan

Kembalikan mOK

Dapatkan Akhir

Setel (Nilai ByVal Sebagai Boolean) SyncLock (pengunci)

mOK = Nilai

Akhiri Kunci Sinkronisasi

Set Akhir

Akhiri Properti

Kelas Akhir

Kelas Perintah dan Monitor SyncLock

Penggunaan perintah SyncLock melibatkan beberapa kehalusan yang tidak ditunjukkan dalam contoh sederhana di atas. Jadi, pilihan objek sinkronisasi memainkan peran yang sangat penting. Coba jalankan program sebelumnya dengan perintah SyncLock (Me) alih-alih SyncLock (mHouse). Suhu naik di atas ambang batas lagi!

Ingat bahwa perintah SyncLock menyinkronkan menggunakan obyek, diteruskan sebagai parameter, bukan oleh cuplikan kode. Parameter SyncLock bertindak sebagai pintu untuk mengakses fragmen yang disinkronkan dari utas lainnya. Perintah SyncLock (Me) sebenarnya membuka beberapa "pintu" yang berbeda, yang persis seperti yang Anda coba hindari dengan sinkronisasi. Moralitas:

Untuk melindungi data bersama dalam aplikasi multithread, perintah SyncLock harus menyinkronkan satu objek dalam satu waktu.

Karena sinkronisasi dikaitkan dengan objek tertentu, dalam beberapa situasi, dimungkinkan untuk secara tidak sengaja mengunci fragmen lain. Katakanlah Anda memiliki dua metode yang disinkronkan, pertama dan kedua, dan kedua metode tersebut disinkronkan pada objek bigLock. Ketika utas 1 memasuki metode terlebih dahulu dan menangkap bigLock, tidak ada utas yang dapat memasuki metode kedua karena aksesnya sudah dibatasi untuk utas 1!

Fungsionalitas perintah SyncLock dapat dianggap sebagai bagian dari fungsionalitas kelas Monitor. Kelas Monitor sangat dapat disesuaikan dan dapat digunakan untuk menyelesaikan tugas sinkronisasi non-sepele. Perintah SyncLock adalah analog perkiraan metode Enter dan Exit dari kelas Monitor:

Mencoba

Monitor.Enter (theObject) Akhirnya

Monitor.Keluar (Object)

Akhiri Coba

Untuk beberapa operasi standar (menambah / mengurangi variabel, menukar konten dua variabel), .NET Framework menyediakan kelas Interlocked, yang metodenya melakukan operasi ini pada tingkat atom. Menggunakan kelas Interlocked, operasi ini jauh lebih cepat daripada menggunakan perintah SyncLock.

Saling mengunci

Selama sinkronisasi, kunci diatur pada objek, bukan utas, jadi saat menggunakan berbeda objek untuk diblokir berbeda potongan kode dalam program terkadang terjadi kesalahan yang tidak sepele. Sayangnya, dalam banyak kasus, sinkronisasi pada satu objek tidak dapat diterima, karena akan menyebabkan terlalu seringnya pemblokiran utas.

Pertimbangkan situasinya saling mengunci(kebuntuan) dalam bentuknya yang paling sederhana. Bayangkan dua programmer di meja makan. Sayangnya, mereka hanya memiliki satu pisau dan satu garpu untuk dua orang. Dengan asumsi Anda membutuhkan pisau dan garpu untuk makan, dua situasi mungkin terjadi:

  • Seorang programmer berhasil mengambil pisau dan garpu dan mulai makan. Ketika dia kenyang, dia menyisihkan makan malam, dan kemudian programmer lain dapat mengambilnya.
  • Satu programmer mengambil pisau dan yang lainnya mengambil garpu. Tidak ada yang bisa mulai makan kecuali yang lain melepaskan peralatannya.

Dalam program multithreaded, situasi ini disebut saling menghalangi. Kedua metode disinkronkan pada objek yang berbeda. Thread A menangkap objek 1 dan memasuki bagian program yang dilindungi oleh objek ini. Sayangnya, agar berfungsi, perlu akses ke kode yang dilindungi oleh Kunci Sinkronisasi lain dengan objek sinkronisasi yang berbeda. Tetapi sebelum ia sempat memasukkan fragmen yang disinkronkan oleh objek lain, aliran B memasukinya dan menangkap objek ini. Sekarang utas A tidak dapat memasuki fragmen kedua, utas B tidak dapat memasuki fragmen pertama, dan kedua utas ditakdirkan untuk menunggu tanpa batas. Tidak ada utas yang dapat terus berjalan karena objek yang diperlukan tidak akan pernah dibebaskan.

Diagnosis kebuntuan diperumit oleh fakta bahwa mereka dapat terjadi dalam kasus yang relatif jarang. Itu semua tergantung pada urutan penjadwal mengalokasikan waktu CPU untuk mereka. Ada kemungkinan bahwa dalam kebanyakan kasus, objek sinkronisasi akan ditangkap dalam urutan non-deadlock.

Berikut ini adalah implementasi dari situasi kebuntuan yang baru saja dijelaskan. Setelah diskusi singkat tentang poin paling mendasar, kami akan menunjukkan cara mengidentifikasi situasi kebuntuan di jendela utas:

1 Opsi Ketat Aktif

2 Sistem Impor.Threading

3 Modul Modul

4 Sub Utama ()

5 Dim Tom Sebagai Programmer Baru ("Tom")

6 Dim Bob Sebagai Programmer Baru ("Bob")

7 Dim aThreadMulai Sebagai ThreadStart Baru (Alamat Tom.Eat)

8 Redupkan aThread Sebagai Utas Baru (aThreadStart)

9 aThread.Name = "Tom"

10 Dim bThreadStart As New ThreadStarttAddressOf Bob.Eat)

11 Redupkan bThread Sebagai Thread Baru (bThreadStart)

12 bThread.Name = "Bob"

13 aThread.Start ()

14 bThread.Start ()

15 Akhir Sub

16 Akhir Modul

17 Garpu Kelas Umum

18 mForkAvaiTable Bersama Pribadi Sebagai Boolean = True

19 Pemilik Bersama Pribadi Sebagai String = "Tidak Ada"

20 Private Readonly Property OwnsUtensil () Sebagai String

21 Dapatkan

22 Kembalikan Pemilik

23 Akhir Dapatkan

24 Akhir Properti

25 Sub Publik GrabForktByVal sebagai Programmer)

26 Console.Writel_ine (Thread.CurrentThread.Name & _

"mencoba meraih garpu.")

27 Console.WriteLine (Me.OwnsUtensil & "memiliki garpu."). ...

28 Monitor.Masukkan (Saya) "SyncLock (aFork)"

29 Jika mFork Tersedia Maka

30 a.HasFork = Benar

31 mPemilik = a.NamaSaya

32 mForkTersedia = Palsu

33 Console.WriteLine (a.MyName & "baru saja mendapatkan fork.waiting")

34 Coba

Thread.Sleep (100) Tangkap e Sebagai Pengecualian Console.WriteLine (e.StackTrace)

Akhiri Coba

35 Akhir Jika

36 Monitor.Keluar (Saya)

Akhiri Kunci Sinkronisasi

37 Akhir Sub

38 Kelas Akhir

39 Pisau Kelas Umum

40 Private Shared mKnifeAvailable As Boolean = True

41 Pemilik Bersama Pribadi Sebagai String = "Tidak Ada"

42 Private Readonly Property OwnsUtensi1 () Sebagai String

43 Dapatkan

44 Kembalikan Pemilik

45 Akhir Dapatkan

46 Akhir Properti

47 Public Sub GrabKnifetByVal sebagai Programmer)

48 Console.WriteLine (Thread.CurrentThread.Name & _

"mencoba mengambil pisau.")

49 Console.WriteLine (Me.OwnsUtensil & "memiliki pisau.")

50 Monitor.Masukkan (Saya) "SyncLock (aKnife)"

51 Jika mKnifeTersedia Maka

52 mKnifeAvailable = Salah

53 a.HasKnife = Benar

54 mPemilik = a.NamaSaya

55 Console.WriteLine (a.MyName & "baru saja mendapatkan pisau.menunggu")

56 Coba

Benang.Tidur (100)

Tangkap e Sebagai Pengecualian

Console.WriteLine (e.StackTrace)

Akhiri Coba

57 Akhiri Jika

58 Monitor.Keluar (Saya)

59 Akhir Sub

60 Kelas Akhir

61 Programmer Kelas Umum

62 Private mName Sebagai String

63 Private Shared mFork Sebagai Fork

64 mKnife Bersama Pribadi Sebagai Pisau

65 Private mHasKnife Sebagai Boolean

66 mHasFork Pribadi Sebagai Boolean

67 Dibagikan Sub Baru ()

68 mFork = Garpu Baru ()

69 mKnife = Pisau Baru ()

70 Akhir Sub

71 Sub Publik Baru (ByVal theName As String)

72 mNama = Nama

73 Akhir Sub

74 Properti Hanya-Baca Publik MyName() As String

75 Dapatkan

76 Kembalikan mName

77 Dapatkan Akhir

78 Akhir Properti

79 Properti Publik HasKnife () Sebagai Boolean

80 Dapatkan

81 Kembalikan mHasKnife

82 Akhir Dapatkan

83 Set (Nilai ByVal Sebagai Boolean)

84 mHasKnife = Nilai

85 Set Akhir

86 Akhir Properti

87 Properti Publik HasFork () Sebagai Boolean

88 Dapatkan

89 Kembalikan mHasFork

90 Akhir Dapatkan

91 Set (Nilai ByVal Sebagai Boolean)

92 mHasFork = Nilai

93 Set Akhir

94 Akhir Properti

95 Sub Makan Umum ()

96 Lakukan Sampai Aku.HasKnife Dan Aku.HasFork

97 Console.Writeline (Thread.CurrentThread.Name & "ada di utas.")

98 Jika Rnd ()< 0.5 Then

99 mFork.GrabFork (Saya)

100 Lainnya

101 mKnife.GrabKnife (Saya)

102 Akhir Jika

103 Putaran

104 MsgBox (Me.MyName & "bisa makan!")

105 mKnife = Pisau Baru ()

106 mFork = Garpu Baru ()

107 Akhir Sub

108 Kelas Akhir

Prosedur utama Main (baris 4-16) membuat dua instance kelas Programmer dan kemudian memulai dua thread untuk mengeksekusi metode Eat kritis dari kelas Programmer (baris 95-108), yang dijelaskan di bawah ini. Prosedur Utama mengatur nama-nama utas dan mengaturnya; mungkin semua yang terjadi dapat dimengerti dan tanpa komentar.

Kode untuk kelas Fork terlihat lebih menarik (baris 17-38) (kelas Knife serupa didefinisikan pada baris 39-60). Baris 18 dan 19 menentukan nilai bidang umum, yang dengannya Anda dapat mengetahui apakah steker saat ini tersedia, dan jika tidak, siapa yang menggunakannya. Properti ReadOnly OwnUtensi1 (baris 20-24) dimaksudkan untuk transfer informasi yang paling sederhana. Inti dari kelas Fork adalah metode “ambil garpu” GrabFork, yang didefinisikan pada baris 25-27.

  1. Baris 26 dan 27 hanya mencetak informasi debug ke konsol. Dalam kode utama metode (baris 28-36), akses ke garpu disinkronkan oleh objekikat pinggang Aku. Karena program kami hanya menggunakan satu garpu, sinkronisasi saya memastikan bahwa tidak ada dua utas yang dapat mengambilnya secara bersamaan. Perintah Slee "p (di blok mulai dari baris 34) mensimulasikan penundaan antara mengambil garpu / pisau dan mulai makan. Perhatikan bahwa perintah Tidur tidak membuka kunci objek dan hanya mempercepat kebuntuan!
    Namun, yang paling menarik adalah kode kelas Programmer (baris 61-108). Baris 67-70 mendefinisikan konstruktor generik untuk memastikan bahwa hanya ada satu garpu dan pisau dalam program. Kode properti (baris 74-94) sederhana dan tidak memerlukan komentar. Hal terpenting terjadi dalam metode Eat, yang dijalankan oleh dua utas terpisah. Proses berlanjut dalam satu lingkaran sampai beberapa aliran menangkap garpu bersama dengan pisau. Pada baris 98-102, objek secara acak mengambil garpu/pisau menggunakan panggilan Rnd, yang menyebabkan kebuntuan. Berikut ini terjadi:
    Utas yang mengeksekusi metode Eat dari objek Thoth dipanggil dan memasuki loop. Dia mengambil pisau dan pergi ke keadaan menunggu.
  2. Utas yang mengeksekusi metode Bob's Eat dipanggil dan memasuki loop. Itu tidak bisa mengambil pisau, tetapi mengambil garpu dan masuk ke keadaan menunggu.
  3. Utas yang mengeksekusi metode Eat dari objek Thoth dipanggil dan memasuki loop. Dia mencoba meraih garpu, tapi Bob sudah meraih garpu; utas masuk ke status menunggu.
  4. Utas yang mengeksekusi metode Bob's Eat dipanggil dan memasuki loop. Dia mencoba untuk mengambil pisau, tetapi pisau itu sudah ditangkap oleh objek Thoth; utas masuk ke status menunggu.

Semua ini berlanjut tanpa batas - kita dihadapkan pada situasi kebuntuan yang khas (coba jalankan program dan Anda akan melihat bahwa tidak ada yang bisa makan dengan cara ini).
Anda juga dapat memeriksa apakah kebuntuan telah terjadi di jendela utas. Jalankan program dan interupsi dengan tombol Ctrl + Break. Sertakan variabel Me di viewport dan buka jendela stream. Hasilnya terlihat seperti yang ditunjukkan pada Gambar. 10.7. Dari gambar, Anda dapat melihat bahwa benang Bob telah mengambil pisau, tetapi tidak memiliki garpu. Klik kanan di jendela Threads pada baris Tot dan pilih perintah Switch to Thread dari menu konteks. Area pandang menunjukkan bahwa aliran Thoth memiliki garpu, tetapi tidak memiliki pisau. Tentu saja, ini bukan bukti seratus persen, tetapi perilaku seperti itu setidaknya membuat Anda curiga bahwa ada sesuatu yang salah.
Jika opsi dengan sinkronisasi oleh satu objek (seperti dalam program dengan meningkatkan -suhu di rumah) tidak memungkinkan, untuk mencegah penguncian bersama, Anda dapat memberi nomor pada objek sinkronisasi dan selalu menangkapnya dalam urutan yang konstan. Mari kita lanjutkan analogi programmer makan: jika utas selalu mengambil pisau terlebih dahulu dan kemudian garpu, tidak akan ada masalah dengan kebuntuan. Aliran pertama yang mengambil pisau akan bisa makan dengan normal. Diterjemahkan ke dalam bahasa aliran program, ini berarti bahwa penangkapan objek 2 hanya dimungkinkan jika objek 1 ditangkap terlebih dahulu.

Beras. 10.7. Analisis kebuntuan di jendela utas

Oleh karena itu, jika kita menghapus panggilan ke Rnd pada baris 98 dan menggantinya dengan cuplikan

mFork.GrabFork (Saya)

mKnife.GrabKnife (Saya)

kebuntuan menghilang!

Berkolaborasi pada data saat dibuat

Dalam aplikasi multithread, sering terjadi situasi di mana utas tidak hanya bekerja dengan data bersama, tetapi juga menunggunya muncul (yaitu, utas 1 harus membuat data sebelum utas 2 dapat menggunakannya). Karena data dibagikan, akses ke data tersebut perlu disinkronkan. Penting juga untuk menyediakan sarana untuk memberi tahu utas yang menunggu tentang munculnya data yang siap.

Situasi ini biasanya disebut masalah pemasok/konsumen. Utas mencoba mengakses data yang belum ada, sehingga harus mentransfer kontrol ke utas lain yang membuat data yang diperlukan. Masalahnya diselesaikan dengan kode berikut:

  • Thread 1 (konsumen) bangun, memasukkan metode yang disinkronkan, mencari data, tidak menemukannya, dan masuk ke status menunggu. Sebelumnyasecara fisik, ia harus melepaskan pemblokiran agar tidak mengganggu pekerjaan utas pemasok.
  • Thread 2 (penyedia) memasuki metode tersinkronisasi yang dibebaskan oleh thread 1, menciptakan data untuk aliran 1 dan entah bagaimana memberi tahu aliran 1 tentang keberadaan data. Ini kemudian melepaskan kunci sehingga utas 1 dapat memproses data baru.

Jangan mencoba untuk memecahkan masalah ini dengan terus-menerus menjalankan thread 1 dan memeriksa kondisi variabel kondisi, yang nilainya> ditetapkan oleh thread 2. Keputusan ini akan sangat mempengaruhi kinerja program Anda, karena dalam kebanyakan kasus thread 1 akan dipanggil tanpa alasan; dan utas 2 akan menunggu begitu sering sehingga kehabisan waktu untuk membuat data.

Hubungan penyedia / konsumen sangat umum, jadi primitif khusus dibuat untuk situasi seperti itu di perpustakaan kelas pemrograman multithreaded. Di NET, primitif ini disebut Wait dan Pulse-PulseAl 1 dan merupakan bagian dari kelas Monitor. Gambar 10.8 mengilustrasikan situasi yang akan kita program. Program mengatur tiga antrian thread: antrian tunggu, antrian pemblokiran, dan antrian eksekusi. Penjadwal utas tidak mengalokasikan waktu CPU ke utas yang ada dalam antrian tunggu. Untuk utas yang akan dialokasikan waktu, itu harus pindah ke antrian eksekusi. Akibatnya, pekerjaan aplikasi diatur jauh lebih efisien daripada dengan polling biasa dari variabel bersyarat.

Dalam pseudocode, idiom konsumen data dirumuskan sebagai berikut:

"Masuk ke blok tersinkronisasi dari jenis berikut

Sementara tidak ada data

Pergi ke antrian menunggu

Lingkaran

Jika ada data, proseslah.

Tinggalkan blok yang disinkronkan

Segera setelah perintah Tunggu dijalankan, utas ditangguhkan, kunci dilepaskan, dan utas memasuki antrian tunggu. Ketika kunci dilepaskan, utas dalam antrian eksekusi diizinkan untuk berjalan. Seiring waktu, satu atau lebih utas yang diblokir akan membuat data yang diperlukan untuk pengoperasian utas yang ada dalam antrian tunggu. Karena validasi data dilakukan dalam satu loop, transisi untuk menggunakan data (setelah loop) hanya terjadi jika ada data yang siap untuk diproses.

Dalam pseudocode, idiom penyedia data terlihat seperti ini:

"Memasuki blok tampilan yang disinkronkan

Sementara data TIDAK diperlukan

Pergi ke antrian menunggu

Data Hasil Lainnya

Jika data sudah siap, panggil Pulse-PulseAll.

untuk memindahkan satu atau lebih utas dari antrian pemblokiran ke antrian eksekusi. Tinggalkan blok yang disinkronkan (dan kembali ke antrian run)

Misalkan program kita mensimulasikan sebuah keluarga dengan satu orang tua yang menghasilkan uang dan seorang anak yang menghabiskan uang itu. Saat uang habisternyata si anak harus menunggu datangnya jumlah baru. Implementasi perangkat lunak model ini terlihat seperti ini:

1 Opsi Ketat Aktif

2 Sistem Impor.Threading

3 Modul Modul

4 Sub Utama ()

5 Redupkan Keluarga Sebagai Keluarga Baru ()

6 theFamily.StartltsLife ()

7 Akhir Sub

8 Akhir fjodul

9

10 Keluarga Kelas Umum

11 mMoney Pribadi Sebagai Integer

12 mWeek Pribadi Sebagai Integer = 1

13 Publik Sub StartltsLife ()

14 Dim aThreadMulai Sebagai Thread BaruStarUAddressOf Me.Produce)

15 Dim bThreadMulai Sebagai Thread BaruStarUAddressOf Me.Consume)

16 Redupkan aThread Sebagai Utas Baru (aThreadStart)

17 Redupkan bThread Sebagai Thread Baru (bThreadStart)

18 aThread.Name = "Produksi"

19 aThread.Start ()

20 bThread.Name = "Konsumsi"

21 b Benang. Awal ()

22 Akhir Sub

23 Properti Publik TheWeek () Sebagai Integer

24 Dapatkan

25 Kembali mweek

26 Dapatkan Akhir

27 Set (Nilai ByVal Sebagai Integer)

28 mminggu - Nilai

29 Set Akhir

30 Akhir Properti

31 Milik Umum OurMoney () Sebagai Integer

32 Dapatkan

33 Mengembalikan mMoney

34 Akhir Dapatkan

35 Set (Nilai ByVal Sebagai Integer)

36 mUang = Nilai

37 Set Akhir

38 Akhir Properti

39 Sub Produk Publik ()

40 Benang.Tidur (500)

41 Lakukan

42 Monitor.Masukkan (Saya)

43 Lakukan Sementara Aku.Uang Kita> 0

44 Monitor.Tunggu (Saya)

45 Putaran

46 Saya.Uang Kita = 1000

47 Monitor.PulseAll (Saya)

48 Monitor.Keluar (Saya)

49 Putaran

50 Akhir Sub

51 Sub Konsumsi Publik ()

52 MsgBox ("Saya sedang mengkonsumsi thread")

53 Lakukan

54 Monitor.Masukkan (Saya)

55 Do While Me.OurMoney = 0

56 Monitor.Tunggu (Saya)

57 Putaran

58 Console.WriteLine ("Dear parent, aku baru saja menghabiskan semua milikmu" & _

uang dalam minggu "& TheWeek)

59 Minggu Ini + = 1

60 Jika Minggu = 21 * 52 Kemudian System.Environment.Exit (0)

61 Saya.Uang Kita = 0

62 Monitor.PulseAll (Saya)

63 Monitor.Keluar (Saya)

64 Putaran

65 Akhir Sub

66 Akhir Kelas

Metode StartltsLife (baris 13-22) bersiap untuk memulai aliran Produksi dan Konsumsi. Hal terpenting terjadi di aliran Produce (baris 39-50) dan Consume (baris 51-65). Prosedur Sub Produce memeriksa ketersediaan uang, dan jika ada uang masuk ke antrian tunggu. Jika tidak, orang tua menghasilkan uang (baris 46) dan memberi tahu objek dalam antrian menunggu tentang perubahan situasi. Perhatikan bahwa panggilan ke Pulse-Pulse All hanya berlaku bila kunci dilepaskan dengan perintah Monitor.Exit. Sebaliknya, prosedur Sub Konsumsi memeriksa ketersediaan uang, dan jika tidak ada uang, memberi tahu orang tua yang menunggu tentang hal itu. Baris 60 hanya mengakhiri program setelah 21 tahun bersyarat; memanggil Sistem. Environment.Exit (0) adalah analog .NET dari perintah End (perintah End juga didukung, tetapi tidak seperti System. Environment. Exit, perintah ini tidak mengembalikan kode keluar ke sistem operasi).

Utas yang ditempatkan pada antrian tunggu harus dibebaskan oleh bagian lain dari program Anda. Karena alasan inilah kami lebih suka menggunakan PulseAll daripada Pulse. Karena tidak diketahui sebelumnya utas mana yang akan diaktifkan saat Pulse 1 dipanggil, dengan jumlah utas yang relatif kecil dalam antrian, Anda juga dapat memanggil PulseAll.

Multithreading dalam program grafis

Diskusi kita tentang multithreading dalam aplikasi GUI dimulai dengan contoh yang menjelaskan untuk apa multithreading dalam aplikasi GUI. Buat formulir dengan dua tombol Mulai (btnStart) dan Batal (btnCancel), seperti yang ditunjukkan pada Gambar. 10.9. Mengklik tombol Mulai menghasilkan kelas yang berisi string acak 10 juta karakter dan metode untuk menghitung kemunculan huruf "E" dalam string panjang itu. Perhatikan penggunaan kelas StringBuilder untuk pembuatan string panjang yang lebih efisien.

Langkah 1

Thread 1 memperhatikan bahwa tidak ada data untuk itu. Itu memanggil Tunggu, melepaskan kunci, dan pergi ke antrian tunggu.



Langkah 2

Ketika kunci dilepaskan, utas 2 atau utas 3 meninggalkan antrian blok dan memasuki blok yang disinkronkan, memperoleh kunci

Langkah3

Katakanlah utas 3 memasuki blok yang disinkronkan, membuat data, dan memanggil Pulse-Pulse All.

Segera setelah keluar dari blok dan melepaskan kunci, utas 1 dipindahkan ke antrian eksekusi. Jika utas 3 memanggil Pluse, hanya satu yang memasuki antrian eksekusiutas, ketika Pluse All dipanggil, semua utas masuk ke antrian eksekusi.



Beras. 10.8. Masalah penyedia / konsumen

Beras. 10.9. Multithreading dalam aplikasi GUI sederhana

Sistem Impor.Teks

Karakter Acak Kelas Publik

m_Data Pribadi Sebagai StringBuilder

Mjength pribadi, m_count Sebagai Integer

Sub Publik Baru (ByVal n As Integer)

m_Panjang = n -1

m_Data = StringBuilder Baru (m_length) MakeString ()

Akhir Sub

Sub MakeString Pribadi ()

Redupkan saya Sebagai Integer

Redupkan myRnd Sebagai Acak Baru ()

Untuk i = 0 Sampai m_panjang

"Hasilkan angka acak antara 65 dan 90,

"ubah menjadi huruf besar

"dan lampirkan ke objek StringBuilder

m_Data.Append (Chr (myRnd.Next (65.90))))

Lanjut

Akhir Sub

Sub StartCount Publik ()

DapatkanEes ()

Akhir Sub

Sub GetEes Pribadi ()

Redupkan saya Sebagai Integer

Untuk i = 0 Sampai m_panjang

Jika m_Data.Chars (i) = CChar ("E") Maka

m_hitung + = 1

Akhiri Jika Selanjutnya

m_CountDone = Benar

Akhir Sub

Publik Hanya Baca

Properti GetCount () Sebagai Integer Get

Jika Tidak (m_CountDone) Maka

Kembalikan m_count

Berakhir jika

Akhir Dapatkan Akhir Properti

Publik Hanya Baca

Properti Selesai () Sebagai Boolean Get

Kembali

m_CountSelesai

Dapatkan Akhir

Akhiri Properti

Kelas Akhir

Ada kode yang sangat sederhana yang terkait dengan dua tombol pada formulir. Prosedur btn-Start_Click membuat instance kelas RandomCharacters di atas, yang merangkum string dengan 10 juta karakter:

Sub Pribadi btnStart_Click (Pengirim ByVal Sebagai System.Object.

ByVal e As System.EventArgs) Menangani btnStart.Click

Redupkan RC Sebagai Karakter Acak Baru (10000000)

RC.Mulai Hitung ()

MsgBox("Jumlah es adalah" & RC.GetCount)

Akhir Sub

Tombol Batal menampilkan kotak pesan:

Sub Pribadi btnCancel_Click (Pengirim ByVal Sebagai System.Object._

ByVal e As System.EventArgs) Menangani btnCancel.Click

MsgBox("Jumlah Terputus!")

Akhir Sub

Ketika program dijalankan dan tombol Start ditekan, ternyata tombol Cancel tidak merespon input pengguna karena loop terus menerus mencegah tombol menangani event yang diterimanya. Ini tidak dapat diterima dalam program modern!

Ada dua kemungkinan solusi. Opsi pertama, terkenal dari versi VB sebelumnya, membuang multithreading: panggilan DoEvents disertakan dalam loop. Di NET perintah ini terlihat seperti ini:

Aplikasi.DoEvents ()

Dalam contoh kami, ini jelas tidak diinginkan - siapa yang ingin memperlambat program dengan sepuluh juta panggilan DoEvents! Jika Anda mengalokasikan loop ke utas terpisah, sistem operasi akan beralih di antara utas dan tombol Batal akan tetap berfungsi. Implementasi dengan utas terpisah ditunjukkan di bawah ini. Untuk menunjukkan dengan jelas bahwa tombol Batal berfungsi, ketika kita mengkliknya, kita cukup menghentikan program.

Langkah selanjutnya: Tampilkan tombol Hitung

Katakanlah Anda memutuskan untuk menunjukkan imajinasi kreatif Anda dan memberikan bentuk tampilan yang ditunjukkan pada gambar. 10.9. Harap diperhatikan: tombol Tampilkan Hitungan belum tersedia.

Beras. 10.10. Bentuk tombol terkunci

Utas terpisah diharapkan untuk melakukan penghitungan dan membuka kunci tombol yang tidak tersedia. Hal ini tentu saja dapat dilakukan; apalagi, tugas seperti itu cukup sering muncul. Sayangnya, Anda tidak akan dapat bertindak dengan cara yang paling jelas - tautkan utas sekunder ke utas GUI dengan menyimpan tautan ke tombol ShowCount di konstruktor, atau bahkan menggunakan delegasi standar. Dengan kata lain, tidak pernah jangan gunakan opsi di bawah ini (dasar keliru garis-garisnya dicetak tebal).

Karakter Acak Kelas Publik

m_0ata Pribadi Sebagai StringBuilder

m_CountSelesai Sebagai Boolean

Mjength pribadi. m_count Sebagai Bilangan Bulat

m_Button Pribadi Sebagai Windows.Forms.Button

Sub Publik Baru (ByVa1 n As Integer, _

ByVal b Sebagai Windows.Forms.Button)

m_panjang = n - 1

m_Data = StringBuilder Baru (mJength)

m_Button = b MakeString ()

Akhir Sub

Sub MakeString Pribadi ()

Redupkan Aku Sebagai Integer

Redupkan myRnd Sebagai Acak Baru ()

Untuk I = 0 Sampai m_panjang

m_Data.Append (Chr (myRnd.Next (65.90))))

Lanjut

Akhir Sub

Sub StartCount Publik ()

DapatkanEes ()

Akhir Sub

Sub GetEes Pribadi ()

Redupkan Aku Sebagai Integer

Untuk I = 0 Sampai mjength

Jika m_Data.Chars (I) = CChar ("E") Maka

m_hitung + = 1

Akhiri Jika Selanjutnya

m_CountDone = Benar

m_Button.Enabled = Benar

Akhir Sub

Publik Hanya Baca

Properti GetCount () Sebagai Integer

Mendapatkan

Jika Tidak (m_CountDone) Maka

Lempar Pengecualian Baru ("Hitung belum selesai") Lain

Kembalikan m_count

Berakhir jika

Dapatkan Akhir

Akhiri Properti

Properti Hanya-Baca Publik Selesai () Sebagai Boolean

Mendapatkan

Kembalikan m_CountDone

Dapatkan Akhir

Akhiri Properti

Kelas Akhir

Kemungkinan kode ini akan berfungsi dalam beberapa kasus. Namun demikian:

  • Interaksi utas sekunder dengan utas yang membuat GUI tidak dapat diatur jelas cara.
  • Tidak pernah jangan memodifikasi elemen dalam program grafis dari aliran program lain. Semua perubahan hanya boleh terjadi di utas yang membuat GUI.

Jika Anda melanggar aturan ini, kami kami jamin bahwa bug halus dan halus akan terjadi di program grafis multi-utas Anda.

Itu juga akan gagal untuk mengatur interaksi objek menggunakan acara. Pekerja 06-acara berjalan di utas yang sama dengan yang dipanggil RaiseEvent sehingga acara tidak akan membantu Anda.

Namun, akal sehat menyatakan bahwa aplikasi grafis harus memiliki sarana untuk memodifikasi elemen dari utas lain. Di .NET Framework, ada cara yang aman untuk memanggil metode aplikasi GUI dari utas lain. Tipe khusus delegasi Method Invoker dari System.Windows namespace digunakan untuk tujuan ini. Formulir. Cuplikan berikut menunjukkan versi baru metode GetEes (baris yang diubah dicetak tebal):

Sub GetEes Pribadi ()

Redupkan Aku Sebagai Integer

Untuk I = 0 Sampai m_panjang

Jika m_Data.Chars (I) = CChar ("E") Maka

m_hitung + = 1

Akhiri Jika Selanjutnya

m_CountDone = Benar Coba

Dim mylnvoker Sebagai Metode Barulnvoker (AddressOf UpDateButton)

myInvoker.Invoke () Tangkap e Sebagai ThreadlnterruptedException

"Kegagalan

Akhiri Coba

Akhir Sub

Tombol Pembaruan Sub Publik ()

m_Button.Enabled = Benar

Akhir Sub

Panggilan antar-utas ke tombol dilakukan tidak secara langsung, tetapi melalui Metode Invoker. .NET Framework menjamin bahwa opsi ini aman untuk utas.

Mengapa ada begitu banyak masalah dengan pemrograman multithreaded?

Sekarang setelah Anda memiliki pemahaman tentang multithreading dan potensi masalah yang terkait dengannya, kami memutuskan bahwa akan tepat untuk menjawab pertanyaan di judul subbagian ini di akhir bab ini.

Salah satu alasannya adalah bahwa multithreading adalah proses non-linear, dan kami terbiasa dengan model pemrograman linier. Pada awalnya, sulit untuk membiasakan diri dengan gagasan bahwa eksekusi program dapat diinterupsi secara acak, dan kontrol akan dialihkan ke kode lain.

Namun, ada alasan lain yang lebih mendasar: saat ini programmer terlalu jarang memprogram dalam assembler, atau setidaknya melihat keluaran kompilator yang dibongkar. Jika tidak, akan jauh lebih mudah bagi mereka untuk terbiasa dengan gagasan bahwa lusinan instruksi perakitan dapat sesuai dengan satu perintah bahasa tingkat tinggi (seperti VB .NET). Utas dapat terputus setelah salah satu dari instruksi ini, dan karena itu di tengah-tengah perintah tingkat tinggi.

Tapi bukan itu saja: kompiler modern mengoptimalkan kinerja program, dan perangkat keras komputer dapat mengganggu manajemen memori. Akibatnya, kompiler atau perangkat keras dapat mengubah urutan perintah yang ditentukan dalam kode sumber program tanpa sepengetahuan Anda [ Banyak kompiler mengoptimalkan penyalinan siklik dari array seperti for i = 0 to n: b (i) = a (i): ncxt. Kompiler (atau bahkan manajer memori khusus) dapat dengan mudah membuat array dan kemudian mengisinya dengan satu operasi penyalinan alih-alih menyalin elemen individual berkali-kali!].

Mudah-mudahan, penjelasan ini akan membantu Anda lebih memahami mengapa pemrograman multithreaded menyebabkan begitu banyak masalah - atau setidaknya kurang terkejut dengan perilaku aneh program multithreaded Anda!

Contoh membangun aplikasi multi-utas sederhana.

Lahir karena banyak pertanyaan tentang membangun aplikasi multithreaded di Delphi.

Tujuan dari contoh ini adalah untuk mendemonstrasikan cara membangun aplikasi multi-utas dengan benar, dengan menghilangkan pekerjaan jangka panjang di utas terpisah. Dan bagaimana, dalam aplikasi seperti itu, untuk memastikan interaksi utas utama dengan pekerja untuk mentransfer data dari formulir (komponen visual) ke aliran dan sebaliknya.

Contoh tidak mengklaim lengkap, itu hanya menunjukkan cara paling sederhana interaksi antar utas. Mengizinkan pengguna untuk "cepat buta" (siapa yang akan tahu betapa saya membencinya) aplikasi multithreaded yang berfungsi dengan baik.
Segala sesuatu di dalamnya dikomentari secara rinci (menurut saya), tetapi jika Anda memiliki pertanyaan, tanyakan.
Tetapi sekali lagi saya memperingatkan Anda: Streaming tidak mudah... Jika Anda tidak tahu cara kerjanya, maka ada bahaya besar bahwa seringkali semuanya akan bekerja dengan baik untuk Anda, dan terkadang program akan berperilaku lebih dari aneh. Perilaku program multithreaded yang salah ditulis sangat bergantung pada sejumlah besar faktor yang terkadang tidak dapat direproduksi selama debugging.

Jadi contoh. Untuk kenyamanan, saya telah menempatkan kedua kode dan melampirkan arsip dengan modul dan kode formulir

satuan ExThreadForm;

menggunakan
Windows, Pesan, SysUtils, Varian, Kelas, Grafik, Kontrol, Formulir,
Dialog, StdCtrls;

// konstanta yang digunakan saat mentransfer data dari aliran ke formulir menggunakan
// kirim pesan jendela
konstan
WM_USER_SendMessageMetod = WM_USER + 10;
WM_USER_PostMessageMetod = WM_USER + 11;

Tipe
// deskripsi kelas utas, turunan dari tThread
tMyThread = kelas (tThread)
pribadi
SyncDataN: Bilangan bulat;
SyncDataS: String;
prosedur SyncMetod1;
terlindung
prosedur Jalankan; mengesampingkan;
publik
Param1: Tali;
Param2: Bilangan bulat;
Param3: Boolean;
Berhenti: Boolean;
TerakhirAcak: Integer;
IterasiNo: Integer;
Daftar Hasil: tStringList;

Buat Konstruktor (aParam1: String);
perusak Hancurkan; mengesampingkan;
akhir;

// deskripsi kelas formulir menggunakan aliran
TForm1 = kelas (TForm)
Label1: TLabel;
Memo1: TMemo;
btnMulai: TButton;
btnStop: TButton;
Sunting1: TEdit;
Sunting2: TEdit;
Kotak Centang1: TCheckBox;
Label2: TLabel;
Label3: TLabel;
Label4: TLabel;
prosedur btnStartClick (Pengirim: TObject);
prosedur btnStopClick (Pengirim: TObject);
pribadi
(Deklarasi pribadi)
MyThread: tMyThread;
prosedur EventMyThreadOnTerminate (Pengirim: tObject);
prosedur EventOnSendMessageMetod (var Msg: TMessage);pesan WM_USER_SendMessageMetod;
prosedur EventOnPostMessageMetod (var Msg: TMessage); pesan WM_USER_PostMessageMetod;

Publik
(Deklarasi publik)
akhir;

var
Formulir1: TForm1;

{
Berhenti - Menunjukkan transfer data dari formulir ke aliran.
Sinkronisasi tambahan tidak diperlukan, karena sederhana
jenis kata tunggal, dan ditulis oleh hanya satu utas.
}

prosedur TForm1.btnStartClick (Pengirim: TObject);
mulai
Acak (); // memastikan keacakan dalam urutan dengan Random () - tidak ada hubungannya dengan aliran

// Buat instance dari objek stream, berikan parameter input
{
PERHATIAN!
Konstruktor aliran ditulis sedemikian rupa sehingga aliran dibuat
ditangguhkan karena memungkinkan:
1. Kontrol saat peluncurannya. Ini hampir selalu lebih nyaman karena
memungkinkan Anda untuk mengatur aliran bahkan sebelum memulai, berikan masukan
parameter, dll.
2. Karena tautan ke objek yang dibuat akan disimpan di bidang formulir, lalu
setelah penghancuran diri dari utas (lihat di bawah) yang ketika utas sedang berjalan
dapat terjadi kapan saja, tautan ini akan menjadi tidak valid.
}
MyThread: = tMyThread.Create (Form1.Edit1.Text);

// Namun, karena utas dibuat ditangguhkan, maka pada kesalahan apa pun
// selama inisialisasi (sebelum memulai), kita harus menghancurkannya sendiri
// untuk apa yang kita gunakan coba / kecuali blok
mencoba

// Menetapkan handler penghentian utas di mana kami akan menerima
// hasil karya aliran, dan "timpa" tautannya
MyThread.OnTerminate: = EventMyThreadOnTerminate;

// Karena hasilnya akan dikumpulkan di OnTerminate, mis. sebelum penghancuran diri
// sungai maka kita akan menghilangkan kekhawatiran menghancurkannya
MyThread.FreeOnTerminate: = Benar;

// Contoh melewatkan parameter input melalui bidang objek aliran, pada titik
// membuat instance ketika belum berjalan.
// Secara pribadi, saya lebih suka melakukan ini melalui parameter yang diganti
// konstruktor (tMyThread.Create)
MyThread.Param2: = StrToInt (Form1.Edit2.Text);

MyThread.Stopped: = Salah; // semacam parameter juga, tetapi berubah
// waktu berjalan utas
kecuali
// karena utas belum dimulai dan tidak dapat dihancurkan sendiri, kami akan menghancurkannya "secara manual"
GratisAndNil (MyThread);
// dan biarkan pengecualian ditangani seperti biasa
menaikkan;
akhir;

// Karena objek utas telah berhasil dibuat dan dikonfigurasi, saatnya untuk memulainya
MyThread.Lanjutkan;

ShowMessage("Streaming dimulai");
akhir;

prosedur TForm1.btnStopClick (Pengirim: TObject);
mulai
// Jika instance utas masih ada, mintalah untuk berhenti
// Dan, tepatnya "bertanya". Pada prinsipnya, kita juga bisa "memaksa", tetapi itu akan
// opsi yang sangat darurat, membutuhkan pemahaman yang jelas tentang semua ini
// mengalirkan dapur. Oleh karena itu, tidak dipertimbangkan di sini.
jika Ditugaskan (MyThread) maka
MyThread.Stopped: = Benar
lain
ShowMessage("Utas tidak berjalan!");
akhir;

prosedur TForm1.EventOnSendMessageMetod (pesan var: TMessage);
mulai
// metode untuk memproses pesan sinkron
// di WParam alamat objek tMyThread, di LParam nilai saat ini dari LastRandom dari utas
dengan tMyThread (Msg.WParam) dimulai
Form1.Label3.Caption: = Format ("% d% d% d",);
akhir;
akhir;

prosedur TForm1.EventOnPostMessageMetod (pesan var: TMessage);
mulai
// metode untuk menangani pesan asinkron
// di WParam nilai IterationNo saat ini, di LParam nilai arus LastRandom saat ini
Form1.Label4.Caption: = Format ("% d% d",);
akhir;

prosedur TForm1.EventMyThreadOnTerminate (Pengirim: tObject);
mulai
// PENTING!
// Metode untuk menangani event OnTerminate selalu dipanggil dalam konteks main
// thread - ini dijamin oleh implementasi tThread. Karena itu, di dalamnya Anda dapat dengan bebas
// gunakan properti dan metode apa pun dari objek apa pun

// Untuk jaga-jaga, pastikan instance objek masih ada
jika tidak Ditugaskan (MyThread) lalu Keluar; // jika tidak ada, maka tidak ada yang bisa dilakukan

// dapatkan hasil kerja utas dari instance objek utas
Form1.Memo1.Lines.Add (Format ("Aliran berakhir dengan hasil% d",));
Form1.Memo1.Lines.AddStrings ((Pengirim sebagai tMyThread) .ResultList);

// Hancurkan referensi ke instance objek stream.
// Karena utas kami merusak diri sendiri (FreeOnTerminate: = True)
// lalu setelah handler OnTerminate selesai, instance objek stream akan menjadi
// dimusnahkan (Gratis) dan semua referensinya akan menjadi tidak valid.
// Agar tidak secara tidak sengaja menemukan tautan seperti itu, timpa MyThread
// Saya akan mencatat sekali lagi - kami tidak akan menghancurkan objek, tetapi hanya menimpa tautannya. Sebuah Objek
// hancurkan dirinya sendiri!
MyThread: = Nihil;
akhir;

konstruktor tMyThread.Create (aParam1: String);
mulai
// Buat instance aliran SUSPENDED (lihat komentar saat membuat instance)
mewarisi Buat (Benar);

// Buat objek internal (jika perlu)
Daftar Hasil: = tStringList.Create;

// Dapatkan data awal.

// Salin data input yang melewati parameter
Param1: = aParam1;

// Contoh penerimaan data input dari komponen VCL dalam konstruktor objek aliran
// Ini dapat diterima dalam kasus ini, karena konstruktor dipanggil dalam konteks
// utas utama. Oleh karena itu, komponen VCL dapat diakses di sini.
// Tapi, saya tidak suka ini, karena saya pikir itu buruk ketika utas mengetahui sesuatu
// tentang beberapa bentuk di sana. Tapi, apa yang tidak bisa Anda lakukan untuk demonstrasi.
Param3: = Form1.CheckBox1.Checked;
akhir;

destructor tMyThread.Destroy;
mulai
// penghancuran objek internal
FreeAndNil (Daftar Hasil);
// hancurkan basis tThread
diwariskan;
akhir;

prosedur tMyThread.Execute;
var
t: Kardinal;
s: Tali;
mulai
IterasiNo: = 0; // penghitung hasil (nomor siklus)

// Dalam contoh saya, badan utas adalah loop yang berakhir
// atau dengan "permintaan" eksternal untuk mengakhiri melewati parameter variabel Dihentikan,
// baik hanya dengan melakukan 5 loop
// Lebih menyenangkan bagi saya untuk menulis ini melalui loop "abadi".

Sementara True dimulai

Inc (No Iterasi); // nomor siklus berikutnya

LastRandom: = Acak (1000); // nomor kunci - untuk mendemonstrasikan transfer parameter dari aliran ke formulir

T: = Acak (5) +1; // waktu di mana kita akan tertidur jika kita tidak selesai

// Pekerjaan bodoh (tergantung pada parameter input)
jika bukan Param3 maka
Inc (Param2)
lain
Des (Param2);

// Bentuk hasil antara
s: = Format ("% s% 5d% s% d% d",
);

// Tambahkan hasil antara ke daftar hasil
ResultList.Add (s);

//// Contoh meneruskan hasil antara ke formulir

//// Melewati metode yang disinkronkan - cara klasik
//// Kekurangan:
//// - metode yang disinkronkan biasanya merupakan metode kelas aliran (untuk mengakses
//// ke bidang objek aliran), tetapi, untuk mengakses bidang formulir, itu harus
//// "tahu" tentang itu dan bidangnya (objek), yang biasanya tidak terlalu baik dengan
//// sudut pandang organisasi program.
//// - utas saat ini akan ditangguhkan hingga eksekusi selesai
//// metode yang disinkronkan.

//// Keuntungan:
//// - standar dan serbaguna
//// - dalam metode yang disinkronkan, Anda dapat menggunakan
//// semua bidang objek aliran.
// pertama, jika perlu, Anda perlu menyimpan data yang ditransfer di
// bidang khusus dari objek objek.
SyncDataN: = IterationNo;
SyncDataS: = "Sinkronisasi" + s;
// dan kemudian berikan panggilan metode yang disinkronkan
Sinkronisasi (SyncMetod1);

//// Mengirim melalui pengiriman pesan sinkron (SendMessage)
//// dalam hal ini, data dapat dilewatkan baik melalui parameter pesan (LastRandom),
//// dan melalui bidang objek, meneruskan alamat instance dalam parameter pesan
//// dari objek aliran - Integer (Self).
//// Kekurangan:
//// - utas harus mengetahui pegangan jendela formulir
//// - seperti halnya Sinkronisasi, utas saat ini akan ditangguhkan hingga
//// menyelesaikan pemrosesan pesan oleh utas utama
//// - membutuhkan banyak waktu CPU untuk setiap panggilan
//// (untuk mengganti utas) oleh karena itu panggilan yang sangat sering tidak diinginkan
//// Keuntungan:
//// - seperti Sinkronisasi, saat memproses pesan, Anda dapat menggunakan
//// semua bidang objek aliran (jika, tentu saja, alamatnya diteruskan)


//// mulai utasnya.
SendMessage (Form1.Handle, WM_USER_SendMessageMetod, Integer (Self), LastRandom);

//// Transfer melalui pengiriman pesan asinkron (PostMessage)
//// Karena dalam kasus ini, pada saat pesan diterima oleh utas utama,
//// aliran pengiriman mungkin sudah selesai, melewati alamat instance
//// objek aliran tidak valid!
//// Kekurangan:
//// - utas harus mengetahui pegangan jendela formulir;
//// - karena asinkron, transfer data hanya dimungkinkan melalui parameter
//// pesan, yang secara signifikan mempersulit transfer data yang memiliki ukuran
//// lebih dari dua kata mesin. Lebih mudah digunakan untuk melewati Integer, dll.
//// Keuntungan:
//// - tidak seperti metode sebelumnya, utas saat ini TIDAK akan
//// dijeda dan akan segera melanjutkan eksekusi
//// - tidak seperti panggilan yang disinkronkan, penangan pesan
//// adalah metode formulir yang harus memiliki pengetahuan tentang objek aliran,
//// atau tidak tahu apa-apa tentang aliran sama sekali jika data hanya dikirimkan
//// melalui parameter pesan. Artinya, utas mungkin tidak tahu apa-apa tentang formulir.
//// umumnya - hanya Pegangannya, yang dapat diteruskan sebagai parameter sebelumnya
//// mulai utasnya.
PostMessage (Form1.Handle, WM_USER_PostMessageMetod, IterationNo, LastRandom);

//// Periksa kemungkinan penyelesaian

// Periksa penyelesaian berdasarkan parameter
jika Berhenti maka Hancurkan;

// Periksa penyelesaian sesekali
jika IterationNo> = 10 maka Break;

Tidur (t * 1000); // Tertidur selama t detik
akhir;
akhir;

prosedur tMyThread.SyncMetod1;
mulai
// metode ini dipanggil melalui metode Sinkronisasi.
// Artinya, terlepas dari fakta bahwa itu adalah metode dari utas tMyThread,
// itu berjalan dalam konteks utas utama aplikasi.
// Karena itu, dia bisa melakukan apa saja, yah, atau hampir semuanya :)
// Tapi ingat, tidak ada gunanya "bermain-main" di sini untuk waktu yang lama

// Parameter yang diteruskan, kita dapat mengekstrak dari bidang khusus, di mana kita memilikinya
// disimpan sebelum menelepon.
Form1.Label1.Caption: = SyncDataS;

// baik dari bidang lain dari objek aliran, misalnya, mencerminkan statusnya saat ini
Form1.Label2.Caption: = Format ("% d% d",);
akhir;

Secara umum, contoh didahului oleh alasan saya berikut tentang topik ...

Pertama:
Aturan PALING PENTING dari pemrograman multithreaded di Delphi adalah:
Dalam konteks utas non-utama, Anda tidak dapat mengakses properti dan metode formulir, dan memang semua komponen yang "tumbuh" dari tWinControl.

Ini berarti (agak disederhanakan) bahwa baik dalam metode Execute yang diwarisi dari TThread, maupun dalam metode/prosedur/fungsi lain yang dipanggil dari Execute, itu dilarang langsung mengakses properti dan metode komponen visual apa pun.

Bagaimana melakukannya dengan benar.
Tidak ada resep yang seragam. Lebih tepatnya, ada begitu banyak dan pilihan yang berbeda, tergantung pada kasus tertentu, Anda harus memilih. Karena itu, mereka merujuk ke artikel. Setelah membaca dan memahaminya, programmer akan dapat memahami dan cara terbaik untuk melakukannya dalam kasus tertentu.

Singkatnya di jari Anda:

Paling sering, aplikasi multithreaded menjadi baik ketika diperlukan untuk melakukan beberapa jenis pekerjaan jangka panjang, atau ketika mungkin untuk secara bersamaan melakukan beberapa hal yang tidak terlalu membebani prosesor.

Dalam kasus pertama, implementasi pekerjaan di dalam utas utama mengarah ke "perlambatan" antarmuka pengguna - saat pekerjaan sedang dilakukan, loop pesan tidak dijalankan. Akibatnya, program tidak merespons tindakan pengguna, dan formulir tidak digambar, misalnya, setelah pengguna memindahkannya.

Dalam kasus kedua, ketika pekerjaan melibatkan pertukaran aktif dengan dunia luar, maka selama "waktu henti" paksa. Sambil menunggu penerimaan/pengiriman data, Anda dapat melakukan hal lain secara paralel, misalnya lagi mengirim/menerima data.

Ada kasus lain, tetapi lebih jarang. Namun, itu tidak masalah. Sekarang bukan tentang itu.

Sekarang, bagaimana semuanya ditulis. Secara alami, kasus tertentu yang paling sering, agak umum, dipertimbangkan. Jadi.

Pekerjaan yang dilakukan di utas terpisah, dalam kasus umum, memiliki empat entitas (saya tidak tahu bagaimana menyebutnya lebih tepat):
1. Data awal
2. Sebenarnya pekerjaan itu sendiri (mungkin tergantung pada data awal)
3. Data perantara (misalnya, informasi tentang status pelaksanaan pekerjaan saat ini)
4. Data keluaran (hasil)

Paling sering, komponen visual digunakan untuk membaca dan menampilkan sebagian besar data. Namun, seperti yang disebutkan di atas, Anda tidak dapat langsung mengakses komponen visual dari aliran. Bagaimana menjadi?
Pengembang Delphi menyarankan menggunakan metode Synchronize dari kelas TThread. Di sini saya tidak akan menjelaskan cara menggunakannya - ada artikel yang disebutkan di atas untuk ini. Izinkan saya mengatakan bahwa penerapannya, bahkan yang benar, tidak selalu dibenarkan. Ada dua masalah:

Pertama, badan metode yang dipanggil melalui Sinkronisasi selalu dieksekusi dalam konteks utas utama, dan oleh karena itu, ketika sedang dieksekusi, sekali lagi, loop pesan jendela tidak dieksekusi. Oleh karena itu, ini harus dieksekusi dengan cepat, jika tidak, kita akan mendapatkan semua masalah yang sama dengan implementasi single-threaded. Idealnya, metode yang dipanggil melalui Sinkronisasi umumnya hanya digunakan untuk mengakses properti dan metode objek visual.

Kedua, menjalankan metode melalui Sinkronisasi adalah kesenangan "mahal" karena perlunya dua sakelar di antara utas.

Selain itu, kedua masalah saling berhubungan, dan menyebabkan kontradiksi: di satu sisi, untuk menyelesaikan yang pertama, perlu "menggiling" metode yang dipanggil melalui Sinkronisasi, dan di sisi lain, mereka kemudian sering harus dipanggil, kehilangan yang berharga sumber daya prosesor.

Oleh karena itu, seperti biasa, perlu untuk mendekati secara wajar, dan untuk kasus yang berbeda, gunakan cara interaksi aliran yang berbeda dengan dunia luar:

Data awal
Semua data yang ditransfer ke aliran, dan tidak berubah selama operasinya, harus ditransfer bahkan sebelum dimulai, mis. saat membuat aliran. Untuk menggunakannya di badan utas, Anda perlu membuat salinan lokalnya (biasanya di bidang turunan TThread).
Jika ada data awal yang dapat berubah saat thread sedang berjalan, maka data tersebut harus diakses baik melalui metode yang disinkronkan (metode yang disebut melalui Synchronize), atau melalui bidang objek thread (keturunan dari TThread). Yang terakhir membutuhkan beberapa kehati-hatian.

Data perantara dan keluaran
Di sini, sekali lagi, ada beberapa cara (dalam urutan preferensi saya):
- Metode pengiriman pesan yang tidak sinkron ke jendela aplikasi utama.
Biasanya digunakan untuk mengirim pesan tentang kemajuan proses ke jendela aplikasi utama, dengan transfer sejumlah kecil data (misalnya, persentase penyelesaian)
- Metode pengiriman pesan secara sinkron ke jendela aplikasi utama.
Biasanya digunakan untuk tujuan yang sama seperti pengiriman asinkron, tetapi memungkinkan Anda mentransfer data dalam jumlah besar, tanpa membuat salinan terpisah.
- Metode yang disinkronkan, jika memungkinkan, menggabungkan transfer data sebanyak mungkin ke dalam satu metode.
Dapat juga digunakan untuk mengambil data dari suatu form.
- Melalui bidang objek aliran, memberikan akses yang saling eksklusif.
Detail lebih lanjut dapat ditemukan di artikel.

Eh. Itu tidak berhasil untuk waktu yang singkat