Dunia tanpa coroutine. Iterator generator

1. Perkenalan



Untuk mengacaukan masalah sebanyak mungkin, percayakan solusi tersebut kepada programmer;). Tapi serius, menurut saya hal serupa terjadi pada coroutine, karena, mau atau tidak, mereka terbiasa mengaburkan situasi. Yang terakhir ini dicirikan oleh fakta bahwa masih ada masalah pemrograman paralel yang tidak berjalan ke mana-mana, dan yang paling penting, coroutine tidak berkontribusi pada solusi utama mereka.



Mari kita mulai dengan terminologi. "Berapa kali mereka memberitahu dunia", tetapi sejauh ini "dunia" masih mengajukan pertanyaan tentang perbedaan antara pemrograman asinkron dan pemrograman paralel (lihat pembahasan tentang topik asinkron di [1] ). Inti dari masalah pemahaman asynchrony versus parallelism dimulai dengan mendefinisikan paralelisme itu sendiri. Itu tidak ada. Ada semacam pemahaman intuitif, yang sering ditafsirkan dengan cara yang berbeda, tetapi tidak ada definisi ilmiah yang akan menghapus semua pertanyaan secara konstruktif seperti diskusi tentang hasil dari operasi "dua dan dua".



Dan karena, sekali lagi, semua ini tidak ada, maka, bingung dalam istilah dan konsep, kami masih membedakan antara pemrograman paralel dan konkuren, asinkron, reaktif dan beberapa lainnya, dll. dll. Saya pikir tidak mungkin ada masalah dalam menyadari bahwa kalkulator mekanis seperti Felix bekerja secara berbeda dari kalkulator perangkat lunak. Tetapi dari sudut pandang formal, yaitu satu set operasi dan hasil akhir, tidak ada perbedaan di antara mereka. Prinsip ini harus diperhitungkan dalam definisi pemrograman paralel.



Kita harus memiliki definisi yang ketat dan cara transparan untuk menggambarkan paralelisme, yang mengarah ke hasil yang konsisten seperti Felix yang "canggung" dan kalkulator perangkat lunak apa pun. Tidak mungkin konsep "paralelisme" dikaitkan dengan sarana implementasinya (dengan jumlah inti yang sama). Dan apa yang ada "di bawah tenda" - ini seharusnya hanya menarik bagi mereka yang terlibat dalam penerapan "mesin", tetapi bukan mereka yang menggunakan "kalkulator paralel" konvensional.



Tapi kami memiliki apa yang kami miliki. Dan kami memiliki, jika tidak menggila, maka diskusi aktif tentang coroutine dan pemrograman asynchronous. Dan apa lagi yang harus dilakukan jika kita tampaknya sudah muak dengan multithreading, tetapi ada hal lain yang tidak ditawarkan? Mereka bahkan berbicara tentang sejenis sihir;) Tetapi semuanya menjadi jelas jika Anda memahami alasannya. Dan mereka terletak persis di sana - di bidang paralelisme. Definisi dan implementasinya.



Tapi mari kita turun dari global dan, sampai batas tertentu, ketinggian filosofis dari ilmu pemrograman (komputer sejak) ke "bumi berdosa" kita. Di sini, tanpa mengurangi manfaat dari bahasa Kotlin yang populer saat ini, saya ingin mengakui kecintaan saya pada Python. Mungkin suatu hari nanti dan dalam situasi lain, preferensi saya akan berubah, tetapi pada kenyataannya, sejauh ini semuanya begitu.



Ada beberapa alasan untuk ini. Diantaranya adalah akses gratis ke Python. Ini bukan argumen terkuat, sejak itu sebuah contoh dengan Qt yang sama mengatakan bahwa situasi dapat berubah sewaktu-waktu. Tetapi sementara Python, tidak seperti Kotlin, gratis, setidaknya dalam bentuk lingkungan PyCharm yang sama dari JetBrains (yang terima kasih khusus kepada mereka), maka simpati saya ada di pihaknya. Menarik juga bahwa ada banyak literatur berbahasa Rusia, contoh Python di Internet, baik mendidik dan cukup nyata. Di Kotlin, mereka tidak berada dalam jumlah tersebut dan ragamnya tidak begitu bagus.



Mungkin sedikit di depan kurva, saya memutuskan untuk menyajikan hasil menguasai Python dalam konteks masalah mendefinisikan dan mengimplementasikan paralelisme perangkat lunak dan asynchrony. Ini diprakarsai oleh artikel [2]... Hari ini kita akan membahas topik generator-coroutine. Ketertarikan saya pada mereka didorong oleh kebutuhan untuk menyadari secara spesifik, menarik, tetapi tidak terlalu familiar bagi saya saat ini, kemungkinan bahasa / bahasa pemrograman modern.



