Mesin acara menjaga siklus hidup

Penafian: Artikel ini menjelaskan solusi yang tidak jelas untuk masalah yang tidak jelas. Sebelum bergegas telur mempraktikkannya, saya sarankan membaca artikel sampai akhir dan berpikir dua kali.

but_why


Halo semuanya! Ketika bekerja dengan kode, kita sering harus berurusan dengan keadaan . Salah satu contohnya adalah siklus hidup benda. Mengelola objek dengan beberapa status yang mungkin bisa menjadi tugas yang sangat sepele. Tambahkan eksekusi asinkron di sini dan tugas menjadi rumit oleh urutan besarnya. Ada solusi yang efektif dan alami. Pada artikel ini saya akan berbicara tentang mesin acara dan cara menerapkannya di Go.


Mengapa mengatur negara?


Untuk memulainya, mari kita mendefinisikan konsep itu sendiri. Contoh paling sederhana dari keadaan: file dan berbagai koneksi. Anda tidak bisa hanya mengambil dan membaca file. Pertama harus dibuka, dan pada akhirnya lebih disukai pastikan untuk menutup. Ternyata tindakan saat ini tergantung pada hasil dari tindakan sebelumnya: membaca tergantung pada pembukaan. Hasil yang disimpan adalah keadaan.


Masalah utama dengan negara adalah kompleksitas. Setiap negara secara otomatis menyulitkan kode. Anda harus menyimpan hasil tindakan dalam memori dan menambahkan berbagai pemeriksaan ke logika. Itulah mengapa arsitektur tanpa kewarganegaraan begitu menarik bagi para programmer - tidak ada yang mau masalah kesulitan. Jika hasil tindakan Anda tidak mempengaruhi logika eksekusi, Anda tidak perlu status.


Namun, ada satu properti yang membuat Anda memperhitungkan kesulitannya. Suatu negara mengharuskan Anda untuk mengikuti urutan tindakan tertentu. Secara umum, situasi seperti itu harus dihindari, tetapi ini tidak selalu memungkinkan. Contohnya adalah siklus hidup objek program. Berkat manajemen keadaan yang baik, seseorang dapat memperoleh perilaku objek yang dapat diprediksi dengan siklus hidup yang kompleks.


Sekarang mari kita cari tahu cara melakukannya dengan dingin .


Otomatis sebagai cara untuk menyelesaikan masalah


AK74


Ketika orang berbicara tentang negara, mesin negara yang terbatas segera muncul dalam pikiran. Itu logis, karena otomat adalah cara paling alami untuk mengelola keadaan.


Saya tidak akan mempelajari teori automata , ada lebih dari cukup informasi di Internet.

Jika Anda mencari contoh mesin negara berhingga untuk Go, Anda pasti akan bertemu dengan lexer dari Rob Pike . Sebuah contoh yang bagus dari otomat di mana data yang diproses adalah alfabet input. Ini berarti bahwa keadaan transisi disebabkan oleh teks yang diproses oleh lexer. Solusi elegan untuk masalah tertentu.


Hal utama yang harus dipahami adalah bahwa robot adalah solusi untuk masalah yang sangat spesifik. Karena itu, sebelum mempertimbangkannya sebagai obat untuk semua masalah, Anda harus sepenuhnya memahami tugas tersebut. Khususnya, entitas yang ingin Anda kontrol:


  • negara - siklus hidup;
  • peristiwa - apa yang sebenarnya menyebabkan transisi ke masing-masing negara;
  • hasil kerja - data keluaran;
  • mode eksekusi (sinkron / tidak sinkron);
  • kasus penggunaan utama.

Lexer itu indah, tetapi hanya berubah karena data yang diprosesnya sendiri. Tetapi bagaimana dengan situasi ketika pengguna mengaktifkan transisi? Di sinilah mesin acara dapat membantu.


Contoh nyata


Untuk membuatnya lebih jelas, saya akan menganalisis contoh dari perpustakaan phono .


Untuk perendaman lengkap dalam konteks, Anda dapat membaca artikel pengantar . Ini tidak perlu untuk topik ini, tetapi akan membantu untuk lebih memahami apa yang kami kelola.

