chim bay

Tuesday, November 24, 2015

Thread và Tại sao không dùng nhiều processes , sao không là multiprocessing mà lại cần đến multithreading ?


Thread là một dòng các điều khiển trong một process hay một ứng dụng. Nguyên văn là : Threads are multiple flows of control within a single program or process.

Với cơ chế multithreading ứng dụng của ta có thể thực thi đồng thời nhiều dòng lệnh cùng lúc. Có nghĩa là ta có thể làm nhiều công việc đồng thời trong cùng một ứng dụng của ta. Có thể hiểu một cách hết sức đơn giản : hệ điều hành với cơ chế đa nhiệm cho phép nhiều ứng dụng chạy cùng lúc. Thì với cơ chế đa luồng, mỗi ứng dụng của ta có thể thực hiện được nhiều công việc đồng thời.

Tại sao không dùng nhiều processes , sao không là multiprocessing mà lại cần đến multithreading ?

Câu trả lời đơn giản nhất là: Việc tạo ra và quản lý các process đòi hỏi nhiều tài nguyên của hệ thống (cả ram và CPU) nhiều hơn rất nhiều so với việc tạo ra một thread. Trong khi đó có thể chỉ cần tạo ra một thread để thực hiện song song một công việc hết sức đơn giản cùng với một công việc chính.

Viết một ứng dụng Java trên bất kỳ nền tảng nào. Khi ứng dụng ,ta chạy thì thực sự đã có một bản sao của JVM khởi động và ứng dụng của ta là một thread nếu ta không dùng multithreading hoặc là nhiều threads nếu tadùng multithreading.

Tạo một threadNhư đã nói, mỗi khi chạy một ứng dụng trong java thì đã có một thread. Đây là thread chính, nó thực thi các dóng lệnh trong method : public static void main . Đây là một điểm nhập bắt buộc cho mọi ứng dụng độc lập.

Để tạo ra một thread khác ngoài thread chính trên, Java cung cấp cho chúng ta hai cách :

- Tạo ra một lớp con của lớp Thread (java.lang.Thread)

- Tạo ra một lớp hiện thực interface Runnable

Chúng ta sẽ tìm hiểu lần lược hai cách trên.

Tạo một lớp con của lớp java.lang.Thread

Bạn khai báo như sau :

class A extends Thread {

public void run() {

... // code for the new thread to execute

}

}

...

A a = new A(); // create the thread object

a.start(); // start the new thread executing

...

Với cách này các dòng lệnh sẽ được đặt trong method run. Method này được override method nguyên thuỷ của lớp Thread.

Sau đó ta sẽ tạo ra một đối tượng từ lớp của ta.

Gọi phương thức start từ đối tượng đó. Lúc này thread của ta chính thức được tạo ra và phương thức start sẽ tự gọi method run của tavà thực thi các dòng lệnh mà đã đặc tả.

Chú ý rằng: method start là method của hệ thống, nó có nhiệu vụ cấp phát bộ nhớ, tạo ra một thread và gọi hàm run của ta. Vì thế không nên override phương thức này. Điều này có thể dẫn đến ko tạo được thread.

Hiện thực interface Runnable
Khai báo như sau:

class B extends … implements Runnable {

public void run() {

... // code for the new thread to execute

}

}

...

B b = new B(); // create the Runnable object

Thread t = new Thread(b); // create a thread object

t.start(); // start the new thread

...

Cũng giống như cách trên, dòng lệnh đặt trong method run (có thể gọi đến các phương thức khác, nhưng phải bắt đầu trong phương thức này)

Sau đó tạo một đối tượng B từ lớp đã hiện thực interface Runnable, tạo thêm một đối tượng t của lớp Thread với thông số cho constructor là đối tượng B.

Sau đó khi gọi phương thức t.start() thì chính thức thread được tạo ra và phương thức run sẽ được triệu gọi một cách tự động.

Bạn sẽ hỏi tại cách thứ hai vẫn phải tạo ra một đối tượng Thread. Vậy tại sao lại đưa ra hai cách hiện thực làm gì ?

Câu trả lời là :

- Bản thân ngôn ngữ Java không hỗ trợ đa thừa kế . Bạn chỉ có thể extends từ một lớp duy nhất. Nhưng bạn lại có thể implements cùng lúc nhiều interface. Khi mà lớp của ta đã [extends] một lớp nào đó rồi (vd : Applet), thì chỉ có thể implements Runnable để tạo ra Thread.

- Việc extends lớp Thread có thể dẫn đến rủi ro là bạn override các method start, stop, ... thì có thể làm cho việc tạo thread là không thể.