Karena saya sebenarnya adalah programmer C ++ murni, ini menjelaskan banyak hal. Misalnya, jika di Python coroutine dan generator sudah ada sejak lama, maka di C ++ mereka belum memenangkan tempatnya. Tetapi apakah C ++ benar-benar membutuhkannya? Menurut saya, bahasa pemrograman perlu dikembangkan secara wajar. Tampaknya C ++ menarik sejauh mungkin, dan sekarang tergesa-gesa mencoba mengejar ketinggalan. Tetapi masalah konkurensi serupa dapat diimplementasikan menggunakan konsep dan model lain yang lebih mendasar daripada coroutine / coroutine. Dan fakta bahwa di balik pernyataan ini tidak hanya ada kata-kata akan didemonstrasikan lebih lanjut.



Jika kita harus mengakui semuanya, maka saya juga mengakui bahwa saya agak konservatif sehubungan dengan C ++. Tentu saja, objek dan kemampuan OOP-nya adalah "segalanya bagi kami" bagi saya, tetapi saya, harus kami katakan, sangat kritis terhadap template. Yah, saya tidak pernah benar-benar melihat "bahasa burung" mereka yang khas, yang, sepertinya, sangat memperumit persepsi kode dan pemahaman tentang algoritme. Meskipun kadang-kadang saya bahkan menggunakan bantuan mereka, jari-jari satu tangan sudah cukup untuk semua ini. Saya menghormati perpustakaan STL dan tidak dapat hidup tanpanya :) Oleh karena itu, bahkan dari fakta ini, terkadang saya ragu tentang template. Jadi saya masih menghindari mereka sebisa saya. Dan sekarang saya menunggu dengan ngeri untuk "coroutines template" di C ++;)



Python adalah masalah lain. Saya belum melihat pola apa pun di dalamnya dan itu menenangkan saya. Tapi, di sisi lain, ini, anehnya, mengkhawatirkan. Namun, ketika saya melihat kode Kotlin dan, terutama, pada kode kompartemen mesinnya, kecemasan dengan cepat berlalu;) Namun, menurut saya ini masih masalah kebiasaan dan prasangka saya. Saya berharap bahwa seiring waktu saya akan melatih diri saya untuk melihatnya (template) secara memadai.



Tapi ... kembali ke coroutine. Ternyata sekarang mereka bernama korutin. Apa yang baru dengan perubahan nama? Ya, sebenarnya tidak ada. Seperti sebelumnya, himpunan dianggap bergantianfungsi dilakukan. Dengan cara yang sama seperti sebelumnya, sebelum keluar dari fungsi, tetapi sebelum menyelesaikan pekerjaannya, titik kembali ditetapkan, dari mana pekerjaan dilanjutkan nanti. Karena urutan switching tidak ditentukan, pemrogram sendiri mengontrol proses ini dengan membuat penjadwal sendiri. Seringkali ini hanya perulangan fungsi. Seperti, misalnya, siklus peristiwa Round Robin dalam video Oleg Molchanov [3] .



Beginilah pengenalan modern coroutine coroutine dan pemrograman asynchronous biasanya terlihat seperti "di jari". Jelas bahwa dengan menyelami topik ini, istilah dan konsep baru muncul. Generator adalah salah satunya. Selanjutnya, contoh mereka akan menjadi dasar untuk mendemonstrasikan "preferensi paralel", tetapi sudah dalam interpretasi otomatis saya.



2. Generator daftar data



Jadi - generator. Pemrograman dan coroutine asynchronous sering dikaitkan dengannya. Serangkaian video dari Oleg Molchanov menceritakan tentang semua ini. Jadi, pada fitur utama generator, ia mengacu pada "kemampuan untuk menghentikan sementara eksekusi suatu fungsi untuk melanjutkan eksekusinya dari tempat yang sama di mana ia berhenti terakhir kali" (untuk lebih jelasnya, lihat [3] ). Dan dalam hal ini, mengingat penjelasan di atas tentang definisi coroutine yang sudah cukup kuno, tidak ada yang baru.



Namun ternyata, generator telah menemukan kegunaan yang cukup spesifik untuk membuat daftar data. Pengantar topik ini sudah tercakup dalam video dari Egorov Artem [4]... Tetapi tampaknya dengan penerapannya kami menggabungkan konsep yang berbeda secara kualitatif - operasi dan proses. Dengan memperluas kemampuan deskriptif bahasa, kami menutupi sebagian besar masalah yang mungkin timbul. Di sini, seperti yang mereka katakan, jangan terlalu banyak bermain. Menggunakan generator-coroutines untuk mendeskripsikan data berkontribusi pada hal ini, menurut saya. Perhatikan bahwa Oleg Molchanov juga memperingatkan agar tidak mengasosiasikan generator dengan struktur data, menekankan bahwa β€œgenerator adalah fungsi” [3] .



Tetapi kembali menggunakan generator untuk mendefinisikan data. Sulit untuk menyembunyikan bahwa kami telah membuat proses yang menghitung item daftar. Oleh karena itu, pertanyaan segera muncul tentang daftar seperti itu sebagai proses. Misalnya, bagaimana cara menggunakannya kembali jika coroutine menurut definisi hanya bekerja "satu arah"? Bagaimana cara menghitung elemen arbitrer jika proses pengindeksan tidak mungkin? Dll dll. Artem tidak memberikan jawaban atas pertanyaan-pertanyaan ini, hanya memperingatkan bahwa, kata mereka, akses berulang ke elemen daftar tidak dapat diatur, dan pengindeksan tidak dapat diterima. Pencarian di Internet meyakinkan saya bahwa tidak hanya saya yang memiliki pertanyaan serupa, tetapi solusi yang diajukan tidak begitu sepele dan jelas.