Dan apa yang kita kelola?


phono didasarkan pada pipa DSP. Ini terdiri dari tiga tahap pemrosesan. Setiap tahap dapat mencakup dari satu hingga beberapa komponen:


pipe_diagram


  1. pipe.Pump (pompa Inggris) adalah tahap wajib menerima suara, selalu hanya satu komponen.
  2. pipe.Processor (pengendali bahasa Inggris) - tahap opsional pemrosesan suara, dari komponen 0 hingga N.
  3. pipe.Sink (English sink) - tahap wajib transmisi suara, dari komponen 1 ke N.

Sebenarnya, kami akan mengatur siklus hidup konveyor.


Siklus hidup


Ini adalah seperti apa bentuk pipe.Pipe Diagram keadaan pipe.Pipe .


pipe_lifecycle


Miring menunjukkan transisi yang disebabkan oleh logika eksekusi internal. Tebal - transisi yang disebabkan oleh peristiwa. Diagram menunjukkan bahwa keadaan dibagi menjadi 2 jenis:


  • status diam - ready dan paused , Anda hanya dapat melompat darinya berdasarkan acara
  • status aktif - running dan pausing , transisi berdasarkan peristiwa dan karena logika eksekusi

Sebelum analisis rinci kode, contoh yang jelas tentang penggunaan semua negara:


 // PlayWav  .wav    portaudio  -. func PlayWav(wavFile string) error { bufferSize := phono.BufferSize(512) //      w, err := wav.NewPump(wavFile, bufferSize) //  wav pump if err != nil { return err } pa := portaudio.NewSink( //  portaudio sink bufferSize, w.WavSampleRate(), w.WavNumChannels(), ) p := pipe.New( //  pipe.Pipe    ready w.WavSampleRate(), pipe.WithPump(w), pipe.WithSinks(pa), ) p.Run() //    running   p.Run() errc := p.Pause() //    pausing   p.Pause() err = pipe.Wait(errc) //     paused if err != nil { return err } errc = p.Resume() //    running   p.Resume() err = pipe.Wait(errc) //     ready if err != nil { return err } return pipe.Wait(p.Close()) //      } 

Sekarang, hal pertama yang pertama.


Semua kode sumber tersedia di repositori .

Negara dan Acara


Mari kita mulai dengan hal yang paling penting.


 // state      . type state interface { listen(*Pipe, target) (state, target) //    transition(*Pipe, eventMessage) (state, error) //   } // idleState  .        . type idleState interface { state } // activeState  .         //   . type activeState interface { state sendMessage(*Pipe) state //    } //  . type ( idleReady struct{} activeRunning struct{} activePausing struct{} idlePaused struct{} ) //  . var ( ready idleReady running activeRunning paused idlePaused pausing activePausing ) 

Berkat jenis yang terpisah, transisi juga dideklarasikan secara terpisah untuk setiap negara. Ini menghindari yang besar sosis fungsi transisi dengan switch bersarang. Negara bagian itu sendiri tidak mengandung data atau logika apa pun. Bagi mereka, Anda dapat mendeklarasikan variabel di tingkat paket sehingga Anda tidak melakukan ini setiap saat. Antarmuka state diperlukan untuk polimorfisme. activeState idleState berbicara tentang activeState dan idleState sedikit kemudian.


Bagian terpenting kedua dari mesin kami adalah acara.


 // event  . type event int //  . const ( run event = iota pause resume push measure cancel ) // target      . type target struct { state idleState //   errc chan error //   ,     } // eventMessage   ,    . type eventMessage struct { event //   params params //   components []string // id  target //      } 

Untuk memahami mengapa jenis target diperlukan, pertimbangkan contoh sederhana. Kami telah membuat conveyor baru, sudah ready . Sekarang jalankan dengan p.Run() . Acara run dikirim ke mesin, pipa masuk ke keadaan running . Bagaimana cara mengetahui kapan konveyor selesai? Di sinilah jenis target akan membantu kami. Ini menunjukkan keadaan istirahat apa yang diharapkan setelah acara. Dalam contoh kami, setelah pekerjaan selesai, pipa kembali akan memasuki status ready . Hal yang sama pada diagram:



Sekarang lebih banyak tentang jenis negara. Lebih tepatnya, tentang activeState dan activeState . Mari kita lihat fungsi listen(*Pipe, target) (state, target) untuk berbagai jenis tahapan:


 // listen     ready. func (s idleReady) listen(p *Pipe, t target) (state, target) { return p.idle(s, t) } // listen     running. func (s activeRunning) listen(p *Pipe, t target) (state, target) { return p.active(s, t) } 

pipe.Pipe memiliki fungsi yang berbeda untuk menunggu transisi! Ada apa disana


 // idle     .    . func (p *Pipe) idle(s idleState, t target) (state, target) { if s == t.state || s == ready { t = t.dismiss() //  ,  target } for { var newState state var err error select { case e := <-p.events: //   newState, err = s.transition(p, e) //    if err != nil { e.target.handle(err) } else if e.hasTarget() { t.dismiss() t = e.target } } if s != newState { return newState, t // ,    } } } // active     .     , //   . func (p *Pipe) active(s activeState, t target) (state, target) { for { var newState state var err error select { case e := <-p.events: //   newState, err = s.transition(p, e) //    if err != nil { //  ? e.target.handle(err) // ,    } else if e.hasTarget() { // ,  target t.dismiss() //   t = e.target //   } case <-p.provide: //     newState = s.sendMessage(p) //    case err, ok := <-p.errc: //   if ok { //   ,  interrupt(p.cancel) //   t.handle(err) //    } //    ,  return ready, t //    ready } if s != newState { return newState, t // ,    } } } 

Dengan demikian, kita dapat mendengarkan saluran yang berbeda di berbagai negara. Misalnya, ini memungkinkan Anda untuk tidak mengirim pesan saat jeda - kami hanya tidak mendengarkan saluran yang sesuai.


Konstruktor dan mulai dari mesin



 // New      . //      ready. func New(sampleRate phono.SampleRate, options ...Option) *Pipe { p := &Pipe{ UID: phono.NewUID(), sampleRate: sampleRate, log: log.GetLogger(), processors: make([]*processRunner, 0), sinks: make([]*sinkRunner, 0), metrics: make(map[string]measurable), params: make(map[string][]phono.ParamFunc), feedback: make(map[string][]phono.ParamFunc), events: make(chan eventMessage, 1), //    cancel: make(chan struct{}), //     provide: make(chan struct{}), consume: make(chan message), } for _, option := range options { //   option(p)() } go p.loop() //    return p } 

Selain opsi inisialisasi dan fungsional , ada awal goroutine terpisah dengan siklus utama. Nah, lihat dia:


 // loop ,     nil . func (p *Pipe) loop() { var s state = ready //   t := target{} for s != nil { s, t = s.listen(p, t) //      p.log.Debug(fmt.Sprintf("%v is %T", p, s)) } t.dismiss() close(p.events) //    } // listen     ready. func (s idleReady) listen(p *Pipe, t target) (state, target) { return p.idle(s, t) } // transition       . func (s idleReady) transition(p *Pipe, e eventMessage) (state, error) { switch e.event { case cancel: interrupt(p.cancel) return nil, nil case push: e.params.applyTo(p.ID()) p.params = p.params.merge(e.params) return s, nil case measure: for _, id := range e.components { e.params.applyTo(id) } return s, nil case run: if err := p.start(); err != nil { return s, err } return running, nil } return s, ErrInvalidState } 

Konveyor dibuat dan membeku untuk mengantisipasi peristiwa.


Waktunya bekerja


Hubungi p.Run() !



 // Run   run  . //     pipe.Close  . func (p *Pipe) Run() chan error { runEvent := eventMessage{ event: run, target: target{ state: ready, //    errc: make(chan error, 1), }, } p.events <- runEvent return runEvent.target.errc } // listen     running. func (s activeRunning) listen(p *Pipe, t target) (state, target) { return p.active(s, t) } // transition       . func (s activeRunning) transition(p *Pipe, e eventMessage) (state, error) { switch e.event { case cancel: interrupt(p.cancel) err := Wait(p.errc) return nil, err case measure: e.params.applyTo(p.ID()) p.feedback = p.feedback.merge(e.params) return s, nil case push: e.params.applyTo(p.ID()) p.params = p.params.merge(e.params) return s, nil case pause: return pausing, nil } return s, ErrInvalidState } // sendMessage   . func (s activeRunning) sendMessage(p *Pipe) state { p.consume <- p.newMessage() return s } 