Một lời khuyên là: nên tạo ra một lớp hiện thực interface Runnable (cách thứ hai) khi muốn tạo ra một Thread. Chương trình sẽ trong sáng và dễ tìm lỗi hơn.

-------------------------------
Việc đồng bộ hóa một method là cách tốt nhất để hạn chế việc sử dụng một method tại một thời điểm. Tuy nhiên sẽ có những trường hợp mà bạn không thể đồng bộ hóa một method, chẳng hạn như khi bạn sử dụng một class được cung cấp bởi bên thứ ba. Trong những trường hợp như thế, bạn không được phép truy cập vào định nghĩa lớp, sẽ ngăn bạn sử dụng từ khóa synchronized.

Sử dụng phát biểu được đồng bộ hoá

Một cách khác để sử dụng từ khóa synchronized là sử dụng phát biểu được đồng bộ hóa. Một phát biểu được đồng bộ hóa chứa một block được đồng bộ hóa , mà bên trong đó đặt những đối tượng và những method được đồng bộ hóa. Gọi các method chứa block được đồng bộ hóa xảy ra khi một thread có được monitor của đối tượng.
Mặc dù bạn có thể gọi những method bên trong một block được đồng bộ hóa, việc công bố method phải được thực hiện bên ngoài một block được đồng bộ hóa.
Ví dụ dưới đây chỉ cách làm thế nào để sử dụng một phát biểu được đồng bộ hóa. Ví dụ này cơ bản cũng giống ví dụ trước, tuy nhiên phát biểu được đồng bộ hóa được sử dụng thay vì từ khóa synchronized. Phát biểu này được đặt trong method run() của class MyThread. Phát biểu được đồng bộ hóa sẽ đồng bộ hóa instance của class Parentheses và vì thế ngăn hai thread sử dụng method display() cùng một lúc.
class Parentheses {
void display(String s) {
System.out.print (”(” + s);
try {
Thread.sleep (1000);
} catch (InterruptedException e) {
System.out.println (”Interrupted”);
}
System.out.println(”)”);
}
}

class MyThread implements Runnable {
String s1;
Parentheses p1;
Thread t;
public MyThread (Parentheses p2, String s2) {
p1= p2;
s1= s2;
t = new Thread(this);
t.start();
}
public void run() {
synchronized(p1){
p1.display(s1);
}
}
}

class Demo{
public static void main (String args[]) {
Parentheses p3 = new Parentheses();
MyThread name1 = new MyThread(p3, “Bob”);
MyThread name2 = new MyThread(p3, “Mary”);
try {
name1.t.join();
name2.t.join();
} catch (InterruptedException e ) {
System.out.println( “Interrupted”);
}
}
}

Ở đây, method display() không sử dụng từ khóa synchronized. Thay vào đó, phát biểu được đồng bộ hóa được sử dụng bên trong method run(). Điều này cho kết quả giống với ví dụ trước bởi vì một thread chờ một khoảng thời gian để thread còn lại kết thúc trước khi tiếp tục xử lý.

Giao tiếp giữa các thread