Masalah lainnya adalah kecepatan pembuatan daftar. Sekarang kita membentuk satu elemen daftar pada setiap saklar coroutine, dan ini meningkatkan waktu pembuatan data. Proses ini dapat dipercepat dengan menghasilkan elemen dalam "batch". Tapi, kemungkinan besar, akan ada masalah dengan ini. Bagaimana cara menghentikan proses yang sudah berjalan? Atau sesuatu yang lain. Daftarnya bisa sangat panjang, hanya menggunakan item yang dipilih. Dalam situasi seperti itu, menghafal data sering digunakan untuk akses yang efisien. Ngomong-ngomong, segera saya menemukan artikel tentang topik ini untuk Python, lihat [5] (untuk informasi lebih lanjut tentang memoization dalam hal automata, lihat artikel [6] ). Tapi bagaimana dengan kasus ini?



Keandalan sintaksis untuk menentukan daftar juga bisa dipertanyakan, karena cukup mudah untuk salah menggunakan tanda kurung siku alih-alih tanda kurung dan sebaliknya. Ternyata solusi yang tampak indah dan elegan dalam praktiknya dapat menimbulkan masalah tertentu. Bahasa pemrograman harus maju secara teknologi, fleksibel dan menjamin terhadap kesalahan yang tidak disengaja.



Ngomong-ngomong, tentang topik daftar dan generator tentang kelebihan dan kekurangannya, bersinggungan dengan pernyataan di atas, Anda dapat menonton video lain oleh Oleg Molchanov [7] .



3. Generator-coroutines



Video berikutnya oleh Oleg Molchanov [8] membahas penggunaan generator untuk mengkoordinasikan kerja coroutine. Sebenarnya, mereka dimaksudkan untuk ini. Perhatian diarahkan pada pilihan momen untuk beralih coroutine. Pengaturannya mengikuti aturan sederhana - letakkan pernyataan hasil di depan fungsi pemblokiran. Yang terakhir dipahami sebagai fungsi, waktu kembalinya begitu lama dibandingkan dengan operasi lain sehingga kalkulasi dikaitkan dengan menghentikannya. Karena itu, mereka disebut pemblokir.



Pengalihan efektif jika proses yang ditangguhkan melanjutkan tugasnya tepat saat panggilan pemblokiran tidak akan menunggu, tetapi akan menyelesaikan pekerjaannya dengan cepat. Dan, untuk kepentingan ini, tampaknya, semua "keributan" ini dimulai di sekitar model coroutine / coroutine dan, oleh karena itu, dorongan diberikan untuk pengembangan pemrograman asinkron. Meskipun, perhatikan, ide asli coroutine masih berbeda - untuk membuat model virtual komputasi paralel.



Dalam video yang dipertimbangkan, seperti dalam kasus umum coroutine, kelanjutan operasi coroutine ditentukan oleh lingkungan eksternal, yang merupakan penjadwal peristiwa. Dalam hal ini, ini diwakili oleh fungsi bernama event_loop. Dan, tampaknya, semuanya logis: penjadwal akan melakukan analisis dan melanjutkan pekerjaan coroutine dengan memanggil operator next (), tepat jika diperlukan. Masalahnya terletak pada menunggu di tempat yang tidak diharapkan: penjadwal bisa sangat rumit. Dalam video Molchanov sebelumnya ( lihat [3] ), semuanya sederhana, sejak itu transfer kontrol bolak-balik sederhana dilakukan, di mana tidak ada kunci, sejak itu tidak ada panggilan yang sesuai. Namun demikian, kami menekankan bahwa dalam hal apa pun setidaknya penjadwal sederhana diperlukan.



Masalah 1. , next() (. event_loop). , , yield. - , , next(), .



2. , select, β€” . .



Tetapi intinya bukanlah kebutuhan akan seorang perencana, tetapi fakta bahwa ia menjalankan fungsi yang tidak biasa baginya. Situasi ini semakin diperumit oleh fakta bahwa algoritma harus diimplementasikan untuk operasi gabungan dari banyak coroutine. Perbandingan penjadwal yang dipertimbangkan dalam dua video yang disebutkan oleh Oleg Molchanov mencerminkan masalah yang sama dengan cukup jelas: algoritme penjadwalan soket di [8] terasa lebih rumit daripada algoritme carousel di [3] .



3. Menuju dunia tanpa coroutine



Karena kami yakin bahwa dunia tanpa coroutine adalah mungkin, menentangnya dengan automata, maka perlu untuk menunjukkan bagaimana tugas serupa telah diselesaikan oleh mereka. Mari kita peragakan ini menggunakan contoh yang sama bekerja dengan soket. Perhatikan bahwa implementasi awalnya ternyata tidak begitu sepele sehingga bisa langsung dipahami. Ini berulang kali ditekankan oleh penulis video itu sendiri. Yang lain menghadapi masalah serupa dalam konteks coroutine. Jadi, kelemahan coroutine terkait dengan kompleksitas persepsi, pemahaman, debugging, dll. dibahas dalam video [10] .