running menghasilkan pesan dan berjalan sampai pipa selesai.


Jeda


Selama pelaksanaan conveyor, kita dapat menghentikannya. Dalam keadaan ini, pipeline tidak akan menghasilkan pesan baru. Untuk melakukan ini, panggil metode p.Pause() .



 // Pause   pause  . //     pipe.Close  . func (p *Pipe) Pause() chan error { pauseEvent := eventMessage{ event: pause, target: target{ state: paused, //    errc: make(chan error, 1), }, } p.events <- pauseEvent return pauseEvent.target.errc } // listen     pausing. func (s activePausing) listen(p *Pipe, t target) (state, target) { return p.active(s, t) } // transition       . func (s activePausing) transition(p *Pipe, e eventMessage) (state, error) { switch e.event { case cancel: interrupt(p.cancel) err := Wait(p.errc) return nil, err case measure: e.params.applyTo(p.ID()) p.feedback = p.feedback.merge(e.params) return s, nil case push: e.params.applyTo(p.ID()) p.params = p.params.merge(e.params) return s, nil } return s, ErrInvalidState } // sendMessage   .   -, //      .    //    ,      .  , // ,   , : // 1.     // 2.      func (s activePausing) sendMessage(p *Pipe) state { m := p.newMessage() if len(m.feedback) == 0 { m.feedback = make(map[string][]phono.ParamFunc) } var wg sync.WaitGroup //     wg.Add(len(p.sinks)) //   Sink for _, sink := range p.sinks { param := phono.ReceivedBy(&wg, sink.ID()) // - m.feedback = m.feedback.add(param) } p.consume <- m //   wg.Wait() // ,     return paused } 

Segera setelah semua penerima menerima pesan, saluran pipa akan paused . Jika pesan adalah yang terakhir, maka transisi ke status ready akan terjadi.


Kembali bekerja!


Untuk keluar dari negara yang paused , panggil p.Resume() .



 // Resume   resume  . //     pipe.Close  . func (p *Pipe) Resume() chan error { resumeEvent := eventMessage{ event: resume, target: target{ state: ready, errc: make(chan error, 1), }, } p.events <- resumeEvent return resumeEvent.target.errc } // listen     paused. func (s idlePaused) listen(p *Pipe, t target) (state, target) { return p.idle(s, t) } // transition       . func (s idlePaused) transition(p *Pipe, e eventMessage) (state, error) { switch e.event { case cancel: interrupt(p.cancel) err := Wait(p.errc) return nil, err case push: e.params.applyTo(p.ID()) p.params = p.params.merge(e.params) return s, nil case measure: for _, id := range e.components { e.params.applyTo(id) } return s, nil case resume: return running, nil } return s, ErrInvalidState } 

Semuanya sepele di sini, saluran pipa kembali running .


Meringkuk


Konveyor dapat dihentikan dari kondisi apa pun. Ada p.Close() .



 // Close   cancel  . //      . //    ,   . func (p *Pipe) Close() chan error { resumeEvent := eventMessage{ event: cancel, target: target{ state: nil, //   errc: make(chan error, 1), }, } p.events <- resumeEvent return resumeEvent.target.errc } 

Siapa yang butuh ini?


Tidak untuk semua orang. Untuk memahami persis bagaimana mengelola negara, Anda harus memahami tugas Anda. Tepat ada dua keadaan di mana Anda dapat menggunakan mesin asinkron berbasis peristiwa:


  1. Siklus hidup yang kompleks - ada tiga negara atau lebih dengan transisi non-linear.
  2. Eksekusi asinkron digunakan.

Meskipun mesin acara menyelesaikan masalah, itu adalah pola yang agak rumit. Karena itu, harus digunakan dengan sangat hati-hati dan hanya setelah pemahaman yang lengkap tentang semua pro dan kontra.


Referensi


Source: https://habr.com/ru/post/id431048/


All Articles