
Kemungkinan besar, Anda telah mendengar tentang bahasa pemrograman Go, popularitasnya terus meningkat, yang cukup masuk akal. Bahasa ini sederhana, cepat dan bergantung pada komunitas yang hebat. Salah satu aspek yang paling aneh dari bahasa ini adalah model pemrograman multi-threaded. Primitif yang mendasarinya memungkinkan Anda membuat program multi-utas dengan mudah dan sederhana. Artikel ini ditujukan bagi mereka yang ingin mempelajari primitif ini: goroutine dan saluran. Dan, melalui ilustrasi, saya akan menunjukkan cara bekerja dengannya. Saya harap ini akan menjadi bantuan yang baik untuk Anda dalam studi Anda selanjutnya.
Program tunggal dan multi-utas
Anda kemungkinan besar sudah menulis program single-threaded. Biasanya terlihat seperti ini: ada satu set fungsi untuk melakukan berbagai tugas, masing-masing fungsi dipanggil hanya ketika yang sebelumnya menyiapkan data untuk itu. Dengan demikian, program berjalan secara berurutan.
Itu akan menjadi contoh pertama kami - program penambangan bijih. Fungsi kami akan mencari, menambang, dan memproses bijih. Bijih di tambang dalam contoh kami diwakili oleh daftar string, fungsinya mengambil parameter dan mengembalikan daftar string "diproses". Untuk program single-threaded, aplikasi kami akan dirancang sebagai berikut:

Dalam contoh ini, semua pekerjaan dilakukan oleh satu utas (Gary's gopher). Tiga fungsi utama: pencarian, produksi, dan pemrosesan dilakukan berurutan satu demi satu.
func main() { theMine := [5]string{"rock", "ore", "ore", "rock", "ore"} foundOre := finder(theMine) minedOre := miner(foundOre) smelter(minedOre) }
Jika kami mencetak hasil dari masing-masing fungsi, kami mendapatkan yang berikut:
From Finder: [ore ore ore] From Miner: [minedOre minedOre minedOre] From Smelter: [smeltedOre smeltedOre smeltedOre]
Desain dan implementasi yang sederhana merupakan nilai tambah dari pendekatan single-threaded. Tetapi bagaimana jika Anda ingin menjalankan dan menjalankan fungsi secara independen satu sama lain? Di sini pemrograman multithreaded membantu Anda.

Pendekatan penambangan bijih ini jauh lebih efisien. Sekarang beberapa utas (pedagang eceran) bekerja secara independen, dan Gary hanya mengerjakan sebagian dari pekerjaan itu. Satu gopher mencari bijih, yang lain menghasilkan, dan yang ketiga meleleh, dan semua ini berpotensi secara simultan. Untuk menerapkan pendekatan ini, kita memerlukan dua hal dalam kode: untuk membuat prosesor-gopher secara independen satu sama lain dan untuk mentransfer bijih di antara mereka. Go memiliki goroutine dan saluran untuk ini.
Gorutin
Goroutine dapat dianggap sebagai "thread ringan", untuk membuat goroutine Anda hanya perlu meletakkan kata kunci
go sebelum kode panggilan fungsi. Untuk mendemonstrasikan betapa sederhananya, mari kita membuat dua fungsi pencarian, panggil mereka dengan kata kunci
go dan cetak pesan setiap kali mereka menemukan "ore" di tambang mereka.

func main() { theMine := [5]string{"rock", "ore", "ore", "rock", "ore"} go finder1(theMine) go finder2(theMine) <-time.After(time.Second * 5)
Output dari program kami adalah sebagai berikut:
Finder 1 found ore! Finder 2 found ore! Finder 1 found ore! Finder 1 found ore! Finder 2 found ore! Finder 2 found ore!
Seperti yang Anda lihat, tidak ada urutan di mana fungsi pertama kali "menemukan bijih"; fungsi pencarian bekerja secara simultan. Jika Anda menjalankan contoh beberapa kali, urutannya akan berbeda. Sekarang kita dapat menjalankan program multi-threaded (multi-sphere), dan ini adalah kemajuan yang serius. Tetapi apa yang harus dilakukan ketika kita perlu membangun hubungan antara goroutine independen? Waktunya akan tiba untuk keajaiban saluran.
Saluran

Saluran memungkinkan goroutine untuk bertukar data. Ini adalah semacam pipa tempat goroutin dapat mengirim dan menerima informasi dari goroutin lain.

Membaca dan menulis ke saluran dilakukan menggunakan operator panah (<-), yang menunjukkan arah pergerakan data.

myFirstChannel := make(chan string) myFirstChannel <- "hello"
Sekarang pencari bakat kami tidak perlu menumpuk bijih, ia dapat segera mentransfernya lebih lanjut menggunakan saluran.

Saya memperbarui contoh, sekarang kode pencari bijih dan penambang adalah fungsi anonim. Jangan terlalu repot jika Anda belum pernah menjumpai mereka sebelumnya, perlu diingat bahwa masing-masing dari mereka dipanggil dengan kata kunci
go , oleh karena itu, itu akan dieksekusi di goroutine sendiri. Yang paling penting di sini adalah bahwa goroutine mengirimkan data di antara mereka sendiri menggunakan saluran
oreChan . Dan kita akan berurusan dengan fungsi anonim lebih dekat hingga akhir.
func main() { theMine := [5]string{“ore1”, “ore2”, “ore3”} oreChan := make(chan string)
Kesimpulan di bawah ini dengan jelas menunjukkan bahwa Penambang kami menerima tiga kali dari saluran tersebut, satu porsi setiap kali.
Miner: Received ore1 from finder Miner: Received ore2 from finder Miner: Received ore3 from finder
Jadi, sekarang kita dapat mentransfer data antara goroutine yang berbeda (akan menghubungkan), tetapi sebelum kita mulai menulis program yang kompleks, mari kita lihat beberapa sifat penting dari saluran.
Kunci
Dalam beberapa situasi, ketika bekerja dengan saluran, goroutin dapat diblokir. Ini diperlukan agar goroutine dapat bersinkronisasi satu sama lain sebelum mereka mulai atau terus bekerja.
Tulis Kunci

Ketika goroutine (gopher) mengirim data ke saluran, itu diblokir sampai goroutine lain membaca data dari saluran.
Baca kunci

Mirip dengan mengunci ketika menulis ke saluran, goroutin dapat dikunci saat membaca dari saluran sampai tidak ada yang tertulis di sana.
Jika kunci, pada pandangan pertama, tampak rumit bagi Anda, Anda dapat membayangkannya sebagai "transfer uang" antara dua goroutine (penjual akan). Ketika seorang gopher ingin mentransfer atau menerima uang, ia harus menunggu peserta kedua dalam transaksi.
Setelah berurusan dengan kunci goroutine pada saluran, mari kita bahas dua jenis saluran yang berbeda: buffered dan unbuffered. Memilih jenis ini atau itu, kami sangat menentukan perilaku program.
Saluran yang tidak dibangun

Dalam semua contoh sebelumnya, kami hanya menggunakan saluran tersebut. Pada saluran tersebut, hanya satu bagian data yang dapat dikirimkan pada satu waktu (dengan pemblokiran, seperti dijelaskan di atas).
Saluran yang disangga

Streaming dalam suatu program tidak selalu dapat disinkronkan dengan sempurna. Misalkan, dalam contoh kita, itu terjadi bahwa pengintai gopher menemukan tiga bagian bijih, dan seorang penambang gopher berhasil mengekstraksi hanya satu bagian dari cadangan yang ditemukan pada saat yang sama. Di sini, agar pengintaian gopher tidak menghabiskan sebagian besar waktunya, menunggu penambang menyelesaikan pekerjaannya, kita akan menggunakan saluran buffered. Mari kita mulai dengan membuat saluran dengan kapasitas 3.
bufferedChan := make(chan string, 3)
Kami dapat mengirim beberapa bagian data ke saluran buffer, tanpa perlu membacanya dengan goroutine lain. Ini adalah perbedaan utama dari saluran yang tidak disadap.

bufferedChan := make(chan string, 3) go func() { bufferedChan <- "first" fmt.Println("Sent 1st") bufferedChan <- "second" fmt.Println("Sent 2nd") bufferedChan <- "third" fmt.Println("Sent 3rd") }() <-time.After(time.Second * 1) go func() { firstRead := <- bufferedChan fmt.Println("Receiving..") fmt.Println(firstRead) secondRead := <- bufferedChan fmt.Println(secondRead) thirdRead := <- bufferedChan fmt.Println(thirdRead) }()
Urutan output dalam program tersebut adalah sebagai berikut:
Sent 1st Sent 2nd Sent 3rd Receiving.. first second third
Untuk menghindari komplikasi yang tidak perlu, kami tidak akan menggunakan saluran buffered dalam program kami. Tetapi penting untuk diingat bahwa jenis saluran ini juga tersedia untuk digunakan.
Penting juga untuk dicatat bahwa saluran buffer tidak selalu menyelamatkan Anda dari pemblokiran. Misalnya, jika pemandu gopher sepuluh kali lebih cepat dari penambang gopher, dan mereka terhubung melalui saluran buffered dengan kapasitas 2, maka pemandu gopher akan diblokir setiap kali dikirim, jika sudah ada dua bagian data dalam saluran.
Menyatukan semuanya
Jadi, dengan berbekal goroutine dan saluran, kita dapat menulis sebuah program menggunakan semua keunggulan pemrograman multithreaded di Go.

theMine := [5]string{"rock", "ore", "ore", "rock", "ore"} oreChannel := make(chan string) minedOreChan := make(chan string)
Program semacam itu akan menampilkan yang berikut:
From Finder: ore From Finder: ore From Miner: minedOre From Smelter: Ore is smelted From Miner: minedOre From Smelter: Ore is smelted From Finder: ore From Miner: minedOre From Smelter: Ore is smelted
Dibandingkan dengan contoh pertama kami, ini merupakan peningkatan besar, sekarang semua fungsi dilakukan secara independen, masing-masing di goroutine sendiri. Dan kami juga mendapatkan konveyor dari saluran, di mana bijih ditransfer segera setelah pemrosesan. Untuk mempertahankan fokus pada pemahaman dasar tentang pengoperasian saluran dan goroutine, saya menghilangkan beberapa poin, yang dapat menyebabkan kesulitan dalam meluncurkan program. Kesimpulannya, saya ingin memikirkan fitur-fitur bahasa ini, karena mereka membantu dalam bekerja dengan goroutine dan saluran.
Gorutin Anonim

Sama seperti kita menjalankan fungsi reguler di goroutine, kita dapat mendeklarasikan fungsi anonim segera setelah kata kunci
go dan memanggilnya menggunakan sintaks berikut:
Jadi, jika kita perlu memanggil fungsi hanya di satu tempat, kita dapat menjalankannya di goroutine terpisah tanpa khawatir tentang deklarasi sebelumnya.
Fungsi utamanya adalah goroutine.

Ya, fungsi
utama tidak bekerja di goroutine sendiri. Dan, yang lebih penting, setelah selesai, semua goroutine lainnya juga berakhir. Karena alasan inilah kami menempatkan penghitung waktu di akhir fungsi
utama kami. Panggilan ini menciptakan saluran dan mengirimkan data setelah 5 detik.
<-time.After(time.Second * 5)
Ingat bahwa goroutine akan diblokir saat membaca dari saluran sampai ada sesuatu yang dikirim ke sana? Inilah yang terjadi ketika kode yang ditentukan ditambahkan. Goroutine utama akan diblokir, memberi goroutias lainnya 5 detik waktu untuk bekerja. Metode ini bekerja dengan baik, tetapi biasanya pendekatan yang berbeda digunakan untuk memverifikasi bahwa semua goroutine telah menyelesaikan pekerjaan mereka. Untuk mengirimkan sinyal tentang penyelesaian pekerjaan, saluran khusus dibuat, goroutine utama diblokir agar tidak dapat membacanya, dan segera setelah putrinya goroutine menyelesaikan pekerjaannya, ia menulis ke saluran ini; Goroutine utama tidak dikunci dan program berakhir.

func main() { doneChan := make(chan string) go func() {
Baca dari pipa dalam lingkaran for-range
Dalam contoh kami, dalam fungsi goffer-getter, kami menggunakan loop
for untuk memilih tiga elemen dari saluran. Tapi apa yang harus dilakukan jika tidak diketahui sebelumnya berapa banyak data yang bisa di saluran? Dalam kasus seperti itu, Anda bisa menggunakan saluran sebagai argumen untuk loop
for-range , seperti halnya dengan koleksi. Fungsi yang diperbarui mungkin terlihat seperti ini:
Dengan demikian, penambang bijih akan membaca semua yang dikirim pramuka kepadanya, menggunakan saluran dalam siklus akan menjamin ini. Harap perhatikan bahwa setelah semua data dari saluran diproses, siklus akan mengunci saat membaca; untuk menghindari pemblokiran, Anda perlu menutup saluran dengan memanggil
tutup (saluran) .
Pembacaan saluran non-blocking
Menggunakan konstruksi
case-pilih , memblokir pembacaan dari pipa dapat dihindari. Berikut ini adalah contoh penggunaan konstruksi ini: goroutine akan membaca data dari saluran, jika hanya ada di sana, jika tidak blok
default dijalankan:
myChan := make(chan string) go func(){ myChan <- “Message!” }() select { case msg := <- myChan: fmt.Println(msg) default: fmt.Println(“No Msg”) } <-time.After(time.Second * 1) select { case msg := <- myChan: fmt.Println(msg) default: fmt.Println(“No Msg”) }
Setelah diluncurkan, kode ini akan menampilkan yang berikut:
No Msg Message!
Rekaman saluran yang tidak menghalangi
Mengunci saat menulis ke saluran dapat dihindari dengan menggunakan konstruksi
case-pilih yang sama. Mari kita edit kecil ke contoh sebelumnya:
select { case myChan <- “message”: fmt.Println(“sent the message”) default: fmt.Println(“no message sent”) }
Apa yang harus dipelajari lebih lanjut

Ada banyak artikel dan laporan yang meliput pekerjaan dengan saluran dan goroutine secara lebih rinci. Dan sekarang, dengan kode Anda memiliki gagasan yang jelas tentang mengapa dan bagaimana alat ini digunakan, Anda bisa mendapatkan yang terbaik dari bahan-bahan berikut:
Terima kasih telah meluangkan waktu untuk membaca. Saya harap saya membantu Anda memahami saluran, goroutine, dan manfaat yang diberikan program multithreaded kepada Anda.