Pertama, beberapa kata tentang kompleksitas algoritme yang sedang dipertimbangkan. Hal ini disebabkan oleh sifat dinamis dan jamak dari proses layanan pelanggan. Untuk melakukan ini, server dibuat yang mendengarkan pada port tertentu dan, saat permintaan muncul, menghasilkan banyak fungsi layanan klien yang mencapainya. Karena ada banyak klien, mereka muncul tidak terduga, daftar dinamis dibuat dari proses soket layanan dan pertukaran informasi dengan mereka. Kode untuk solusi generator Python yang dibahas dalam video [8] ditunjukkan pada Listing 1.



Daftar 1. Soket pada generator
import socket
from select import select
tasks = []
to_read = {}
to_write = {}

def server():

    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server_socket.bind(('localhost', 5001))
    server_socket.listen()

    while True:
        yield ('read', server_socket)
        client_socket, addr = server_socket.accept()    
        print('Connection from', addr)
        tasks.append(client(client_socket, addr))       
    print('exit server')

def client(client_socket, addr):

    while True:
        yield ('read', client_socket)
        request = client_socket.recv(4096)              

        if not request:
            break
        else:
            response = 'Hello World\n'.encode()

            yield ('write', client_socket)

            client_socket.send(response)                
    client_socket.close()                               
    print('Stop client', addr)

def event_loop():
    while any([tasks, to_read, to_write]):

        while not tasks:

            ready_to_read, ready_to_write, _ = select(to_read, to_write, [])

            for sock in ready_to_read:
                tasks.append(to_read.pop(sock))

            for sock in ready_to_write:
                tasks.append(to_write.pop(sock))
        try:
            task = tasks.pop(0)

            reason, sock = next(task)   

            if reason == 'read':
                to_read[sock] = task
            if reason == 'write':
                to_write[sock] = task
        except StopIteration:
            print('Done!')
tasks.append(server())
event_loop()




Algoritme server dan klien cukup mendasar. Tetapi harus mengkhawatirkan bahwa server menempatkan fungsi klien dalam daftar tugas. Lebih jauh - lebih: sulit untuk memahami algoritma dari event loop event_loop. Hingga daftar tugas bisa kosong jika setidaknya proses server harus selalu ada di dalamnya? ..



Selanjutnya, kamus to_read dan to_write diperkenalkan. Pekerjaan dengan kamus membutuhkan penjelasan terpisah. ini lebih sulit daripada bekerja dengan daftar biasa. Karena itu, informasi yang dikembalikan oleh pernyataan hasil disesuaikan untuk mereka. Kemudian "menari dengan rebana" dimulai di sekitar kamus dan semuanya menjadi seperti "mendidih": sesuatu tampaknya ditempatkan di kamus, dari mana ia masuk ke daftar tugas, dll. dll. Anda bisa "mematahkan kepala", memilah-milah semua ini.



Dan seperti apa solusi dari tugas yang sedang dihadapi? Akan logis bagi automata untuk membuat model yang setara dengan proses bekerja dengan soket yang telah dibahas dalam video. Dalam model server, sepertinya tidak ada yang perlu diubah. Ini akan menjadi robot yang bekerja seperti fungsi server (). Grafiknya ditunjukkan pada Gambar. 1a. Tindakan automaton y1 () membuat soket server dan menghubungkannya ke port yang ditentukan. Predikat x1 () mendefinisikan koneksi klien, dan jika ada, tindakan y2 () membuat proses layanan soket klien, menempatkan yang terakhir dalam daftar proses kelas, yang menyertakan kelas objek aktif.



Dalam gambar. 1b menunjukkan grafik model untuk klien individu. Berada dalam status "0", robot menentukan kesiapan klien untuk mengirimkan informasi (predikat x1 () benar) dan menerima respons dalam tindakan y1 () pada transisi ke status "1". Selanjutnya, ketika klien siap untuk menerima informasi (sudah x2 () harus benar), tindakan y2 () mengimplementasikan operasi pengiriman pesan ke klien pada transisi ke status awal "0". Jika klien memutuskan koneksi dengan server (dalam hal ini, x3 () salah), maka automaton beralih ke status "4", menutup soket klien dalam tindakan y3 (). Proses tetap dalam status "4" hingga dikeluarkan dari daftar kelas aktif (lihat deskripsi model server di atas untuk pembentukan daftar).



Dalam gambar. 1c menunjukkan automaton yang mengimplementasikan peluncuran proses yang mirip dengan fungsi event_loop () di Listing 1. Hanya dalam kasus ini, algoritme operasinya jauh lebih sederhana. Itu semua bermuara pada fakta bahwa mesin menelusuri elemen-elemen dari daftar kelas aktif dan memanggil metode loop () untuk masing-masing kelas. Tindakan ini diimplementasikan oleh y2 (). Tindakan y4 () mengecualikan dari daftar kelas yang berada dalam status "4". Tindakan lainnya bekerja dengan indeks dari daftar objek: tindakan y3 () meningkatkan indeks, tindakan y1 () menyetel ulang.



