Thread signalling

Mục đích của thread signaling là để cho phép thread gửi signal đến các thread khác. Thêm vào đó, thread signaling cho phép một hoặc nhiều thread chờ signal từ một hoặc nhiều thread khác.

Một ví dụ đơn giản là, thread B chờ signal từ thread A thông báo rằng dữ liệu đã sẵn sàng để được xử lý.

  1. Gửi signal thông qua shared object

Một cách đơn giản đề gửi signal giữa các thread là sử dụng shared object.

Thread A có thể set giá trị của một biến member của một shared object (ví dụ như hasDataToProcess) thành true ở bên trong synchronized block, và thread B có thể đọc giá trị của member của shared object đó, tất nhiên cũng phải ở trong synchronized block.

https://gist.github.com/namchuai/9a7f1250dcbe820d3baa5621cd956d98

Thread A và thread B phải cùng có reference tới instance của MySignal để có thể thực hiện truyền tín hiệu (thread signaling). Tất nhiên là nếu reference của thread A và thread B trỏ tới hai instance khác nhau của MySignal thì sẽ không thực hiện được việc truyền tín hiệu.

  1. Busy wait

Thread B sẽ chờ cho tới khi dữ liệu sẵn sàng để được xử lý. Nói cách khác, chờ tín hiệu từ thread A.

https://gist.github.com/namchuai/d9e1e11b42a2e85670371420f08f5deb

Tức là thread B vẫn tiếp tục hoạt động trong khi đợi tín hiệu từ thread A. Điều này gọi là busy waiting. Giống như tên gọi của phương pháp này, thread B sẽ bị giữ bên trong hàm while cho tới khi thread A set lại giá trị của MySignal.hasDataToProcess

  1. wait(), notify() và notifyAll()

Trong khi Busy Waiting có thể giữ cho thread B chờ, nhưng thread B vẫn phải chiếm được CPU để thực thi. Điều này gây ảnh hưởng tới hiệu năng của hệ thống.

Method sleep() là một trong những phương án tốt để tạm dừng hoạt động của thread B. sleep() sẽ đưa thread B vào trạng thái BLOCKED cho đến khi có signal gọi thread B trở lại trạng thái RUNNABLE.

Java cung cấp cơ chế cho phép thread trở nên inactive trong khi chờ tín hiệu. Class java.lang.Object cung cấp ba phương thức: wait(), notify() và notifyAll() để làm việc này.

Một thread khi gọi wait() trên một object nào đó sẽ trở nên inactive cho tới khi một thread khác gọi notify() trên object đó. Để gọi wait() hoặc notify(), thead cần phải lấy được khóa ở trên object đó. Nói cách khác, thread sẽ phải gọi wait() hoặc notify() bên trong synchronized block.

https://gist.github.com/namchuai/8f24ebb2801edeb15aec15317c9467f2

Thread cần đợi tín hiệu sẽ gọi doWait(), và thread gửi tín hiệu sẽ gọi doNotify(). Khi một thread gọi notify() trên một object, một trong số những thread đang đợi trên object đó sẽ được đánh thức và cho phép khởi chạy.

Ngoài ra, ta còn có method notifyAll(). Method này sẽ đánh thức tất cả những thread đang đợi trên object đó.

Cả hai thread gửi và nhận tín hiệu đều gọi wait() và notify() bên trong synchronized block. Điều này là bắt buộc! Một thread không thể gọi wait() hoặc notify() hoặc notifyAll() mà không giữ lock của object được gọi. Nếu vẫn cố gắng, IllegalMonitorStateException sẽ bị ném ra.

Nhưng, bằng cách nào? Chẳng phải thread chờ tín hiệu giữ lock của monitor object (myMonitorObject) trong suốt quá trình thread đó nằm trong synchronized block? Tại sao thread chờ tín hiệu không block thread gửi tín hiệu khỏi việc đi vào trong synchronized block ở trong doNotify()?

Câu trả lời là không. Khi một thread gọi wait(), thread đó nhả lock. Điều này cho phép các thread khác gọi wait() và notify().

Một khi thread được đánh thức, nó không thể thoát khỏi wait() cho đến khi thread gửi tín hiệu gọi notify() thoát khỏi synchronized block. Nói cách khác, thread sau khi tỉnh lại cần phải chiếm được lock trước khi có thể thoát khỏi synchronized block. Bởi vì wait() được gọi bên trong synchronized block.

Nếu nhiều thread được gọi dậy bởi notifyAll(), sẽ chỉ có một thread ở một thời điểm, có thể thoát khỏi wait(), bởi mỗi thread cần phải có lock trước khi thoát khỏi wait().

  1. Missed signals

Method notify() và notifyAll() không lưu method gọi tới chúng trong trường hợp không có thread nào đang đợi khi chúng được gọi. Lúc đó tín hiệu sẽ mất. Vì vậy, nếu một thread gửi tín hiệu gọi notify() trước khi thread nhận gọi wait(), tín hiệu sẽ bị mất.