Các thread mở ra cho các lập trình viên một khoảng không mới trong lập trình, nơi mà ở đó những phần của một chương trình thực thi không đồng bộ với nhau, mỗi một xử lý độc lập với những xử lý khác. Tuy nhiên mỗi thread thỉnh thoảng cần tính toán việc xử lý của chúng và vì thế cần có thể giao tiếp với những thread khác trong suốt quá trình xử lý. Các lập trình viên gọi đây là inter-process communication (giao tiếp trong xử lý).
Bạn có thể có các thread giao tiếp với các thread khác trong chương trình của bạn bằng cách sử dụng những method wait(), notify() và notifyAll(). Những method này được gọi từ bên trong một method được đồng bộ hóa. Method wait() nói cho một thread giải phóng monitor và đi vào trạng thái suspend. Có hai dạng method wait() khác nhau. Một dạng không yêu cầu đối số và vì thế một thread sẽ chờ cho đến khi nó được thông báo. Một dạng khác có đối số để bạn xác định khoảng thời gian chờ. Bạn xác định độ dài thời gian trong mili giây và đặt nó vào trong method wait().
Method notify() nói cho một thread đang suspend bởi method wait() và lấy lại điều khiển của monitor. Method notifyAll() đánh thức tất cả các thread đang chờ điều khiển của monitor. Những thread khác chờ trong trạng thái suspend cho đến khi monitor có sẵn trở lại.
Ví dụ dưới đây chỉ cho bạn làm thế nào để sử dụng những method này trong một ứng dụng. Mục đích của chương trình là có một class Pulishser cho một giá trị cho class Consumer thông qua sử dụng class Queue. Ví dụ này định nghĩa bốn class, class Pulisher, class Comsumer, class Queue và class Demo. Class Queue định nghĩa hai instance: exchangeValue và một biến cờ. exchangeValue đặt vào một giá trị trong queue bởi publisher. Biến cờ được sử dụng như một cách đánh dấu giá trị được đặt vào trong queue. Class Queue cũng định nghĩa một method get() và một method put(). Method put() sử dụng để đặt một giá trị vào queue (gán một giá trị cho exchangeValue), method get() sử dụng để nhận giá trị chứa trong queue (trả về giá trị của exchangeValue. Khi một giá trị được gán, method put() thay đổi giá trị của biến cờ từ false thành true xác định một giá trị được đặt vào trong queue. Chú ý giá trị của biến cờ được sử dụng như thế nào trong method get() và method put() để có thread gọi method chờ cho đến khi có một giá trị trong queue hoặc không có giá trị nào trong queue, phụ thuộc vào method nào đang được gọi.
Class Publisher công bố một instance của class Queue và sau đó gọi method put() đặt vào năm số nguyên integer trong queue. Mặc dù method put() được đặt trong một vòng lặp for, mỗi số nguyên integer được đặt vào trong queue, và sau đó có một khoảng tạm dừng cho đến khi số nguyên integer được nhận bởi class Consumer.
Class Consumer tương tự như thiết kế class Publisher, ngoại trừ class Consumer gọi method get() năm lần bên trong một vòng lặp for. Mỗi lần gọi, method get() tạm dừng cho đến khi class Publisher đặt một số nguyên integer vào trong queue.
Method main() của class Demo tạo ra các instance của class Publisher, class Consumer và class Queue. Chú ý rằng các khởi dựng của của class Publisher và class Consumer đều đặt vào một reference đến instance của class Queue. Chúng sử dụng instance của class Queue cho inter-process communication.
Dưới đây là những gì mà bạn sẽ thấy khi chạy chương trình và mã nguồn của ví dụ
Put: 0
Get: 0
Put: 1
Get: 1
Put: 2
Get: 2
Put: 3
Get: 3
Put: 4
Get: 4

class Queue {
int exchangeValue;
boolean busy = false;
synchronized int get() {
if (!busy)
try {
wait();
} catch (InterruptedException e) {
System.out.println(
“Get: InterruptedException”);
}
System.out.println(”Get: ” + exchangeValue);
notify();
return exchangeValue;
}
synchronized void put (int exchangeValue) {
if (busy)
try {
wait();
} catch (InterruptedException e) {
System.out.println(
“Put: InterruptedException”);
}
this.exchangeValue = exchangeValue;
busy = true;
System.out.println(”Put: ” + exchangeValue);
notify();
}
}

class Publisher implements Runnable {
Queue q;
Publisher(Queue q) {
this.q = q;
new Thread (this, “Publisher”).start();
}
public void run() {
for (int i = 0; i < 5; i++){
q.put(i);
}
}
}

class Consumer implements Runnable {
Queue q;
Consumer (Queue q) {
this.q = q;
new Thread (this, “Consumer”).start();
}
public void run() {
for (int i = 0; i < 5; i++){
q.get();
}
}
}

class Demo {
public static void main(String args []) {
Queue q = new Queue ();
new Publisher (q);
new Consumer (q);
}
}
----------------------------------------------------------------------
----------------------------------------------------------------------
Điều phối các thread ngang hàng

HĐH điều phối tiến trình theo phương pháp phân chia cho mỗi tiến trình một khoảng thời gian delta t bằng nhau
Khi một tiến trình bị sleep thì tiến trình khác sẽ chiếm hữu CPU.

Các nguyên tắc khi lập trình thread trong java
Nguyên tắc đầu tiên các bạn nên nhớ đó là không có khái niệm chạy đồng thời. Chúng ta thường nghe nói các thread có thể chạy đồng thời nhưng thực ra đó là cách nói của những vắn tắt của những chuyên gia. Và khi đến tai của chúng ta thì nó đã bị chính các bạn hiểu một cách sai lệch là CPU xử lý đồng thời 2 tiến trình nhưng thực ra trong một thời điểm thì CPU chỉ có thể xử lý được một công việc duy nhất hay nói cách khác là một phép toán duy nhất. Sở dĩ chúng ta có thể thấy 2 kết quả xuất hiện đồng thời khi bạn cho 2 thread chạy với một thời gian sleep như nhau là vì chúng ta không cảm nhận được khoảng thời gian nhường quyền được xử lý giữa 2 thread đó. Giả sử ta có 2 thread ngang hàng A, B. A cho sleep (10000) và B cũng sleep(10000) tức là 10 giây. Sau 10 giây bạn sẽ thấy 2 kết quả xuất hiện đồng thời. Để có thể lý giải được điều này thì mời bạn đọc tiếp. (1)

Thứ 1: Khi hhông sử dụng hàm sleep trong phương thức run của A và B
 Các thread sẽ chạy tuần tự. Tức là thread nào khởi tạo trước thì chạy trước, thread nào khởi tạo sau thì chạy sau.
Thứ 2:
Khi sử dụng hàm sleep thì 2 thread A và B sẽ được phân phối dựa theo thời gian sleep của mỗi thread. Thời gian sleep được tính bắt đầu từ lúc bắt đầu khởi chạy và lúc một thread vừa được xử lý xong và bắt đầu chuyển sang trang thái sleep.

Mặc dù hàm sleep(int a) phải truyền vào tham số là một số nguyên dương hoặc 0 nhưng thực ra nếu truyền vào giá trị là 0 thì cũng không có nghĩa là tương đương với việc không sử dụng hàm sleep.
Nên nhớ rằng sleep(0) khác với không sử dụng hàm sleep. Mặc dù đối truyền vào là 0 nhưng thực ra CPU vẫn dành ra một khoảng thời gian > 0 & < 1 (mili giây) để phân phối cho thread đó.
Vì vậy mà nếu A dùng sleep(0) và B dùng sleep(1) thì số lần A được CPU xử lý hiều hơn B và tất nhiên là A sẽ có nhiều kết quả đầu ra hơn B.

Tôi có thể lý giải vấn đề này một cách cụ thể và logic hơn như sau:

+ A sleep(0) nhưng CPU tính là nó sleep trong một khoảng thời gian là delta t với t < 1 mili giây. Tôi giả sử là delta t = 1/3 mili giây.
+ B sleep(1) thì CPU tính là sleep trong 1 mili giây.
Khi bạn cho chạy 2 thread A và B thì CPU sẽ xử lý như sau (giả sử A khởi tạo trước):
A được CPU xử lý trong quá trình đó thì tất cả các thread khác sẽ bị khoá trong vòng 1/3 mili giây. Sau 1/3 mili giây thì B vẫn đang bị khoá nên A tiếp tục được xử lý và sau khi A được xử lý xong thì tổng thời gian sleep của A là 2/3 mili giây sau 3 lần chạy thì B đã ngủ đủ 1 mili giây B bắt đầu chạy. Trong khi đó thì A đang phải sleep tiếp 1/3 mili giây. Sau khi B xử lí xong thì A sẽ tiếp tục được xử lý nhưng nếu A được wakeup trong khi B đang được xử lí thì A sẽ đặt vào trạng thái chờ trong hàng đợi và sau khi B xử lý xong thì ngay lập thức A sẽ được xử lý... Và cứ như vậy ta có thể đưa ra một kết luận là sẽ không bao giờ có chuyện A cho ra nhiều hơn 3 kết quả liên tục và cũng không bao giờ có chuyện B cho ra nhiều hơn 1 kết quả liên tục.
Các bạn có thể kiểm chứng điều này bằng cách A cho sleep(1) và B cho sleep(3).
Bạn sẽ không bao giờ nhận được một kết quả như là:
A
A
A
A
B
hay là:
B
B
A

Nếu cả 2 thread A và B đều có thời gian sleep bằng nhau kể cả là sleep(0) thì các kết quả xử lý sẽ là xen kẽ nhau và bạn sẽ chẳng bao giờ nhận được một kết quả như là:
A
A
B
hoặc là:
B
B
A
mà chỉ có:
A
B
A
B
...
Đối với trường hợp này khi cả A và B sleep đủ thời gian thì cả 2 sẽ được wakeup để xử lý nhưng thread nào khởi tạo trước sẽ được ưu tiên xử lý trước. Thread chưa khi được wakeup mà CPU lại đang bận xử lý thread khác thì nó được đẩy vào hàng đợi. Do đó A, B nối đuôi nhau xử lý. A xử lý xong thì in ngay kết quả ra màn hình trong khi đo B cũng đang được xử lý và in ra kết quả ngay khi nó xử lý xong. Bây giờ bạn đọc lại phần đầu (1) và ngẫm lại thì sẽ hiểu vì sao lại gọi
là xử lý đồng thời.

0 comments:

Post a Comment