Kemampuan pemrograman objek di Python berbeda dari pemrograman objek di C ++. Oleh karena itu, implementasi model otomat yang paling sederhana akan diambil sebagai dasar (tepatnya, ini adalah tiruan dari otomat). Ini didasarkan pada prinsip objek yang mewakili proses, di mana setiap proses sesuai dengan kelas aktif yang terpisah (mereka sering juga disebut agen). Kelas berisi properti dan metode yang diperlukan (lihat detail lebih lanjut tentang metode otomat tertentu - predikat dan tindakan di [9] ), dan logika otomat (fungsi transisi dan keluarnya) terkonsentrasi di dalam metode yang disebut loop (). Untuk mengimplementasikan logika perilaku robot, kita akan menggunakan konstruksi if-elif-else.



Dengan pendekatan ini, "event loop" tidak ada hubungannya dengan menganalisis ketersediaan soket. Mereka diperiksa oleh proses itu sendiri, yang menggunakan pernyataan pilih yang sama dalam predikat. Dalam situasi ini, mereka beroperasi dengan satu soket, dan bukan daftar mereka, memeriksanya untuk operasi yang diharapkan untuk soket khusus ini dan tepatnya dalam situasi yang ditentukan oleh algoritme operasi. Ngomong-ngomong, dalam proses debugging implementasi seperti itu, esensi pemblokiran yang tak terduga dari pernyataan pilih muncul.



Angka: 1. Grafik proses otomatis untuk bekerja dengan soket
image



