Threads và asynchronous environments ban đầu hơi tricky. Không có good mental model để organize interaction, dễ dàng get into trouble và end up với unexpected results. Ngoài ra, testing asynchronous code có thể difficult without right tools hoặc test patterns.
Nghĩ về threads như people và shared objects như 'things' có thể owned giúp organize working của multithreaded system. Trong episode này chúng ta sẽ đi qua ví dụ để học all about testing asynchronous Ruby code.
Nếu bạn dùng Rails hoặc Rack hoặc really any application có web browser front end bạn đang ở asynchronous environment. Rack #call luôn được gọi asynchronously. Vì vậy dù bạn biết hay không, có good chance bạn đã dùng multithreaded components.
Testing: Trigger, collect and check (Kích hoạt, thu thập và kiểm tra)
Testing asynchronous callback API có thể made synchronous bằng follow pattern ba bước; trigger, collect, và check. Nghĩ mỗi thread như separate individual và objects như, well, things có thể owned bởi just one individual tại một thời điểm.
Chúng ta sẽ dùng ví dụ Batman và 7 suits khác nhau. Vì đó là practical example và chúng ta hiểu importance của việc biết whether all suits với Alfred để giặt khi bạn sắp chạy ra cứu thành phố.
Ví dụ: Laundry day at the batcave (Ngày giặt ở hang dơi)
Ví dụ là Alfred giặt suits của Batman. SuitWashScheduler là scheduler invoke callback cho mỗi washing event. Scheduler làm bảy callbacks at one-second intervals bắt đầu one second sau start. Trigger là creation của SuitWashScheduler.
class SuitWashScheduler def initialize(cnt) Thread.new { cnt.times { sleep(1.0) yield } } end endCollecting (Thu thập)
Collecting results phải thread safe để avoid race conditions. Object shared across more than one thread phải được protected. Protection là cách keep track of owner của object. Chỉ owner có thể make changes hoặc view object. Suit chỉ có thể với Batman để dùng trong fights, hoặc với Alfred để giặt.
Để stay friendly thread (trong metaphor Batman hoặc Alfred) chỉ take ownership cho short times và rồi give up ownership. Mutex thường dùng để keep track of owner. SuitwashScheduler callback sẽ own result counter khi counter được increment. Callback chạy trong thread SuitWashScheduler signal rằng all results đã received khi counter hit target.
Viết ví dụ bắt đầu với setup some globals. Trong real application globals sẽ được replace bởi class hoặc object attributes.
$main_thread = Thread.current $mu = Mutex.new $count = 0 $target = 7Management và owners (Quản lý và chủ sở hữu)
$main_thread và $mu dùng để manage threads và wait cho completion của test trong khi $target và $count track test results. Nhớ đây là trivial test nên collecting và checking results phải simple.
Test bắt đầu bằng tạo instance mới của SuitWashScheduler, cho initializer $target number of iterations. Trong trường hợp này, 7 suits cần giặt. Block provided sẽ chạy trong thread SuitWashScheduler. Mỗi iteration $count được increment và printed.
Looking ahead chúng ta realize main, test thread sẽ check $count cũng nghĩa là nó cần ownership của $count cũng vậy nên means of taking ownership của $count needed. $mu Mutex instance là ownership token. Trong block pass cho SuitWashScheduler.new call một $mu.synchronize block take ownership đủ lâu để set $count và check results. More on results check in a moment.
SuitWashScheduler.new($target) { $mu.synchronize { $count += 1 puts $count $main_thread.wakeup if $target <= $count } }Check: are all suits done? (Tất cả suits đã xong chưa?)
Quay lại main thread chúng ta cần wait cho test finish. Batman cần wait trước khi all 7 suits done. Có hai condition check; test update $count as expected hoặc Batman gets bored waiting cho test finish và times out. Trước khi check $count xem có reach $target không, ownership của $count needed. Giống như trong block cho SuitWashScheduler call $mu.synchronize được dùng.
Nhưng đó không thể đúng, nếu chúng ta lock up main thread làm sao SuitWashScheduler thread ever change $count? May mắn cho chúng ta có neat trick lo chuyện này. Mutex class có #sleep method give up ownership và wait cho đến khi timeout hoặc được woken. Once woken either through timeout hoặc #wakeup call to main thread $mu attempt take ownership again trước khi continue. Once ownership achieved results có thể checked và determination của pass hoặc fail state của test có thể made.
$mu.synchronize { $mu.sleep($target + 1) if $target != $count puts 'FAILED' else puts 'Passed! All suits are washed and clean' end }Nếu bạn muốn deeper vào này, bạn có thể làm ví dụ interesting hơn bằng thử tạo multiple schedulers và xem Mutex keep $count changes từ colliding. Giống như Batman gửi some suits cho Alfred giặt, và some others cho dry cleaners. Đảm bảo đổi logic để $target check là total của all expected yields.
Roundup (Tổng kết)
Làm việc với threads và asynchronous environment dễ hơn với right mental model. Trong post này chúng ta dùng people as metaphor cho threads và physical objects (suits) as metaphor cho shared objects, chỉ có thể owned bởi one thread hoặc person tại một thời điểm. Chúng ta nghĩ cách này để abstract makes it easier to understand and remember.
Hy vọng examples làm bạn remember mechanisms của async, nhưng hy vọng image của Batman chạy ra ngoài trong khi all suits đang được giặt won't stick với bạn quá lâu.
P.S. Nếu bạn done với all the batman metaphors on the blog, let us know.