Trong nhiều trường hợp, điều này sẽ dẫn tới deadlock. Thread nhận tín hiệu sẽ chờ đợi tín hiệu mãi mãi.

Để tránh trường hợp này, tín hiệu nên được lưu lại.

https://gist.github.com/namchuai/ba7c76921d244a15c92b6f05f5f61b32

  1. Spurious wakeups (thức dậy giả mạo)

Có khả năng một thread wake up kể cả khi notify() và notifyAll() chưa được gọi. Trường hợp này gọi là spurious wakeup. Wakeup mà không bởi lý do nào.

Nếu spurious wakeup xảy ra trong MyWaitNotify2 trong hàm doWait() thì thread đang chờ tín hiệu có thể tiếp tục xử lý cho dù không nhận được tín hiệu.

Để chắc chắn spurious wakeup không xảy ra, ta sẽ check bên trong vòng while thay vì để bên trong if. Vòng lặp while được sử dụng trong trường hợp này có thể gọi với một tên khác là spin lock. Thread wakeup bởi spurious thì sẽ bị lock bên trong while loop, và chỉ có thể thoát ra khỏi spin lock khi điều kiện của spin lock trả về false.

https://gist.github.com/namchuai/c3393ccbf7564528dbb0d16417c30c35

Như trên, wait() sẽ nằm trong spin lock. Nếu điều kiện chưa thỏa mãn mà thread bị tỉnh dậy, nó sẽ lại quay trở lại wait().

  1. Multiple thread waiting for the same signals

Spin lock là một solution tốt khi có nhiều thread cùng đợi chung một tín hiệu. Chúng sẽ cùng thức giấc nếu notifyAll() được gọi. Nhưng chỉ có một thread được phép chạy tiếp. Chỉ một thread ở một thời điểm sẽ chiếm được khóa trên monitor object, có nghĩa là chỉ một thread có thể thoát khỏi wait() và clear cờ wasSignalled. Một khi thread này thoát khỏi synchronized block trong hàm doWait(), các thread khác có thể thoát khỏi wait() và kiểm tra cờ wasSignalled bên trong spin lock. Tuy nhiên, cờ này đã bị clear bởi thread đầu tiên tỉnh dậy, và tất cả các thread còn lại quay trở lại trang thái waiting, cho đến khi tín hiệu tiếp theo cập bến.

  1. Don’t call wait() on constant String’s or global objects

https://gist.github.com/namchuai/9a6e1f031b0096bcf2cff3d0872a3f5e

Vấn đề của chúng ta là chúng ta gọi wait() và notify() trên một string rỗng. Hoặc một constant string nào đó, JVM/Compiler sẽ translate chúng thành một object. Có nghĩa là nếu chúng ta có hai MyWaitNotify instance, chúng sẽ sử dụng chung một khóa. Có nghĩa là những thread wait() ở instance này có thể nhận tín hiệu awake từ instance khác.

 

 

 

 

 

 

Remember, that even if the 4 threads call wait() and notify() on the same shared string instance, the signals from the doWait() and doNotify() calls are stored individually in the two MyWaitNotify instances. A doNotify() call on the MyWaitNotify 1 may wake threads waiting in MyWaitNotify 2, but the signal will only be stored in MyWaitNotify 1.

At first this may not seem like a big problem. After all, if doNotify() is called on the second MyWaitNotify instance all that can really happen is that Thread A and B are awakened by mistake. This awakened thread (A or B) will check its signal in the while loop, and go back to waiting because doNotify() was not called on the first MyWaitNotify instance, in which they are waiting. This situation is equal to a provoked spurious wakeup. Thread A or B awakens without having been signaled. But the code can handle this, so the threads go back to waiting.

The problem is, that since the doNotify() call only calls notify() and not notifyAll(), only one thread is awakened even if 4 threads are waiting on the same string instance (the empty string). So, if one of the threads A or B is awakened when really the signal was for C or D, the awakened thread (A or B) will check its signal, see that no signal was received, and go back to waiting. Neither C or D wakes up to check the signal they had actually received, so the signal is missed. This situation is equal to the missed signals problem described earlier. C and D were sent a signal but fail to respond to it.

If the doNotify() method had called notifyAll() instead of notify(), all waiting threads had been awakened and checked for signals in turn. Thread A and B would have gone back to waiting, but one of either C or D would have noticed the signal and left the doWait() method call. The other of C and D would go back to waiting, because the thread discovering the signal clears it on the way out of doWait().

You may be tempted then to always call notifyAll() instead notify(), but this is a bad idea performance wise. There is no reason to wake up all threads waiting when only one of them can respond to the signal.

So: Don’t use global objects, string constants etc. for wait() / notify() mechanisms. Use an object that is unique to the construct using it. For instance, each MyWaitNotify3 (example from earlier sections) instance has its own MonitorObject instance rather than using the empty string for wait() / notify() calls.

 

Leave a Reply

Your email address will not be published. Required fields are marked *