Kode 2 menunjukkan kode objek automaton dengan Python untuk bekerja dengan soket Ini adalah jenis kami "dunia tanpa coroutines". Ini adalah "dunia" dengan prinsip berbeda untuk merancang proses perangkat lunak. Hal ini ditandai dengan adanya model algoritme komputasi paralel (untuk lebih jelasnya, lihat [9] , yang merupakan perbedaan utama dan kualitatif antara teknologi pemrograman otomatis (AP) dan "teknologi coroutine".



Pemrograman otomat dengan mudah menerapkan prinsip asinkron dari desain program, proses paralelisme, dan pada saat yang sama segala sesuatu yang dapat dipikirkan oleh pikiran pemrogram. Artikel saya sebelumnya membahas hal ini secara lebih rinci, dimulai dengan deskripsi model struktural komputasi otomatis dan definisi formal hingga contoh penerapannya. Kode di atas dengan Python mendemonstrasikan implementasi otomatis prinsip coroutine dari coroutine, benar-benar tumpang tindih, melengkapi dan memperluasnya dengan model mesin state.



Daftar 2. Soket pada mesin
import socket
from select import select

timeout = 0.0; classes = []

class Server:
    def __init__(self): self.nState = 0;

    def x1(self):
        self.ready_client, _, _ = select([self.server_socket], [self.server_socket], [], timeout)
        return self.ready_client

    def y1(self):
        self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.server_socket.bind(('localhost', 5001))
        self.server_socket.listen()
    def y2(self):
        self.client_socket, self.addr = self.server_socket.accept()
        print('Connection from', self.addr)
        classes.append(Client(self.client_socket, self.addr))

    def loop(self):
        if (self.nState == 0):      self.y1();      self.nState = 1
        elif (self.nState == 1):
            if (self.x1()):         self.y2();      self.nState = 0

class Client:
    def __init__(self, soc, adr): self.client_socket = soc; self.addr = adr; self.nState = 0

    def x1(self):
        self.ready_client, _, _ = select([self.client_socket], [], [], timeout)
        return self.ready_client
    def x2(self):
        _, self.write_client, _ = select([], [self.client_socket], [], timeout)
        return self.write_client
    def x3(self): return self.request

    def y1(self): self.request = self.client_socket.recv(4096);
    def y2(self): self.response = 'Hello World\n'.encode(); self.client_socket.send(self.response)
    def y3(self): self.client_socket.close(); print('close Client', self.addr)

    def loop(self):
        if (self.nState == 0):
            if (self.x1()):                     self.y1(); self.nState = 1
        elif (self.nState == 1):
            if (not self.x3()):                 self.y3(); self.nState = 4
            elif (self.x2() and self.x3()):     self.y2(); self.nState = 0

class EventLoop:
    def __init__(self): self.nState = 0; self.i = 0

    def x1(self): return self.i < len(classes)

    def y1(self): self.i = 0
    def y2(self): classes[self.i].loop()
    def y3(self): self.i += 1
    def y4(self):
        if (classes[self.i].nState == 4):
            classes.pop(self.i)
            self.i -= self.i

    def loop(self):
        if (self.nState == 0):
            if (not self.x1()): self.y1();
            if (self.x1()):     self.y2(); self.y4(); self.y3();

namSrv = Server(); namEv = EventLoop()
while True:
    namSrv.loop(); namEv.loop()




Kode di Daftar 2 jauh lebih maju secara teknologi daripada kode di Daftar 1. Dan ini adalah kelebihan model penghitungan otomatis. Ini difasilitasi oleh integrasi perilaku otomat ke dalam model objek pemrograman. Akibatnya, logika perilaku proses otomat terkonsentrasi tepat di tempat ia dihasilkan, dan tidak didelegasikan, seperti yang dipraktikkan di coroutine, ke dalam loop peristiwa kontrol proses. Solusi baru memprovokasi pembuatan "event loop" universal, prototipe yang dapat dianggap sebagai kode kelas EventLoop.



4. Tentang prinsip SRP dan DRY



Prinsip "tanggung jawab tunggal" - SRP (Prinsip Tanggung Jawab Tunggal) dan "jangan ulangi diri Anda sendiri" - KERING (jangan mengulangi diri sendiri) disuarakan dalam konteks video lain oleh Oleg Molchanov [11] . Menurut mereka, fungsi tersebut harus hanya berisi kode target agar tidak melanggar prinsip SRY, dan tidak mendorong pengulangan "kode tambahan" agar tidak melanggar prinsip KERING. Untuk tujuan ini, diusulkan untuk menggunakan dekorator. Tetapi ada solusi lain - solusi otomatis.



Di artikel sebelumnya [2]tidak menyadari adanya prinsip-prinsip seperti itu, contoh diberikan menggunakan dekorator. Dianggap sebagai penghitung, yang, omong-omong, dapat menghasilkan daftar jika diinginkan. Objek stopwatch yang mengukur waktu berjalan penghitung disebutkan. Jika objek sesuai dengan prinsip SRP dan DRY, maka fungsinya tidak sepenting protokol komunikasi. Dalam implementasinya, kode penghitung tidak ada hubungannya dengan kode stopwatch, dan mengubah salah satu objek tidak akan mempengaruhi yang lain. Mereka hanya terikat oleh protokol, yang disepakati objek "di pantai" dan kemudian dengan ketat mengikutinya.



Jadi, model robot paralel pada dasarnya mengesampingkan kemampuan dekorator. Lebih fleksibel dan lebih mudah untuk menerapkan kemampuan mereka, karena tidak "mengelilingi" (tidak menghiasi) kode fungsi. Untuk tujuan penilaian objektif dan perbandingan robot dan teknologi konvensional, Daftar 3 menunjukkan analog objek penghitung yang dibahas dalam artikel sebelumnya [2] , di mana versi yang disederhanakan dengan waktu pelaksanaannya dan versi asli penghitung disajikan setelah komentar.



Daftar 3. Implementasi penghitung otomatis
import time
# 1) 110.66 sec
class PCount:
    def __init__(self, cnt ): self.n = cnt; self.nState = 0
    def x1(self): return self.n > 0
    def y1(self): self.n -=1
    def loop(self):
        if (self.nState == 0 and self.x1()):
            self.y1();
        elif (self.nState == 0 and not self.x1()):  self.nState = 4;

class PTimer:
    def __init__(self, p_count):
        self.st_time = time.time(); self.nState = 0; self.p_count = p_count
#    def x1(self): return self.p_count.nStat == 4 or self.p_count.nState == 4
    def x1(self): return self.p_count.nState == 4
    def y1(self):
        t = time.time() - self.st_time
        print ("speed CPU------%s---" % t)
    def loop(self):
       if (self.nState == 0 and self.x1()): self.y1(); self.nState = 1
       elif (self.nState == 1): pass

cnt1 = PCount(1000000)
cnt2 = PCount(10000)
tmr1 = PTimer(cnt1)
tmr2 = PTimer(cnt2)
# event loop
while True:
    cnt1.loop(); tmr1.loop()
    cnt2.loop(); tmr2.loop()

# # 2) 73.38 sec
# class PCount:
#     def __init__(self, cnt ): self.n = cnt; self.nState = 0
#     def loop(self):
#         if (self.nState == 0 and self.n > 0): self.n -= 1;
#         elif (self.nState == 0 and not self.n > 0):  self.nState = 4;
# 
# class PTimer:
#     def __init__(self): self.st_time = time.time(); self.nState = 0
#     def loop(self):
#        if (self.nState == 0 and cnt.nState == 4):
#            t = time.time() - self.st_time
#            print("speed CPU------%s---" % t)
#            self.nState = 1
#        elif (self.nState == 1): exit()
# 
# cnt = PCount(100000000)
# tmr = PTimer()
# while True:
#     cnt.loop();
#     tmr.loop()

# # 3) 35.14 sec
# class PCount:
#     def __init__(self, cnt ): self.n = cnt; self.nState = 0
#     def loop(self):
#         if (self.nState == 0 and self.n > 0):
#             self.n -= 1;
#             return True
#         elif (self.nState == 0 and not self.n > 0):  return False;
#
# cnt = PCount(100000000)
# st_time = time.time()
# while cnt.loop():
#     pass
# t = time.time() - st_time
# print("speed CPU------%s---" % t)

# # 4) 30.53 sec
# class PCount:
#     def __init__(self, cnt ): self.n = cnt; self.nState = 0
#     def loop(self):
#         while self.n > 0:
#             self.n -= 1;
#             return True
#         return False
#
# cnt = PCount(100000000)
# st_time = time.time()
# while cnt.loop():
#     pass
# t = time.time() - st_time
# print("speed CPU------%s---" % t)

# # 5) 18.27 sec
# class PCount:
#     def __init__(self, cnt ): self.n = cnt; self.nState = 0
#     def loop(self):
#         while self.n > 0:
#             self.n -= 1;
#         return False
# 
# cnt = PCount(100000000)
# st_time = time.time()
# while cnt.loop():
#     pass
# t = time.time() - st_time
# print("speed CPU------%s---" % t)

# # 6) 6.96 sec
# def count(n):
#   st_time = time.time()
#   while n > 0:
#     n -= 1
#   t = time.time() - st_time
#   print("speed CPU------%s---" % t)
#   return t
#
# def TestTime(fn, n):
#   def wrapper(*args):
#     tsum=0
#     st = time.time()
#     i=1
#     while (i<=n):
#       t = fn(*args)
#       tsum +=t
#       i +=1
#     return tsum
#   return wrapper
#
# test1 = TestTime(count, 2)
# tt = test1(100000000)
# print("Total ---%s seconds ---" % tt)




Mari kita rangkum waktu operasi berbagai opsi dalam tabel dan mengomentari hasil pekerjaan.



  1. Realisasi robot klasik - 110,66 dtk
  2. Implementasi automata tanpa metode automata - 73,38 dtk
  3. Tanpa stopwatch otomatis - 35.14
  4. Penghitung dalam bentuk sedangkan dengan keluaran pada setiap iterasi - 30,53
  5. Penghitung dengan siklus pemblokiran - 18.27
  6. Penghitung asli dengan dekorator - 6.96


Opsi pertama, mewakili model penghitung otomatis secara penuh, mis. penghitung itu sendiri dan stopwatch memiliki waktu berjalan terlama. Waktu pengoperasian dapat dikurangi dengan melepaskan, boleh dikatakan, prinsip-prinsip teknologi otomatis. Sesuai dengan ini, pada opsi 2, panggilan ke predikat dan tindakan diganti dengan kodenya. Dengan cara ini kami menghemat waktu pada operator panggilan metode dan ini cukup terlihat, mis. lebih dari 30 detik, mengurangi waktu pengoperasian.



Kami menghemat sedikit lebih banyak di opsi ke-3, membuat implementasi penghitung yang lebih sederhana, tetapi dengan keluar darinya di setiap iterasi siklus penghitung (meniru operasi coroutine). Dengan menghilangkan penghentian penghitung (lihat opsi 5), kami mencapai pengurangan terkuat dalam pekerjaan penghitung. Tetapi pada saat yang sama, kami kehilangan keuntungan dari pekerjaan coroutine. Opsi 6 - ini adalah penghitung asli dengan dekorator yang sudah berulang dan memiliki waktu berjalan terpendek. Tapi, seperti opsi 5, ini adalah implementasi pemblokiran, yang tidak cocok untuk kita dalam konteks membahas operasi fungsi coroutine.



5. Kesimpulan



Apakah akan menggunakan teknologi automata atau kepercayaan coroutines - keputusan sepenuhnya ada di tangan programmer. Penting bagi kami di sini bahwa dia tahu bahwa ada pendekatan / teknologi yang berbeda dari coroutine untuk desain program. Anda bahkan bisa membayangkan opsi eksotis berikut. Pertama, pada tahap desain model, dibuat model solusi otomat. Ini sangat ilmiah, berbasis bukti, dan didokumentasikan dengan baik. Kemudian, sebagai contoh, untuk meningkatkan kinerja, itu adalah "disfigured" ke versi "normal" dari kode, seperti yang ditunjukkan oleh Listing 3. Anda bahkan dapat membayangkan "pemfaktoran ulang terbalik" dari kode; transisi dari pilihan ke-7 ke 1, tapi ini, meskipun mungkin, tapi kemungkinan kejadian yang paling kecil :)



Dalam gbr. 2 menunjukkan slide dari video tentang topik "asynchronous" [10]... Dan "buruk" tampaknya lebih besar daripada "baik". Dan jika menurut saya automata selalu bagus, maka dalam kasus pemrograman asinkron, pilih, seperti yang mereka katakan, sesuai selera Anda. Tapi sepertinya opsi "buruk" akan menjadi yang paling mungkin. Dan programmer harus mengetahui hal ini terlebih dahulu saat merancang sebuah program.



Angka: 2. Karakteristik pemrograman asynchronous
image



Tentu saja, kode otomatnya agak "bukan tanpa dosa". Ini akan memiliki jumlah kode yang sedikit lebih besar. Tapi, pertama, itu lebih terstruktur dan oleh karena itu lebih mudah dipahami dan lebih mudah dipelihara. Dan kedua, tidak selalu lebih besar, karena dengan meningkatnya kompleksitas, kemungkinan besar bahkan akan ada hasil (karena, misalnya, metode automata yang digunakan kembali). Mendebug lebih mudah dan lebih jelas. Ya, pada akhirnya itu benar-benar SRP dan KERING. Dan ini, kadang-kadang, melebihi banyak hal.



Akan diinginkan, dan bahkan mungkin perlu, untuk memperhatikan, katakanlah, standar untuk desain fungsi. Programmer harus, sejauh mungkin, menghindari perancangan fungsi pemblokiran. Untuk melakukan ini, ia hanya harus memulai proses komputasi, yang kemudian diperiksa kelengkapannya, atau memiliki sarana untuk memeriksa kesiapan untuk memulai, seperti fungsi pemilihan yang dipertimbangkan dalam contoh. Kode yang menggunakan fungsi-fungsi yang berasal dari masa DOS, yang ditunjukkan pada Listing 4, menunjukkan bahwa masalah seperti itu memiliki riwayat "pra-rutin" yang panjang.



Daftar 4. Membaca karakter dari keyboard
/*
int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    int C=0;
    while (C != 'e')
    {
        C = getch();
        putchar (C);
    }
    return a.exec();
}
*/
//*
int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    int C=0;
    while (C != 'e')
    {
        if (kbhit()) {
            C = getch();
            putch(C);
        }
    }

    return a.exec();
}
//*/




Berikut dua opsi untuk membaca karakter dari keyboard. Opsi pertama adalah memblokir. Ini akan memblokir komputasi dan tidak akan menjalankan pernyataan untuk mengeluarkan karakter sampai fungsi getch () menerimanya dari keyboard. Pada varian kedua, fungsi yang sama akan diluncurkan hanya pada saat yang tepat, ketika fungsi yang dipasangkan kbhit () mengonfirmasi bahwa karakter tersebut ada dalam buffer input. Dengan demikian, tidak akan ada pemblokiran perhitungan.



Jika fungsinya "berat" dengan sendirinya, yaitu membutuhkan banyak waktu untuk bekerja, dan keluar secara berkala darinya oleh jenis pekerjaan coroutine (ini dapat dilakukan tanpa menggunakan mekanisme coroutine yang sama, agar tidak mengikatnya) sulit untuk dilakukan atau tidak masuk akal, maka tetap menempatkan fungsi tersebut di utas terpisah dan kemudian mengontrol penyelesaian pekerjaan mereka (lihat implementasi kelas QCount di [2]).



Anda selalu dapat menemukan jalan keluar untuk mengecualikan pemblokiran komputasi. Di atas, kami menunjukkan bagaimana Anda dapat membuat kode asinkron dalam kerangka kerja bahasa yang biasa, tanpa menggunakan mekanisme coroutine / coroutine dan bahkan lingkungan khusus seperti lingkungan pemrograman otomatis VKP (a). Dan apa dan bagaimana menggunakan terserah programmer untuk memutuskan.



literatur



1. Podcast Python Junior. Tentang asynchrony di python. [Sumber daya elektronik], Mode akses: www.youtube.com/watch?v=Q2r76grtNeg , gratis. Bahasa. Rusia (tanggal pengobatan 13/07/2020).

2. Konkurensi dan efisiensi: Python vs FSM. [Sumber daya elektronik], Mode akses: habr.com/ru/post/506604 , gratis. Bahasa. Rusia (tanggal pengobatan 13/07/2020).

3. Molchanov O. Dasar-dasar asinkron dengan Python # 4: Generator dan event loop Round Robin. [Sumber daya elektronik], Mode akses: www.youtube.com/watch?v=PjZUSSkGLE8 ], gratis. Bahasa. Rusia (tanggal pengobatan 13/07/2020).

4. 48 Generator dan iterator. Ekspresi generator dengan Python. [Sumber daya elektronik], Mode akses: www.youtube.com/watch?v=vn6bV6BYm7w, Gratis. Bahasa. Rusia (tanggal pengobatan 13/07/2020).

5. Memoisasi dan kari (Python). [Sumber daya elektronik], Mode akses: habr.com/ru/post/335866 , gratis. Yaz. Rusia (tanggal pengobatan 13/07/2020).

6. Lyubchenko V.S. Tentang menangani rekursi. "PC World", No. 11/02. www.osp.ru/pcworld/2002/11/164417

7. Molchanov O. Python Cast # 10 - Apakah hasil. [Sumber daya elektronik], Mode akses: www.youtube.com/watch?v=ZjaVrzOkpZk , gratis. Bahasa. Rusia (tanggal pengobatan 18/7/2020).

8. Molchanov O. Dasar-dasar async dengan Python # 5: Async pada generator. [Sumber daya elektronik], Mode akses: www.youtube.com/watch?v=hOP9bKeDOHs , gratis. Yaz. Rusia (tanggal pengobatan 13/07/2020).

9. Model komputasi paralel. [Sumber daya elektronik], Mode akses: habr.com/ru/post/486622 , gratis. Yaz. Rusia (tanggal pengobatan 07/20/2020).

10. Polishchuk A. Asinkronisme dengan Python. [Sumber daya elektronik], Mode akses: www.youtube.com/watch?v=lIkA0TDX8tE , gratis. Yaz. Rusia (tanggal pengobatan 13/07/2020).

11. Molchanov O. Lessons Python cast # 6 - Dekorator. [Sumber daya elektronik], Mode akses: www.youtube.com/watch?v=Ss1M32pp5Ew , gratis. Bahasa. Rusia (tanggal pengobatan 13/07/2020).



All Articles