text:
Thảo luận Bài 5 (Đa luồng)
Trang 6 trong tổng số 6 trang • Share •
Trang 6 trong tổng số 6 trang • 1, 2, 3, 4, 5, 6
Thảo luận bài 5
Những thuận lợi của lập trình đa luồng.1. Sự đáp ứng.
- Đa luồng là một ứng dụng giao tiếp cho phép một chương trình tiếp tục chạy thậm chí nếu một phần của nó bị khóa hay đang thực hiện một thao tác dài, do đó gia tăng sự đáp ứng đối với người dùng. Ví dụ, một trình duyết web vẫn có thể đáp ứng người dùng bằng một luồng trong khi một ảnh đang nạp bằng một luồng khác.
2. Chia sẻ tài nguyên.
- Mặc đinh, các luồng chia sẻ bộ nhớ và các tài nguyên của các quá trình mà chúng ta thuộc về. Thuận lợi của việc chia sẻ mã là nó cho phép một ứng dụng có nhiều hoạt động của các luồng khác hau nằm trong không gian địa chỉ.
3. Kinh tế.
- Cấp phát bộ nhớ và các tài nguyên cho việc tạo các quá trình là rất đắt. Vì các luồng chia sẻ tài nguyên của quá trình mà chúng ta thuộc về nên nó kinh tế hơn để tạo và chuyển ngữ cảnh giữa các luồng. Khó để đánh giá theo kinh nghiệm sự khác biệt chi phí cho việc tạo duy trì một quá trình hơn một luồng, nhưng thường nó sẽ mất nhiều thời gian để tạo và quản lý một quá trình hơn một luồng. Trong Solaris 2, tạo một quá trình chậm hơn khoảng 30 lần tạo một luồng và chuyển đổi ngữ cảnh chậm hơn 5 lần.
4. Sử dụng kiến trúc đa xử lý.
- Các lợi điểm của đa luồng có thể phát huy trong kiến trúc đa xử lý, ở đó mỗi luồng thực thi song song trên một bộ xử lý khác nhau. Một quá trình đơn luồng chỉ có thể chạy trên một CPU. Đa luồng trên một máy nhiều CPU gia tăng tính đồng hành. Trong kiến trúc đơn xử lý, CPU thường chuyển đổi qua lại giữa mỗi luồng quá nhanh để tạo ra hình ảnh của sự song song nhưng trong thực tế chỉ một luồng đang chạy tại một thời điểm.
HuynhVanNhut (I11C)
- Tổng số bài gửi : 12
Join date : 07/09/2011
-
Lập trình đa luồng trong C#
Trong series bài viết này, mình sẽ cố gắng truyền đạt hết những gì mình đã tìm hiểu được về kỹ thuật lập trình đa luồng trong C#, một cách dễ hiểu nhất. Nội dung series bài viết gồm có:
1.Cái nhìn tổng quan về lập trình đa luồng
2.Viết một chương trình xử lý đa luồng đơn giản
3.Đồng bộ hóa trong lập trình đa luồng
4.Giao tiếp trong một chương trình xử lý đa luồng
Mình dự định sẽ tự viết series này, sẽ mất thời gian và gặp nhiều khó khăn nên rất mong các bạn ủng hộ. Mọi thắc mắc và trao đổi các bạn cứ thoải mái comment để chúng ta cùng tìm hiểu.
Cái nhìn tổng quan về lập trình đa luồng
Nhắc lại một số kiến thức chung
Windows là một hệ điều hành đa nhiệm (multitasking), tức là có thể thực hiện nhiều công việc cùng lúc. Để làm được như vậy, hệ điều hành cần phải phân chia công việc và quản lý chúng sao cho khi có sự cố thì sẽ không ảnh hưởng toàn bộ hệ thống. Hãy xem Windows đã làm thế nào nhé:
Mỗi chương trình được Windows cho phép thực hiện trên một tiến trình (process), trong một tiến trình có ít nhất một Application Domain và trong Application Domain lại có thể có chứa Thread… Ta có thể tưởng tượng như sau: trong khu phố X, ta xây các ngôi nhà A, B, C,… cho các gia đình ở, để họ không phải chung đụng nhau, dễ gây lộn; trong mỗi ngôi nhà lại có các phòng 1,2,3,… để các thành viên có thể làm việc riêng mà không ảnh hưởng lẫn nhau…
=> Như vậy ta có thể nhận xét ý tưởng là khoanh vùng và định ra các ranh giới. ^^!
Khái niệm lập trình đa luồng
Bước vào trong thế giới lập trình, lập trình đa luồng là một kỹ thuật lập trình giúp ta có thể thực hiện nhiều công việc song song cùng lúc. Trong kỹ thuật này ta sẽ tạo ra các luồng (thread) chạy cùng lúc để thực hiện các nhiệm vụ khác nhau mà không gián đoạn chương trình chính.
Một số ưu điểm:
•Giúp chương trình thực hiện nhiều công việc cùng lúc, tiết kiệm thời gian thực thi.
•Giúp tăng sự linh hoạt trong khi thực thi chương trình, tăng hiệu suất.
Và một số hạn chế:
•Đòi hỏi sự cẩn thận trong việc quản lý tài nguyên hệ thống.
•Cần phải có sự đồng bộ hóa giữa các luồng để đảm bảo tính đúng đắn của chương trình.
“Công cụ” lập trình đa luồng
Namespace System.Threading và các thành phần của nó.
1.Cái nhìn tổng quan về lập trình đa luồng
2.Viết một chương trình xử lý đa luồng đơn giản
3.Đồng bộ hóa trong lập trình đa luồng
4.Giao tiếp trong một chương trình xử lý đa luồng
Mình dự định sẽ tự viết series này, sẽ mất thời gian và gặp nhiều khó khăn nên rất mong các bạn ủng hộ. Mọi thắc mắc và trao đổi các bạn cứ thoải mái comment để chúng ta cùng tìm hiểu.
Cái nhìn tổng quan về lập trình đa luồng
Nhắc lại một số kiến thức chung
Windows là một hệ điều hành đa nhiệm (multitasking), tức là có thể thực hiện nhiều công việc cùng lúc. Để làm được như vậy, hệ điều hành cần phải phân chia công việc và quản lý chúng sao cho khi có sự cố thì sẽ không ảnh hưởng toàn bộ hệ thống. Hãy xem Windows đã làm thế nào nhé:
Mỗi chương trình được Windows cho phép thực hiện trên một tiến trình (process), trong một tiến trình có ít nhất một Application Domain và trong Application Domain lại có thể có chứa Thread… Ta có thể tưởng tượng như sau: trong khu phố X, ta xây các ngôi nhà A, B, C,… cho các gia đình ở, để họ không phải chung đụng nhau, dễ gây lộn; trong mỗi ngôi nhà lại có các phòng 1,2,3,… để các thành viên có thể làm việc riêng mà không ảnh hưởng lẫn nhau…
=> Như vậy ta có thể nhận xét ý tưởng là khoanh vùng và định ra các ranh giới. ^^!
Khái niệm lập trình đa luồng
Bước vào trong thế giới lập trình, lập trình đa luồng là một kỹ thuật lập trình giúp ta có thể thực hiện nhiều công việc song song cùng lúc. Trong kỹ thuật này ta sẽ tạo ra các luồng (thread) chạy cùng lúc để thực hiện các nhiệm vụ khác nhau mà không gián đoạn chương trình chính.
Một số ưu điểm:
•Giúp chương trình thực hiện nhiều công việc cùng lúc, tiết kiệm thời gian thực thi.
•Giúp tăng sự linh hoạt trong khi thực thi chương trình, tăng hiệu suất.
Và một số hạn chế:
•Đòi hỏi sự cẩn thận trong việc quản lý tài nguyên hệ thống.
•Cần phải có sự đồng bộ hóa giữa các luồng để đảm bảo tính đúng đắn của chương trình.
“Công cụ” lập trình đa luồng
Namespace System.Threading và các thành phần của nó.
Những lợi ích của lập trình đa luồng
Những lợi ích của lập trình đa luồng
Được chia làm 4 loại như sau:
1.Sự đáp ứng: đa luồng một ứng dụng giao tiếp cho phép một chương trình tiếp tục chạy thậm chí nếu một phần của nó bị khóa hay đang thực hiện một thao tác dài, do đó gia tăng sự đáp ứng đối với người dùng. Thí dụ, một trình duyệt web vẫn có thể đáp ứng người dùng bằng một luồng trong khi một ảnh đang được nạp bằng một luồng khác.
2.Chia sẻ tài nguyên: Mặc định, các luồng chia sẻ bộ nhớ và các tài nguyên của các quá trình mà chúng thuộc về. Thuận lợi của việc chia sẽ mã là nó cho phép một ứng dụng có nhiều hoạt động của các luồng khác nhau nằm trong cùng không gian địa chỉ.
3.Kinh tế: cấp phát bộ nhớ và các tài nguyên cho việc tạo các quá trình là rất
đắt. Vì các luồng chia sẻ tài nguyên của quá trình mà chúng thuộc về nên nó kinh tế hơn để tạo và chuyển ngữ cảnh giữa các luồng. Khó để đánh giá theo kinh nghiệm sự khác biệt chi phí cho việc tạo và duy trì một quá trình hơn một luồng, nhưng thường nó sẽ mất nhiều thời gian để tạo và quản lý một quá trình hơn một luồng. Trong Solaris 2, tạo một quá trình chậm hơn khoảng 30 lần tạo một luồng và chuyển đổi ngữ cảnh chậm hơn 5 lần.
4.Sử dụng kiến trúc đa xử lý: các lợi điểm của đa luồng có thể phát huy trong
kiến trúc đa xử lý, ở đó mỗi luồng thực thi song song trên một bộ xử lý khác nhau. Một quá trình đơn luồng chỉ có thể chạy trên một CPU. Đa luồng trên một máy nhiều CPU gia tăng tính đồng hành. Trong kiến trúc đơn xử lý, CPU thường chuyển đổi qua lại giữa mỗi luồng quá nhanh để tạo ra hình ảnh của sự song song nhưng trong thực tế chỉ một luồng đang chạy tại một thời điểm.
Được chia làm 4 loại như sau:
1.Sự đáp ứng: đa luồng một ứng dụng giao tiếp cho phép một chương trình tiếp tục chạy thậm chí nếu một phần của nó bị khóa hay đang thực hiện một thao tác dài, do đó gia tăng sự đáp ứng đối với người dùng. Thí dụ, một trình duyệt web vẫn có thể đáp ứng người dùng bằng một luồng trong khi một ảnh đang được nạp bằng một luồng khác.
2.Chia sẻ tài nguyên: Mặc định, các luồng chia sẻ bộ nhớ và các tài nguyên của các quá trình mà chúng thuộc về. Thuận lợi của việc chia sẽ mã là nó cho phép một ứng dụng có nhiều hoạt động của các luồng khác nhau nằm trong cùng không gian địa chỉ.
3.Kinh tế: cấp phát bộ nhớ và các tài nguyên cho việc tạo các quá trình là rất
đắt. Vì các luồng chia sẻ tài nguyên của quá trình mà chúng thuộc về nên nó kinh tế hơn để tạo và chuyển ngữ cảnh giữa các luồng. Khó để đánh giá theo kinh nghiệm sự khác biệt chi phí cho việc tạo và duy trì một quá trình hơn một luồng, nhưng thường nó sẽ mất nhiều thời gian để tạo và quản lý một quá trình hơn một luồng. Trong Solaris 2, tạo một quá trình chậm hơn khoảng 30 lần tạo một luồng và chuyển đổi ngữ cảnh chậm hơn 5 lần.
4.Sử dụng kiến trúc đa xử lý: các lợi điểm của đa luồng có thể phát huy trong
kiến trúc đa xử lý, ở đó mỗi luồng thực thi song song trên một bộ xử lý khác nhau. Một quá trình đơn luồng chỉ có thể chạy trên một CPU. Đa luồng trên một máy nhiều CPU gia tăng tính đồng hành. Trong kiến trúc đơn xử lý, CPU thường chuyển đổi qua lại giữa mỗi luồng quá nhanh để tạo ra hình ảnh của sự song song nhưng trong thực tế chỉ một luồng đang chạy tại một thời điểm.
so sánh sự khác nhau giữa lập trình đa luồng và đa tiến trình
so sánh sự khác nhau giữa lập trình đa luồng và đa tiến trình
Giống nhau :
1. Đều có thông tin trạng thái
2. Luồng còn được gọi là tiến trình nhẹ
3. Nhiều luồng or nhiều tiến trình có thể liên quan đến một chương trình
4. Chia sẻ tài nguyên cho nhau
5. Tăng tốc tính toán
6. Đảm bảo tính đơn thể
Khác nhau
1. Do luồng cùng vận hành trong 1 vùng địa chỉ vì vậy dể dùng chung tài nguyên hơn đa tiến trình
VD : Mượn đồ dùng trong gia đình thì dể hơn mượn của hàng xóm
2. Do luồng chung tài nguyên với cha & các luồng khác nên việc tạo lập & chuyển ngữ cảnh cũng
nhanh hơn tiến trình
VD : Việc ngăn riêng ra một phòng trong nhà thì nhanh hơn là chờ nhà nước cấp cho
một căn hộ
3. Cấp phát bộ nhớ & tài nguyên cho tiến trình thì tốn kém hơn luồng
4. Lập trình đa luồng thì dể hơn đa tiến trình
Giống nhau :
1. Đều có thông tin trạng thái
2. Luồng còn được gọi là tiến trình nhẹ
3. Nhiều luồng or nhiều tiến trình có thể liên quan đến một chương trình
4. Chia sẻ tài nguyên cho nhau
5. Tăng tốc tính toán
6. Đảm bảo tính đơn thể
Khác nhau
1. Do luồng cùng vận hành trong 1 vùng địa chỉ vì vậy dể dùng chung tài nguyên hơn đa tiến trình
VD : Mượn đồ dùng trong gia đình thì dể hơn mượn của hàng xóm
2. Do luồng chung tài nguyên với cha & các luồng khác nên việc tạo lập & chuyển ngữ cảnh cũng
nhanh hơn tiến trình
VD : Việc ngăn riêng ra một phòng trong nhà thì nhanh hơn là chờ nhà nước cấp cho
một căn hộ
3. Cấp phát bộ nhớ & tài nguyên cho tiến trình thì tốn kém hơn luồng
4. Lập trình đa luồng thì dể hơn đa tiến trình
Đa luồng trong java (multithreading in Java)
Multithread trong java
1.1 Khái niệm
Thread là một đơn vị (nhóm) chứa các dòng điều khiển trong một tiến trình hay ứng dụng, chúng được hệ điều hành sử dụng để thực thi một công việc. Và với cơ chế multithread ứng dụng của ta có thể thực thi nhiều công việc đồng thời. Mỗi thread sẽ có một chỉ số xác định quyền ưu tiên, thread nào có quyền ưu tiên cao sẽ được thực thi trước, và thấp thì thực thi sau. Việc thực thi và đánh chỉ số ưu tiên sẽ được thực hiện tự động bởi hệ điều hành để tránh một thread nào đợi quá lâu mà không được vào xử lý.
Cũng như các ngôn ngữ lập trình khác, java hỗ trợ ngay (trong bản thân ngôn ngữ) khá đầy đủ các yêu cầu, xử lý của một ứng dụng multithread.
1.2 Tạo và quản lý thread
Trong chương trình java luôn có một thread chính được thực thi, các thread khác sẽ được tạo ra như các thread con, việc mở đầu và kết thúc một ứng dụng được thực hiện bởi thread chính đó.
Hình dưới minh họa vòng đời của một thread:
Để tạo và quản lý thread trong java, chúng ta dựa vào lớp Thread và Runnable interface
Runnable là một interface dùng để các lớp khác implement để sử dụng thread. Trong lớp implement đó bắt buộc phải định nghĩa một phương thức là void run().
Ví dụ ta có 2 lớp StringThread implement Runnable, và một lớp TestClass để tạo ra thread và run nó.
public class StringThread implements Runnable {
private String str;
private int num;
StringThread(String s, int n)
{
str = new String(s);
num = n;
}
/* (non-Javadoc)
* @see java.lang.Runnable#run()
*/
@Override
public void run() {
for (int i = 1; i <= num; i++)
System.out.print(str + " ");
}
}
public class TestClass {
/**
* @param args
*/
public static void main(String[] args) {
StringThread t = new StringThread("Thread", 10);
new Thread(t).start();
}
}
Kết quả in ra màn hình sẽ là: Thread Thread Thread Thread Thread Thread Thread Thread Thread Thread
Thread là một lớp viết sẵn trong java, nó implement Runnable interface. Nếu chúng ta muốn sử dụng thread chỉ cần viết một lớp và kế thừa (extends) từ lớp Thread này.
Một số hàm thông dụng của Thread
• start bắt đầu thực thi thread
• sleep đưa trạng thái về trạng thái đợi (không thực thi nữa)
• resume thực thi lại thread từ trạng thái đợi hoặc dead-lock
• join đợi để cho đến khi thread này chết
• isAlive kiểm tra thread còn đang sống hay đã bị kill
• getName lấy tên của thread
• getPriority trả về độ ưu tiên của thread
• setName đặt tên cho thread
• setPriority đặt độ ưu tiên cho thread
• …
Ví dụ tương tự việc sử dụng thread với Runnable interface, chúng ta cũng có thể kế thừa tự lớp có sẵn là Thread như sau:
class StringThread extends Thread {
public void run() {
System.out.println("Inherit successfully from Thread class!");
}
}
public class TestClass {
public static void main(String[] args) {
StringThread t = new StringThread();
new Thread(t).start();
}
}
Kết quả in ra sẽ là: Inherit successfully from Thread class!
1.3 Các lớp Timer, TimerTask và SchedulingTask
Timer là một lớp tiện ích giúp cho việc lập lịch và kiểm soát việc thực thi một task vụ nào đó. Ví dụ bạn muốn dùng việc cập nhật thông trong 5 phút tới, hay thông tin chỉ được tồn tại 10h sau đó sẽ tự động bị xóa đi,… Một số hàm thông dụng trong Timer như:
• schedule lên lịch để thực thi task khi nào bắt đầu, kết thúc hay lặp lại, …
• cancel dừng timer và hủy tất cả các task đã lên lịch trong timer
• purge xóa tất cả các task đã dùng trong hàng đợi timer
TimerTask là một lớp trừu tượng implement Runnable interface, nó giúp cho việc lập lịch thực thi các thread
Ví dụ ta có một lớp MyTimerTask kế thừa từ TimerTask như bên dưới
class MyTimerTask extends TimerTask {
public void run() {
System.out.println("Running the scheduled task by Timer");
System.gc();
}
}
public class TestClass {
public static void main(String[] args) {
Timer timer = new Timer();
MyTimerTask task = new MyTimerTask();
timer.schedule(task, 3000, 2000);//Execute task after 3 senconds and then repeat in 2 seconds period
}
}
Kết quả in ra màn hình sau 5 giây sẽ là: Running the scheduled task by Timer
Running the scheduled task by Timer
1.4 Xử lý các vấn đề chúng trong multithread như synchronized, lock, dead lock, wait, notify …
Đồng bộ (Synchronization) là cơ chế giúp cho đồng bộ việc truy cập cùng tài nguyên của nhiều thread khác nhau. Như chúng ta biết vì cơ chế thread là cơ chế làm việc đồng thời do đó có khả năng một tài nguyên sẽ bị truy cập bởi nhiều thread khác nhau.
Ví dụ ta xây dựng 3 lớp Socola, Producer, Consumer. Lớp Socola chứa thông tin, lớp Producer tạo thông tin và gán vào cho Socola, Lớp Consumer sẽ ăn (tiêu thụ) thông tin đó.
public class TestClass {
public class Socola {
private int contents = 0;
public boolean isRemainingContents;
public synchronized int get() {
//print onto screen
System.out.println("\tConsumer get " + this.contents);
return contents;
}
public synchronized void set(int piContents) {
this.contents = piContents;
//print onto screen
System.out.println("Producer set " + this.contents);
}
}
public class Producer extends Thread {
private Socola socola;
Producer(Socola pSocola) {
socola = pSocola;
}
public void run() {
for(int i = 0; i < 10; i++) {
socola.set(i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
}
}
}
public class Consumer extends Thread {
private Socola socola;
Consumer(Socola pSocola) {
socola = pSocola;
}
public void run() {
for(int i = 0; i < 10; i++) {
socola.get();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
}
}
}
public static void main(String[] args) {
TestClass t = new TestClass();
Socola s = t.new Socola();
Producer p = t.new Producer(s);
Consumer c = t.new Consumer(s);
p.start();
c.start();
}
}
Kết quả in ra màn hinh sẽ không giống nhau tại các lần chạy, ví dụ 1 lần chạy:
Producer set 0
Consumer get 0
Producer set 1
Consumer get 1
Producer set 2
Consumer get 2
Producer set 3
Consumer get 3
Producer set 4
Consumer get 4
Consumer get 4
Producer set 5
Producer set 6
Consumer get 6
Producer set 7
Consumer get 7
Consumer get 7
Producer set 8
Consumer get 8
Producer set 9
Từ ví dụ trên ta thấy có chung một tài nguyên là Socola mà có 2 đối tượng cùng truy cập có thể sinh ra mâu thuẫn khi truy cập đồng thời. Bây giờ ta muốn là chỉ khi Producer tạo ra thông tin thì Consumer mới ăn thông tin đó, không muốn Consumer ăn nhiều lần cùng một thông tin, khi Producer đang tạo ra thông tin thì Consumer đứng chờ, khi nào Producer tạo ra thông tin xong thì Consumer mới vào ăn thông tin. vậy thì ta phải dùng cơ chế đồng bộ trong java với các từ khóa synchronized, wait(), notify(), notifyAll() và chỉnh lại 2 hàm get và set trong lớp Socola như sau:
public synchronized int get() {
while(isRemainingContents == false) {
try {
wait();
} catch (InterruptedException e) {
}
}
isRemainingContents = false;
//print onto screen
System.out.println("\tConsumer get " + this.contents);
//Notify Producer to produce new contents
notifyAll();
return contents;
}
public synchronized void set(int piContents) {
while(isRemainingContents == true) {
try {
wait();
} catch (InterruptedException e) {
}
}
this.contents = piContents;
isRemainingContents = true;
//print onto screen
System.out.println("Producer set " + this.contents);
//Notify Consumer to eat this contents
notifyAll();
}
}
Kết quả in ra luôn luôn như sau:
Producer set 0
Consumer get 0
Producer set 1
Consumer get 1
Producer set 2
Consumer get 2
Producer set 3
Consumer get 3
Producer set 4
Consumer get 4
Producer set 5
Consumer get 5
Producer set 6
Consumer get 6
Producer set 7
Consumer get 7
Producer set 8
Consumer get 8
Producer set 9
Consumer get 9
Có 3 hàm cần để ý:
• wait() đưa thread vào trạng thái chờ khi nào được notify() hoặc notifyAll() thì sẽ thực hiện tiếp
• notify() đánh thức thread đang đợi, nếu có nhiều thread đang đợi, thì một trong những thread đó được đánh thức thôi.
• notifyAll() đánh thức tất cả các thread đang đợi
Note: Nhiều khi chúng ta không cần hoặc không thể đồng bộ nguyên hàm (dùng synchronized) mà chỉ cần đồng bộ một vài dòng code truy cập tài nguyên, ta có thể dùng khối đồng bộ như sau:
synchronized(object) {
// Các cấu lệnh cần đồng bộ
}
Ví dụ: Khi bạn sử dụng lớp Socola là của khách hàng bạn đưa cho, bạn không thể chỉnh sửa định nghĩa hàm get và set trong Socola được vậy thì bạn có thể dùng khối đồng bộ bên trên cũng sẽ cho kết quả tương tự.
Deadlock (khóa chết) trong xử lý đa luồng xảy là khi mà một thread được đưa vào trạng thái chờ tài nguyên và không bao giờ được đánh thức lại vì tại nguyên đó không bao giờ được giải phóng. Để rõ hơn ta ví dụ A, B, C là thread trong trạng thái chờ; rA, rB, rC là tài nguyên dùng chung và chỉ duy nhất một thread sử dụng trong một thời điểm, tài nguyên chỉ được giải phóng khi thread làm xong và không sử dụng nữa.
• A giữ rA, chờ rB
• B giữ rB, chờ rC
• C giữ rC, chờ rA
Từ ví dụ trên ta thấy A, B, C là những thread đang ở trong trạng thái chờ tài nguyên, tuy nhiên tài nguyên của chúng không bao giờ được giải phóng, và mãi mãi chúng cứ chờ thôi. Để hạn chế deadlock thì chúng ta phải đưa ra, cải tiến việc cấp phát, thu hồi tài nguyên cho các thread và hiện này có khá nhiều thuật toán cải tiến việc này, tuy nhiên không có cái nào giúp tránh hoàn toàn deadlock.
1.5 Những lớp tiện ích trong lập trình đa luồng (multithread)
Trong java đã xây dựng một vài lớp giúp hỗ trợ lập trình đa luồng, chúng được đặt trong gói Concurent như: Semaphore, Lock, Condition, ConcurrentMap, ConcurrentLinkedQueue, …
Semaphore là một lớp tiện ích đã xây dựng trong java (java 5) dùng để giới hạn số lượng thread truy cập đến tài nguyên. Đầu tiên thread cần yêu cầu sự chấp nhận truy cập tài nguyên từ semaphore bằng hàm acquire, và thread sẽ về trạng thái đợi (block) cho đến khi được chấp nhận, khi thread được chấp nhận nó thực hiện công việc của nó xong sẽ gọi hàm release để trả về cho semaphore một sự chấp nhận còn trống để semaphore cấp cho thread khác.
Lock là một interface được xây dựng nhằm mở rộng hơn việc quản lý truy cập tài nguyên trong nhiều thread hơn so với cách dùng synchronized chẳng hạn như có thể hỗ trợ nhiều điều kiện (condition object) trong lock, hoặc hỗ trợ nhiều thuật toán cho chain-lock để tránh deadlock tốt hơn, … Khi dùng Lock chúng ta nên theo một template chuẩn như sau:
Lock l = ...;
l.lock();
try {
// Các task truy cập tài nguyên bảo vệ bởi lock đặt ở đây
} finally {
l.unlock();
}
Condition là một interface dùng để nhóm cách xử lý trong các phương thức (wait, notify, notifyAll) của lớp Object vào thành một đối tượng nhằm đưa ra nhiều chọn lựa hơn trong việc đồng bộ, ví dụ chúng ta có thể dùng nhiều condition và khi nào chúng ta muốn đánh thức chính xác điều kiện nào được sử dụng tài nguyên.
Ví dụ ta dùng lại ví dụ Consumer-Producer ở trên, thêm vào 2 biến lock, condition và chỉnh sửa hai làm get và put thế là ta cũng được kết quả như dùng với từ khóa synchronized:
final Lock lock = new ReentrantLock();
final Condition condition = lock.newCondition();
public int get() {
int result;
lock.lock();
try {
while(isRemainingContents == false) {
try {
condition.await();
} catch (InterruptedException e) {
}
}
isRemainingContents = false;
//print onto screen
System.out.println("\tConsumer get " + this.contents);
//get value
result = contents;
//Notify Producer to produce new contents
condition.signalAll();
} finally {
lock.unlock();
}
return result;
}
public void set(int piContents) {
lock.lock();
try {
while(isRemainingContents == true) {
try {
condition.await();
} catch (InterruptedException e) {
}
}
this.contents = piContents;
isRemainingContents = true;
//print onto screen
System.out.println("Producer set " + this.contents);
//Notify Consumer to eat this contents
condition.signalAll();
} finally {
lock.unlock();
}
}
Kết quả luôn luôn là thế này:
Producer set 0
Consumer get 0
Producer set 1
Consumer get 1
Producer set 2
Consumer get 2
Producer set 3
Consumer get 3
Producer set 4
Consumer get 4
Producer set 5
Consumer get 5
Producer set 6
Consumer get 6
Producer set 7
Consumer get 7
Producer set 8
Consumer get 8
Producer set 9
Consumer get 9
ConcurrentMap là một interface kế thừa từ Map dùng để cải tiến, tối ưu hơn khi chạy Map trong ứng dụng đa luồng (multithread)
Ví dụ cách dùng ConcurrentHashMap tương tự dùng Map, tuy nhiên người ta đã thống kê là nếu chương trình có càng nhiều thread truy cập vào cMap này thì dùng ConcurrentMap sẽ cho performance tốt hơn rất nhiều so với dùng Map với đồng bộ kiểu synchronized
public static ConcurrentMap<String,String> cMap =
new ConcurrentHashMap<String, String>(1000);
public static void main(String[] args) {
cMap.putIfAbsent("id", "1");
cMap.putIfAbsent("name", "kien");
cMap.putIfAbsent("email", "nakien2a@yahoo.com");
System.out.println(cMap); //Print to screen
}
Kết quả: {name=kien, email=nakien2a@yahoo.com, id=1}
1.1 Khái niệm
Thread là một đơn vị (nhóm) chứa các dòng điều khiển trong một tiến trình hay ứng dụng, chúng được hệ điều hành sử dụng để thực thi một công việc. Và với cơ chế multithread ứng dụng của ta có thể thực thi nhiều công việc đồng thời. Mỗi thread sẽ có một chỉ số xác định quyền ưu tiên, thread nào có quyền ưu tiên cao sẽ được thực thi trước, và thấp thì thực thi sau. Việc thực thi và đánh chỉ số ưu tiên sẽ được thực hiện tự động bởi hệ điều hành để tránh một thread nào đợi quá lâu mà không được vào xử lý.
Cũng như các ngôn ngữ lập trình khác, java hỗ trợ ngay (trong bản thân ngôn ngữ) khá đầy đủ các yêu cầu, xử lý của một ứng dụng multithread.
1.2 Tạo và quản lý thread
Trong chương trình java luôn có một thread chính được thực thi, các thread khác sẽ được tạo ra như các thread con, việc mở đầu và kết thúc một ứng dụng được thực hiện bởi thread chính đó.
Hình dưới minh họa vòng đời của một thread:
Để tạo và quản lý thread trong java, chúng ta dựa vào lớp Thread và Runnable interface
Runnable là một interface dùng để các lớp khác implement để sử dụng thread. Trong lớp implement đó bắt buộc phải định nghĩa một phương thức là void run().
Ví dụ ta có 2 lớp StringThread implement Runnable, và một lớp TestClass để tạo ra thread và run nó.
public class StringThread implements Runnable {
private String str;
private int num;
StringThread(String s, int n)
{
str = new String(s);
num = n;
}
/* (non-Javadoc)
* @see java.lang.Runnable#run()
*/
@Override
public void run() {
for (int i = 1; i <= num; i++)
System.out.print(str + " ");
}
}
public class TestClass {
/**
* @param args
*/
public static void main(String[] args) {
StringThread t = new StringThread("Thread", 10);
new Thread(t).start();
}
}
Kết quả in ra màn hình sẽ là: Thread Thread Thread Thread Thread Thread Thread Thread Thread Thread
Thread là một lớp viết sẵn trong java, nó implement Runnable interface. Nếu chúng ta muốn sử dụng thread chỉ cần viết một lớp và kế thừa (extends) từ lớp Thread này.
Một số hàm thông dụng của Thread
• start bắt đầu thực thi thread
• sleep đưa trạng thái về trạng thái đợi (không thực thi nữa)
• resume thực thi lại thread từ trạng thái đợi hoặc dead-lock
• join đợi để cho đến khi thread này chết
• isAlive kiểm tra thread còn đang sống hay đã bị kill
• getName lấy tên của thread
• getPriority trả về độ ưu tiên của thread
• setName đặt tên cho thread
• setPriority đặt độ ưu tiên cho thread
• …
Ví dụ tương tự việc sử dụng thread với Runnable interface, chúng ta cũng có thể kế thừa tự lớp có sẵn là Thread như sau:
class StringThread extends Thread {
public void run() {
System.out.println("Inherit successfully from Thread class!");
}
}
public class TestClass {
public static void main(String[] args) {
StringThread t = new StringThread();
new Thread(t).start();
}
}
Kết quả in ra sẽ là: Inherit successfully from Thread class!
1.3 Các lớp Timer, TimerTask và SchedulingTask
Timer là một lớp tiện ích giúp cho việc lập lịch và kiểm soát việc thực thi một task vụ nào đó. Ví dụ bạn muốn dùng việc cập nhật thông trong 5 phút tới, hay thông tin chỉ được tồn tại 10h sau đó sẽ tự động bị xóa đi,… Một số hàm thông dụng trong Timer như:
• schedule lên lịch để thực thi task khi nào bắt đầu, kết thúc hay lặp lại, …
• cancel dừng timer và hủy tất cả các task đã lên lịch trong timer
• purge xóa tất cả các task đã dùng trong hàng đợi timer
TimerTask là một lớp trừu tượng implement Runnable interface, nó giúp cho việc lập lịch thực thi các thread
Ví dụ ta có một lớp MyTimerTask kế thừa từ TimerTask như bên dưới
class MyTimerTask extends TimerTask {
public void run() {
System.out.println("Running the scheduled task by Timer");
System.gc();
}
}
public class TestClass {
public static void main(String[] args) {
Timer timer = new Timer();
MyTimerTask task = new MyTimerTask();
timer.schedule(task, 3000, 2000);//Execute task after 3 senconds and then repeat in 2 seconds period
}
}
Kết quả in ra màn hình sau 5 giây sẽ là: Running the scheduled task by Timer
Running the scheduled task by Timer
1.4 Xử lý các vấn đề chúng trong multithread như synchronized, lock, dead lock, wait, notify …
Đồng bộ (Synchronization) là cơ chế giúp cho đồng bộ việc truy cập cùng tài nguyên của nhiều thread khác nhau. Như chúng ta biết vì cơ chế thread là cơ chế làm việc đồng thời do đó có khả năng một tài nguyên sẽ bị truy cập bởi nhiều thread khác nhau.
Ví dụ ta xây dựng 3 lớp Socola, Producer, Consumer. Lớp Socola chứa thông tin, lớp Producer tạo thông tin và gán vào cho Socola, Lớp Consumer sẽ ăn (tiêu thụ) thông tin đó.
public class TestClass {
public class Socola {
private int contents = 0;
public boolean isRemainingContents;
public synchronized int get() {
//print onto screen
System.out.println("\tConsumer get " + this.contents);
return contents;
}
public synchronized void set(int piContents) {
this.contents = piContents;
//print onto screen
System.out.println("Producer set " + this.contents);
}
}
public class Producer extends Thread {
private Socola socola;
Producer(Socola pSocola) {
socola = pSocola;
}
public void run() {
for(int i = 0; i < 10; i++) {
socola.set(i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
}
}
}
public class Consumer extends Thread {
private Socola socola;
Consumer(Socola pSocola) {
socola = pSocola;
}
public void run() {
for(int i = 0; i < 10; i++) {
socola.get();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
}
}
}
public static void main(String[] args) {
TestClass t = new TestClass();
Socola s = t.new Socola();
Producer p = t.new Producer(s);
Consumer c = t.new Consumer(s);
p.start();
c.start();
}
}
Kết quả in ra màn hinh sẽ không giống nhau tại các lần chạy, ví dụ 1 lần chạy:
Producer set 0
Consumer get 0
Producer set 1
Consumer get 1
Producer set 2
Consumer get 2
Producer set 3
Consumer get 3
Producer set 4
Consumer get 4
Consumer get 4
Producer set 5
Producer set 6
Consumer get 6
Producer set 7
Consumer get 7
Consumer get 7
Producer set 8
Consumer get 8
Producer set 9
Từ ví dụ trên ta thấy có chung một tài nguyên là Socola mà có 2 đối tượng cùng truy cập có thể sinh ra mâu thuẫn khi truy cập đồng thời. Bây giờ ta muốn là chỉ khi Producer tạo ra thông tin thì Consumer mới ăn thông tin đó, không muốn Consumer ăn nhiều lần cùng một thông tin, khi Producer đang tạo ra thông tin thì Consumer đứng chờ, khi nào Producer tạo ra thông tin xong thì Consumer mới vào ăn thông tin. vậy thì ta phải dùng cơ chế đồng bộ trong java với các từ khóa synchronized, wait(), notify(), notifyAll() và chỉnh lại 2 hàm get và set trong lớp Socola như sau:
public synchronized int get() {
while(isRemainingContents == false) {
try {
wait();
} catch (InterruptedException e) {
}
}
isRemainingContents = false;
//print onto screen
System.out.println("\tConsumer get " + this.contents);
//Notify Producer to produce new contents
notifyAll();
return contents;
}
public synchronized void set(int piContents) {
while(isRemainingContents == true) {
try {
wait();
} catch (InterruptedException e) {
}
}
this.contents = piContents;
isRemainingContents = true;
//print onto screen
System.out.println("Producer set " + this.contents);
//Notify Consumer to eat this contents
notifyAll();
}
}
Kết quả in ra luôn luôn như sau:
Producer set 0
Consumer get 0
Producer set 1
Consumer get 1
Producer set 2
Consumer get 2
Producer set 3
Consumer get 3
Producer set 4
Consumer get 4
Producer set 5
Consumer get 5
Producer set 6
Consumer get 6
Producer set 7
Consumer get 7
Producer set 8
Consumer get 8
Producer set 9
Consumer get 9
Có 3 hàm cần để ý:
• wait() đưa thread vào trạng thái chờ khi nào được notify() hoặc notifyAll() thì sẽ thực hiện tiếp
• notify() đánh thức thread đang đợi, nếu có nhiều thread đang đợi, thì một trong những thread đó được đánh thức thôi.
• notifyAll() đánh thức tất cả các thread đang đợi
Note: Nhiều khi chúng ta không cần hoặc không thể đồng bộ nguyên hàm (dùng synchronized) mà chỉ cần đồng bộ một vài dòng code truy cập tài nguyên, ta có thể dùng khối đồng bộ như sau:
synchronized(object) {
// Các cấu lệnh cần đồng bộ
}
Ví dụ: Khi bạn sử dụng lớp Socola là của khách hàng bạn đưa cho, bạn không thể chỉnh sửa định nghĩa hàm get và set trong Socola được vậy thì bạn có thể dùng khối đồng bộ bên trên cũng sẽ cho kết quả tương tự.
Deadlock (khóa chết) trong xử lý đa luồng xảy là khi mà một thread được đưa vào trạng thái chờ tài nguyên và không bao giờ được đánh thức lại vì tại nguyên đó không bao giờ được giải phóng. Để rõ hơn ta ví dụ A, B, C là thread trong trạng thái chờ; rA, rB, rC là tài nguyên dùng chung và chỉ duy nhất một thread sử dụng trong một thời điểm, tài nguyên chỉ được giải phóng khi thread làm xong và không sử dụng nữa.
• A giữ rA, chờ rB
• B giữ rB, chờ rC
• C giữ rC, chờ rA
Từ ví dụ trên ta thấy A, B, C là những thread đang ở trong trạng thái chờ tài nguyên, tuy nhiên tài nguyên của chúng không bao giờ được giải phóng, và mãi mãi chúng cứ chờ thôi. Để hạn chế deadlock thì chúng ta phải đưa ra, cải tiến việc cấp phát, thu hồi tài nguyên cho các thread và hiện này có khá nhiều thuật toán cải tiến việc này, tuy nhiên không có cái nào giúp tránh hoàn toàn deadlock.
1.5 Những lớp tiện ích trong lập trình đa luồng (multithread)
Trong java đã xây dựng một vài lớp giúp hỗ trợ lập trình đa luồng, chúng được đặt trong gói Concurent như: Semaphore, Lock, Condition, ConcurrentMap, ConcurrentLinkedQueue, …
Semaphore là một lớp tiện ích đã xây dựng trong java (java 5) dùng để giới hạn số lượng thread truy cập đến tài nguyên. Đầu tiên thread cần yêu cầu sự chấp nhận truy cập tài nguyên từ semaphore bằng hàm acquire, và thread sẽ về trạng thái đợi (block) cho đến khi được chấp nhận, khi thread được chấp nhận nó thực hiện công việc của nó xong sẽ gọi hàm release để trả về cho semaphore một sự chấp nhận còn trống để semaphore cấp cho thread khác.
Lock là một interface được xây dựng nhằm mở rộng hơn việc quản lý truy cập tài nguyên trong nhiều thread hơn so với cách dùng synchronized chẳng hạn như có thể hỗ trợ nhiều điều kiện (condition object) trong lock, hoặc hỗ trợ nhiều thuật toán cho chain-lock để tránh deadlock tốt hơn, … Khi dùng Lock chúng ta nên theo một template chuẩn như sau:
Lock l = ...;
l.lock();
try {
// Các task truy cập tài nguyên bảo vệ bởi lock đặt ở đây
} finally {
l.unlock();
}
Condition là một interface dùng để nhóm cách xử lý trong các phương thức (wait, notify, notifyAll) của lớp Object vào thành một đối tượng nhằm đưa ra nhiều chọn lựa hơn trong việc đồng bộ, ví dụ chúng ta có thể dùng nhiều condition và khi nào chúng ta muốn đánh thức chính xác điều kiện nào được sử dụng tài nguyên.
Ví dụ ta dùng lại ví dụ Consumer-Producer ở trên, thêm vào 2 biến lock, condition và chỉnh sửa hai làm get và put thế là ta cũng được kết quả như dùng với từ khóa synchronized:
final Lock lock = new ReentrantLock();
final Condition condition = lock.newCondition();
public int get() {
int result;
lock.lock();
try {
while(isRemainingContents == false) {
try {
condition.await();
} catch (InterruptedException e) {
}
}
isRemainingContents = false;
//print onto screen
System.out.println("\tConsumer get " + this.contents);
//get value
result = contents;
//Notify Producer to produce new contents
condition.signalAll();
} finally {
lock.unlock();
}
return result;
}
public void set(int piContents) {
lock.lock();
try {
while(isRemainingContents == true) {
try {
condition.await();
} catch (InterruptedException e) {
}
}
this.contents = piContents;
isRemainingContents = true;
//print onto screen
System.out.println("Producer set " + this.contents);
//Notify Consumer to eat this contents
condition.signalAll();
} finally {
lock.unlock();
}
}
Kết quả luôn luôn là thế này:
Producer set 0
Consumer get 0
Producer set 1
Consumer get 1
Producer set 2
Consumer get 2
Producer set 3
Consumer get 3
Producer set 4
Consumer get 4
Producer set 5
Consumer get 5
Producer set 6
Consumer get 6
Producer set 7
Consumer get 7
Producer set 8
Consumer get 8
Producer set 9
Consumer get 9
ConcurrentMap là một interface kế thừa từ Map dùng để cải tiến, tối ưu hơn khi chạy Map trong ứng dụng đa luồng (multithread)
Ví dụ cách dùng ConcurrentHashMap tương tự dùng Map, tuy nhiên người ta đã thống kê là nếu chương trình có càng nhiều thread truy cập vào cMap này thì dùng ConcurrentMap sẽ cho performance tốt hơn rất nhiều so với dùng Map với đồng bộ kiểu synchronized
public static ConcurrentMap<String,String> cMap =
new ConcurrentHashMap<String, String>(1000);
public static void main(String[] args) {
cMap.putIfAbsent("id", "1");
cMap.putIfAbsent("name", "kien");
cMap.putIfAbsent("email", "nakien2a@yahoo.com");
System.out.println(cMap); //Print to screen
}
Kết quả: {name=kien, email=nakien2a@yahoo.com, id=1}
Thuận lợi của lập trình đa luồng
Thuận lợi của lập trình đa luồng
Thuận lợi
Những thuận lợi của lập trình đa luồng có thể được chia làm bốn loại:
• Sự đáp ứng: đa luồng một ứng dụng giao tiếp cho phép một chương trình tiếp tục chạy thậm chí nếu một phần của nó bị khóa hay đang thực hiện một thao tác dài, do đó gia tăng sự đáp ứng đối với người dùng. Thí dụ, một trình duyệt web vẫn có thể đáp ứng người dùng bằng một luồng trong khi một ảnh đang được nạp bằng một luồng khác.
• Chia sẻ tài nguyên: Mặc định, các luồng chia sẻ bộ nhớ và các tài nguyên của các quá trình mà chúng thuộc về. Thuận lợi của việc chia sẽ mã là nó cho phép một ứng dụng có nhiều hoạt động của các luồng khác nhau nằm trong cùng không gian địa chỉ.
• Kinh tế: cấp phát bộ nhớ và các tài nguyên cho việc tạo các quá trình là rất đắt. Vì các luồng chia sẻ tài nguyên của quá trình mà chúng thuộc về nên nó kinh tế hơn để tạo và chuyển ngữ cảnh giữa các luồng. Khó để đánh giá theo kinh nghiệm sự khác biệt chi phí cho việc tạo và duy trì một quá trình hơn một luồng, nhưng thường nó sẽ mất nhiều thời gian để tạo và quản lý một quá trình hơn một luồng. Trong Solaris 2, tạo một quá trình chậm hơn khoảng 30 lần tạo một luồng và chuyển đổi ngữ cảnh chậm hơn 5 lần.
• Sử dụng kiến trúc đa xử lý: các lợi điểm của đa luồng có thể phát huy trong kiến trúc đa xử lý, ở đó mỗi luồng thực thi song song trên một bộ xử lý khác nhau. Một quá trình đơn luồng chỉ có thể chạy trên một CPU. Đa luồng trên một máy nhiều CPU gia tăng tính đồng hành. Trong kiến trúc đơn xử lý, CPU thường chuyển đổi qua lại giữa mỗi luồng quá nhanh để tạo ra hình ảnh của sự song song nhưng trong thực tế chỉ một luồng đang chạy tại một thời điểm.
Thuận lợi
Những thuận lợi của lập trình đa luồng có thể được chia làm bốn loại:
• Sự đáp ứng: đa luồng một ứng dụng giao tiếp cho phép một chương trình tiếp tục chạy thậm chí nếu một phần của nó bị khóa hay đang thực hiện một thao tác dài, do đó gia tăng sự đáp ứng đối với người dùng. Thí dụ, một trình duyệt web vẫn có thể đáp ứng người dùng bằng một luồng trong khi một ảnh đang được nạp bằng một luồng khác.
• Chia sẻ tài nguyên: Mặc định, các luồng chia sẻ bộ nhớ và các tài nguyên của các quá trình mà chúng thuộc về. Thuận lợi của việc chia sẽ mã là nó cho phép một ứng dụng có nhiều hoạt động của các luồng khác nhau nằm trong cùng không gian địa chỉ.
• Kinh tế: cấp phát bộ nhớ và các tài nguyên cho việc tạo các quá trình là rất đắt. Vì các luồng chia sẻ tài nguyên của quá trình mà chúng thuộc về nên nó kinh tế hơn để tạo và chuyển ngữ cảnh giữa các luồng. Khó để đánh giá theo kinh nghiệm sự khác biệt chi phí cho việc tạo và duy trì một quá trình hơn một luồng, nhưng thường nó sẽ mất nhiều thời gian để tạo và quản lý một quá trình hơn một luồng. Trong Solaris 2, tạo một quá trình chậm hơn khoảng 30 lần tạo một luồng và chuyển đổi ngữ cảnh chậm hơn 5 lần.
• Sử dụng kiến trúc đa xử lý: các lợi điểm của đa luồng có thể phát huy trong kiến trúc đa xử lý, ở đó mỗi luồng thực thi song song trên một bộ xử lý khác nhau. Một quá trình đơn luồng chỉ có thể chạy trên một CPU. Đa luồng trên một máy nhiều CPU gia tăng tính đồng hành. Trong kiến trúc đơn xử lý, CPU thường chuyển đổi qua lại giữa mỗi luồng quá nhanh để tạo ra hình ảnh của sự song song nhưng trong thực tế chỉ một luồng đang chạy tại một thời điểm.
Tạo luồng từ lớp Thread trong Java
Ngoài việc tạo luồng (thread) bằng cách thi công giao diện Runnable như đã trình bày trong bài trước, bạn còn có thể tạo luồng để hiển thị hình ảnh trong chương trình Slide Show bằng cách kế thừa từ lớp Thread có sẵn trong Java. Bạn hãy chép đoạn mã 1, biên dịch và chạy thử để xem kết quả trên màn hình ra sao (H1). Qua đoạn mã 1, bạn cũng cần chú ý kỹ thuật đồng bộ đa luồng (multi-thread synchronization) trong Java.
Đoạn mã 1
// ThreadSync.java
class MyThread extends Thread {
static String message[]={"I","love","Java."};
public MyThread(String id) {
// Gọi hàm dựng (constructor) của lớp Thread cha
super(id);
}
public void run() {
Sync.displayList(getName(), message);
}
void waiting() {
try {
sleep(2000);
}
catch(InterruptedException e){
System.out.println("Interrupted");
}
}
};
class Sync {
public static synchronized void displayList(String name, String list[]) {
/* Nếu không dùng từ khóa synchronized thì sẽ không
đảm bào một luồng làm xong hết công việc của nó
rồi một luồng khác mới được bắt đầu chạy.
*/
for (int i=0; i< list.length; ++i) {
MyThread thread = (MyThread)Thread.currentThread();
// Hàm waiting đã định nghĩa ở trên
thread.waiting();
System.out.println(name + list[i]);
}
}
};
public class ThreadSync {
public static void main(String args[]) {
MyThread thread1 = new MyThread("Thread 1: ");
MyThread thread2 = new MyThread("Thread 2: ");
thread1.start(); // Hàm run của thread1 được gọi
thread2.start(); // Hàm run của thread2 được gọi
}
};
Đoạn mã 1
// ThreadSync.java
class MyThread extends Thread {
static String message[]={"I","love","Java."};
public MyThread(String id) {
// Gọi hàm dựng (constructor) của lớp Thread cha
super(id);
}
public void run() {
Sync.displayList(getName(), message);
}
void waiting() {
try {
sleep(2000);
}
catch(InterruptedException e){
System.out.println("Interrupted");
}
}
};
class Sync {
public static synchronized void displayList(String name, String list[]) {
/* Nếu không dùng từ khóa synchronized thì sẽ không
đảm bào một luồng làm xong hết công việc của nó
rồi một luồng khác mới được bắt đầu chạy.
*/
for (int i=0; i< list.length; ++i) {
MyThread thread = (MyThread)Thread.currentThread();
// Hàm waiting đã định nghĩa ở trên
thread.waiting();
System.out.println(name + list[i]);
}
}
};
public class ThreadSync {
public static void main(String args[]) {
MyThread thread1 = new MyThread("Thread 1: ");
MyThread thread2 = new MyThread("Thread 2: ");
thread1.start(); // Hàm run của thread1 được gọi
thread2.start(); // Hàm run của thread2 được gọi
}
};
Đa luồng và Đa tiến trinh
Đa luồng và Đa tiến trinh
Đa luồng dùng chung các tiến trình trong hệ thống, quản trị công việc bên trong cấp phát giữa các tiến trình. (vd: tài nguyên dùng chung là bảng lớp học)
Tiến trình truyền thống dùng chung các tiến trình bên ngoài. Lập trình đa luồng dễ hơn lập trình đa tiến trình (như tạo mới luồng, chia sẻ, điều khiển dễ hơn..)
Giống nhau:
Đều có thông tin trạng thái
Luồng còn được gọi là tiến trình nhẹ
Nhiều luồng hoặc nhiều tiến trình có thể liên quan đến một chu trình
Chia sẻ tài nguyên cho nhau, tăng tốc tính toán, đảm bảo tính đơn thể
Khác nhau:
Do luồng cùng vận hành trong 1 vùng địa chỉ, vì vậy dễ dùng chung tài nguyên hơn đa tiến trình ví dụ mượn đồ trong gia đình dễ mượn đồ hơn của hàng xóm
Do luồng chung tài nguyên với luồng cha nên việc tạo lập và chuyển đổi ngữ cảnh cũng nhanh hơn tiến trình
Ví dụ: việc ngăn riêng ra một phòng trong nhà thì dễ hơn được nhà nước cấp cho một căn hộ
Cấp phát tài nguyên cho bộ nhớ thì tốn kém
Đa luồng dùng chung các tiến trình trong hệ thống, quản trị công việc bên trong cấp phát giữa các tiến trình. (vd: tài nguyên dùng chung là bảng lớp học)
Tiến trình truyền thống dùng chung các tiến trình bên ngoài. Lập trình đa luồng dễ hơn lập trình đa tiến trình (như tạo mới luồng, chia sẻ, điều khiển dễ hơn..)
Giống nhau:
Đều có thông tin trạng thái
Luồng còn được gọi là tiến trình nhẹ
Nhiều luồng hoặc nhiều tiến trình có thể liên quan đến một chu trình
Chia sẻ tài nguyên cho nhau, tăng tốc tính toán, đảm bảo tính đơn thể
Khác nhau:
Do luồng cùng vận hành trong 1 vùng địa chỉ, vì vậy dễ dùng chung tài nguyên hơn đa tiến trình ví dụ mượn đồ trong gia đình dễ mượn đồ hơn của hàng xóm
Do luồng chung tài nguyên với luồng cha nên việc tạo lập và chuyển đổi ngữ cảnh cũng nhanh hơn tiến trình
Ví dụ: việc ngăn riêng ra một phòng trong nhà thì dễ hơn được nhà nước cấp cho một căn hộ
Cấp phát tài nguyên cho bộ nhớ thì tốn kém
Lập Trình Đa Luồng Trong JAVA
Thread có một định nghĩa về thread như sau :
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
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
Đa Luồng là gì ?
Đa Luồng là gì ?
Luồng - Thread: Một dòng các lệnh mà CPU phải thực thi.
Các hệ điều hành mới cho phép nhiều luồng được thực thi đồng thời. Chúng ta đã quen với việc mở nhiều ứng dụng trong một lần làm việc với máy tính.
- Một luồng là một chuỗi các lệnh nằm trong bộ nhớ (chương trình đã được nạp).
- 1 application thông thường khi thực thi là một luồng.
- Trong 1 application có thể có nhiều luồng. Thí dụ chuyển động của 10 đối tượng hiện hành trong 1 trò chơi là một luồng.
Lợi ích của đa luồng:
- Tăng hiệu suất sử dụng CPU: Phần lớn thời gian thực thi của 1 ứng dụng là chờ đợi nhập liệu từ user -> hiệu suất sử dụng CPU chưa hiệu quả.
- Tạo được sự đồng bộ giữa các đối tượng: thí dụ như trong 1 trò chơi, các nhân vật cùng nhau chuyển động.
- Quản lý được thời gian trong các ứng dụng nhu thi online, thời gian chơi một trò chơi.
Luồng - Thread: Một dòng các lệnh mà CPU phải thực thi.
Các hệ điều hành mới cho phép nhiều luồng được thực thi đồng thời. Chúng ta đã quen với việc mở nhiều ứng dụng trong một lần làm việc với máy tính.
- Một luồng là một chuỗi các lệnh nằm trong bộ nhớ (chương trình đã được nạp).
- 1 application thông thường khi thực thi là một luồng.
- Trong 1 application có thể có nhiều luồng. Thí dụ chuyển động của 10 đối tượng hiện hành trong 1 trò chơi là một luồng.
Lợi ích của đa luồng:
- Tăng hiệu suất sử dụng CPU: Phần lớn thời gian thực thi của 1 ứng dụng là chờ đợi nhập liệu từ user -> hiệu suất sử dụng CPU chưa hiệu quả.
- Tạo được sự đồng bộ giữa các đối tượng: thí dụ như trong 1 trò chơi, các nhân vật cùng nhau chuyển động.
- Quản lý được thời gian trong các ứng dụng nhu thi online, thời gian chơi một trò chơi.
Luồng người dùng và luồng nhân &Mô hình đa luồng
Chúng ta vừa mới thảo luận là xem xét luồng như một chiều hướng chung. Tuy
nhiên, hỗ trợ luồng được cung cấp hoặc ở cấp người dùng, cho các luồng người dùng
hoặc ở cấp nhân, cho các luồng nhân.
• Luồng người dùng: được hỗ trợ dưới nhân và được cài đặt bởi thư viện luồng
tại cấp người dùng. Thư viện cung cấp hỗ trợ cho việc tạo luồng, lập thời biểu,
và quản lý mà không có sự hỗ trợ từ nhân. Vì nhân không biết các luồng cấp
người dùng, tất cả việc tạo luồng và lập thời biểu được thực hiện trong không
gian người dùng mà không cần sự can thiệp của nhân. Do đó, các luồng cấp
người dùng thường tạo và quản lý nhanh, tuy nhiên chúng cũng có những trở
ngại. Thí dụ, nếu nhân là đơn luồng thì bất cứ luồng cấp người dùng thực hiện
một lời gọi hệ thống nghẽn sẽ làm cho toàn bộ quá trình bị nghẽn, thậm chí
nếu các luồng khác sẳn dùng để chạy trong ứng dụng. Các thư viện luồng
người dùng gồm các luồng POSIX Pthreads, Mach C-threads và Solaris 2 UI-
threads.
• Luồng nhân: được hỗ trợ trực tiếp bởi hệ điều hành. Nhân thực hiện việc tạo
luồng, lập thời biểu, và quản lý không gian nhân. Vì quản lý luồng được thực
hiện bởi hệ điều hành, luồng nhân thường tạo và quản lý chậm hơn luồng
người dùng. Tuy nhiên, vì nhân được quản lý các luồng nếu một luồng thực
hiện lời gọi hệ thống nghẽn, nhân có thể lập thời biểu một luồng khác trong
ứng dụng thực thi. Trong môi trường đa xử lý, nhân có thể lập thời biểu luồng
trên một bộ xử lý khác. Hầu hết các hệ điều hành hiện nay như Windows NT,
Windows 2000, Solaris 2, BeOS và Tru64 UNIX (trước Digital UNIX)-hỗ trợ
các luồng nhân
Nhiều hệ thống cung cấp sự hỗ trợ cả hai luồng nhân và luồng người dùng nên
tạo ra nhiều mô hình đa luồng khác nhau. Chúng ta sẽ xem xét ba loại cài đặt luồng
thông thường :
1. Mô hình nhiều-một
Mô hình nhiều-một (như hình IV.2) ánh xạ nhiều luồng cấp người dùng tới
một luồng cấp nhân. Quản lý luồng được thực hiện trong không gian người dùng vì
thế nó hiệu quả nhưng toàn bộ quá trình sẽ bị khóa nếu một luồng thực hiện lời gọi hệ
thống khóa. Vì chỉ một luồng có thể truy xuất nhân tại một thời điểm nên nhiều luồng
không thể chạy song song trên nhiều bộ xử lý. Green threads-một thư viện luồng được
cài đặt trên các hệ điều hành không hỗ trợ luồng nhân dùng mô hình nhiều-một.
2. Mô hình một-một
Mô hình một-một (hình IV.3) ánh xạ mỗi luồng người dùng tới một luồng
nhân. Nó cung cấp khả năng đồng hành tốt hơn mô hình nhiều-một bằng cách cho một
luồng khác chạy khi một luồng thực hiện lời gọi hệ thống nghẽn; nó cũng cho phép
nhiều luồng chạy song song trên các bộ xử lý khác nhau. Chỉ có một trở ngại trong
mô hình này là tạo luồng người dùng yêu cầu tạo một luồng nhân tương ứng. Vì chi
phí cho việc tạo luồng nhân có thể đè nặng lên năng lực thực hiện của ứng dụng, các
cài đặt cho mô hình này giới hạn số luồng được hỗ trợ bởi hệ thống. Windows NT,
Windows 2000 và OS/2 cài đặt mô hình một-một này.
3 Mô hình nhiều-nhiều
Mô hình nhiều-nhiều (như hình VI.4) đa hợp nhiều luồng cấp người dùng tới
số lượng nhỏ hơn hay bằng các luồng nhân. Số lượng các luồng nhân có thể được xác
định hoặc một ứng dụng cụ thể hay một máy cụ thể (một ứng dụng có thể được cấp
nhiều luồng nhân trên một bộ đa xử lý hơn trên một bộ đơn xử lý). Trong khi mô hình
nhiều-một cho phép người phát triển tạo nhiều luồng người dùng như họ muốn, thì
đồng hành thật sự là không đạt được vì nhân có thể lập thời biểu chỉ một luồng tại một
thời điểm. Mô hình một-một cho phép đồng hành tốt hơn nhưng người phát triển phải
cẩn thận không tạo ra quá nhiều luồng trong một ứng dụng. Mô hình nhiều-nhiều gặp
phải một trong hai vấn đề khiếm khuyết: người phát triển có thể tạo nhiều luồng
người dùng khi cần thiết và các luồng nhân tương ứng có thể chạy song song trên một
bộ đa xử lý. Khi một luồng thực hiện một lời gọi hệ thống khóa, nhân có thể lập thời
biểu một luồng khác thực thi. Solaris 2, IRIX, HP-UX, và Tru64 UNIX hỗ trợ mô
hình này.
nhiên, hỗ trợ luồng được cung cấp hoặc ở cấp người dùng, cho các luồng người dùng
hoặc ở cấp nhân, cho các luồng nhân.
• Luồng người dùng: được hỗ trợ dưới nhân và được cài đặt bởi thư viện luồng
tại cấp người dùng. Thư viện cung cấp hỗ trợ cho việc tạo luồng, lập thời biểu,
và quản lý mà không có sự hỗ trợ từ nhân. Vì nhân không biết các luồng cấp
người dùng, tất cả việc tạo luồng và lập thời biểu được thực hiện trong không
gian người dùng mà không cần sự can thiệp của nhân. Do đó, các luồng cấp
người dùng thường tạo và quản lý nhanh, tuy nhiên chúng cũng có những trở
ngại. Thí dụ, nếu nhân là đơn luồng thì bất cứ luồng cấp người dùng thực hiện
một lời gọi hệ thống nghẽn sẽ làm cho toàn bộ quá trình bị nghẽn, thậm chí
nếu các luồng khác sẳn dùng để chạy trong ứng dụng. Các thư viện luồng
người dùng gồm các luồng POSIX Pthreads, Mach C-threads và Solaris 2 UI-
threads.
• Luồng nhân: được hỗ trợ trực tiếp bởi hệ điều hành. Nhân thực hiện việc tạo
luồng, lập thời biểu, và quản lý không gian nhân. Vì quản lý luồng được thực
hiện bởi hệ điều hành, luồng nhân thường tạo và quản lý chậm hơn luồng
người dùng. Tuy nhiên, vì nhân được quản lý các luồng nếu một luồng thực
hiện lời gọi hệ thống nghẽn, nhân có thể lập thời biểu một luồng khác trong
ứng dụng thực thi. Trong môi trường đa xử lý, nhân có thể lập thời biểu luồng
trên một bộ xử lý khác. Hầu hết các hệ điều hành hiện nay như Windows NT,
Windows 2000, Solaris 2, BeOS và Tru64 UNIX (trước Digital UNIX)-hỗ trợ
các luồng nhân
Nhiều hệ thống cung cấp sự hỗ trợ cả hai luồng nhân và luồng người dùng nên
tạo ra nhiều mô hình đa luồng khác nhau. Chúng ta sẽ xem xét ba loại cài đặt luồng
thông thường :
1. Mô hình nhiều-một
Mô hình nhiều-một (như hình IV.2) ánh xạ nhiều luồng cấp người dùng tới
một luồng cấp nhân. Quản lý luồng được thực hiện trong không gian người dùng vì
thế nó hiệu quả nhưng toàn bộ quá trình sẽ bị khóa nếu một luồng thực hiện lời gọi hệ
thống khóa. Vì chỉ một luồng có thể truy xuất nhân tại một thời điểm nên nhiều luồng
không thể chạy song song trên nhiều bộ xử lý. Green threads-một thư viện luồng được
cài đặt trên các hệ điều hành không hỗ trợ luồng nhân dùng mô hình nhiều-một.
2. Mô hình một-một
Mô hình một-một (hình IV.3) ánh xạ mỗi luồng người dùng tới một luồng
nhân. Nó cung cấp khả năng đồng hành tốt hơn mô hình nhiều-một bằng cách cho một
luồng khác chạy khi một luồng thực hiện lời gọi hệ thống nghẽn; nó cũng cho phép
nhiều luồng chạy song song trên các bộ xử lý khác nhau. Chỉ có một trở ngại trong
mô hình này là tạo luồng người dùng yêu cầu tạo một luồng nhân tương ứng. Vì chi
phí cho việc tạo luồng nhân có thể đè nặng lên năng lực thực hiện của ứng dụng, các
cài đặt cho mô hình này giới hạn số luồng được hỗ trợ bởi hệ thống. Windows NT,
Windows 2000 và OS/2 cài đặt mô hình một-một này.
3 Mô hình nhiều-nhiều
Mô hình nhiều-nhiều (như hình VI.4) đa hợp nhiều luồng cấp người dùng tới
số lượng nhỏ hơn hay bằng các luồng nhân. Số lượng các luồng nhân có thể được xác
định hoặc một ứng dụng cụ thể hay một máy cụ thể (một ứng dụng có thể được cấp
nhiều luồng nhân trên một bộ đa xử lý hơn trên một bộ đơn xử lý). Trong khi mô hình
nhiều-một cho phép người phát triển tạo nhiều luồng người dùng như họ muốn, thì
đồng hành thật sự là không đạt được vì nhân có thể lập thời biểu chỉ một luồng tại một
thời điểm. Mô hình một-một cho phép đồng hành tốt hơn nhưng người phát triển phải
cẩn thận không tạo ra quá nhiều luồng trong một ứng dụng. Mô hình nhiều-nhiều gặp
phải một trong hai vấn đề khiếm khuyết: người phát triển có thể tạo nhiều luồng
người dùng khi cần thiết và các luồng nhân tương ứng có thể chạy song song trên một
bộ đa xử lý. Khi một luồng thực hiện một lời gọi hệ thống khóa, nhân có thể lập thời
biểu một luồng khác thực thi. Solaris 2, IRIX, HP-UX, và Tru64 UNIX hỗ trợ mô
hình này.
Cấp phát luồng
1 Lời gọi hệ thống fork và exec
Trong chương trước chúng ta mô tả lời gọi hệ thống fork được dùng để tạo
một quá trình bản sao riêng như thế nào. Trong một chương trình đa luồng, ngữ nghĩa
của các lời gọi hệ thống fork và exec thay đổi. Nếu một luồng trong lời gọi chương
trình fork thì quá trình mới sao chép lại quá trình tất cả luồng hay là một quá trình đơn
luồng mới? Một số hệ thống UNIX chọn hai ấn bản fork, một sao chép lại tất cả luồng
và một sao chép lại chỉ luồng được nạp lên lời gọi hệ thống fork. Lời gọi hệ thống
exec điển hình thực hiện công việc trong cùng một cách như được mô tả trong chương
trước. Nghĩa là, nếu một luồng nạp lời gọi hệ thống exec, chương trình được xác định
trong tham số exec sẽ thay thế toàn bộ quá trình-chứa tất cả luồng và các quá trình tải
nhẹ.
Việc sử dụng hai ấn bản fork phụ thuộc vào ứng dụng. Nếu exec bị hủy tức thì
sau khi phân nhánh (forking) thì sự sao chép lại tất cả luồng là không cần thiết khi
chương trình được xác định trong các tham số exec sẽ thay thế quá trình. Trong
trường hợp này, việc sao chép lại chỉ gọi luồng hợp lý. Tuy nhiên, nếu quá trình riêng
biệt này không gọi exec sau khi phân nhánh thì quá trình riêng biệt này nên sao chép
lại tất cả luồng.
2 Sự hủy bỏ luồng
Hủy một luồng là một tác vụ kết thúc một luồng trước khi nó hoàn thành.Thí
dụ, nếu nhiều luồng đang tìm kiếm đồng thời thông qua một cơ sở dữ liệu và một
luồng trả về kết quả, các luồng còn lại có thể bị hủy. Một trường hợp khác có thể xảy
ra khi người dùng nhấn một nút trên trình duyệt web để dừng trang web đang được
tải. Thường một trang web được tải trong một luồng riêng. Khi người dùng nhấn nút
stop, luồng đang nạp trang bị hủy bỏ.
Một luồng bị hủy thường được xem như luồng đích. Sự hủy bỏ một luồng đích có
thể xảy ra hai viễn cảnh khác nhau:
• Hủy bất đồng bộ: một luồng lập tức kết thúc luồng đích
• Hủy trì hoãn: luồng đích có thể kiểm tra định kỳ nếu nó sắp kết thúc, cho
phép luồng đích một cơ hội tự kết thúc trong một cách có thứ tự.
Sự khó khăn của việc hủy này xảy ra trong những trường hợp khi tài nguyên
được cấp phát tới một luồng bị hủy hay một luồng bị hủy trong khi việc cập nhật dữ
liệu xảy ra giữa chừng, nó đang chia sẻ với các luồng khác. Điều này trở nên đặc biệt
khó khăn với sự hủy bất đồng bộ. Hệ điều hành thường đòi lại tài nguyên hệ thống từ
luồng bị hủy nhưng thường nó sẽ không đòi lại tất cả tài nguyên. Do đó, việc hủy một
luồng bất đồng bộ có thể không giải phóng hết tài nguyên hệ thống cần thiết.
Một chọn lựa khác, sự hủy trì hoãn thực hiện bằng một luồng báo hiệu rằng
một luồng đích bị hủy. Tuy nhiên, sự hủy sẽ xảy ra chỉ khi luồng đích kiểm tra để xác
định nếu nó được hủy hay không. Điều này cho phép một luồng kiểm tra nếu nó sẽ bị
hủy tại điểm nó có thể an toàn bị hủy. Pthreads gọi những điểm như thế là các điểm
hủy (cancellation points).
Hầu hết hệ điều hành cho phép một quá trình hay một luồng bị hủy bất đồng
bộ. Tuy nhiên, Pthread API cung cấp sự hủy trì hoãn. Điều này có nghĩa rằng một hệ
điều hành cài đặt Pthread API sẽ cho phép sự hủy có trì hoãn.
3 Tín hiệu quản lý
Một tín hiệu (signal) được dùng trong hệ điều hành UNIX thông báo một sự
kiện xác định xảy ra. Một tín hiệu có thể được nhận hoặc đồng bộ hoặc bất đồng bộ
phụ thuộc mã và lý do cho sự kiện đang được báo hiệu. Một tín hiệu hoặc đồng bộ
hoặc bất đồng bộ đều theo sau cùng mẫu:
• Tín hiệu được phát sinh bởi sự xảy ra của một sự kiện xác định.
• Tín hiệu được phát sinh được phân phát tới một quá trình.
• Khi được phân phát xong, tín hiệu phải được quản lý.
Một thí dụ của tín hiệu đồng bộ gồm một truy xuất bộ nhớ không hợp lệ hay
chia cho 0. Trong trường hợp này, nếu một chương trình đang chạy thực hiện một
trong các hoạt động này, một tín hiệu được phát sinh. Các tín hiệu đồng bộ được phân
phát tới cùng một quá trình thực hiện thao tác gây ra tín hiệu (do đó lý do chúng được
xem đồng bộ).
Khi một tín hiệu được phát sinh bởi một sự kiện bên ngoài tới một quá trình
đang chạy, quá trình đó nhận tín hiệu bất đồng bộ. Thí dụ, tín hiệu kết thúc quá trình
với phím xác định (như <control><C>) hay thời gian hết hạn. Điển hình, một tín hiệu
bất đồng bộ được gởi tới quá trình khác.
Mỗi tín hiệu có thể được quản lý bởi một trong hai bộ quản lý:
• Bộ quản lý tín hiệu mặc định
• Bộ quản lý tín hiệu được định nghĩa bởi người dùng
Mỗi tín hiệu có một bộ quản lý tín hiệu mặc định được thực thi bởi nhân khi
quản lý tín hiệu. Hoạt động mặc định có thể được ghi đè bởi một hàm quản lý tín hiệu
được định nghĩa bởi người dùng. Trong trường hợp này, hàm được định nghĩa bởi
người dùng được gọi để quản lý tín hiệu hơn là hoạt động mặc định. Cả hai tín hiệu
đồng bộ và bất đồng bộ có thể được quản lý trong các cách khác nhau. Một số tín hiệu
có thể được bỏ qua (như thay đổi kích thước của cửa sổ); các tín hiệu khác có thể
được quản lý bằng cách kết thúc chương trình (như truy xuất bộ nhớ không hợp lệ).
Quản lý tín hiệu trong những chương trình đơn luồng không phức tạp; các tín
hiệu luôn được phân phát tới một quá trình. Tuy nhiên, phân phát tín hiệu là phức tạp
hơn trong những chương trình đa luồng, như một quá trình có nhiều luồng. Một tín
hiệu nên được phân phát ở đâu?
Thông thường, các tuỳ chọn sau tồn tại:
• Phân phát tín hiệu tới luồng mà tín hiệu áp dụng
• Phân phát tín hiệu tới mỗi luồng trong quá trình.
• Phân phát tín hiệu tới các luồng cụ thể trong quá trình.
• Gán một luồng xác định để nhận tất cả tín hiệu cho quá trình.
Phương pháp cho việc phân phát tín hiệu phụ thuộc vào loại tín hiệu được phát
sinh. Thí dụ, các tín hiệu đồng bộ cần được phân phát tới luồng đã phát sinh ra tín
hiệu và không phân phát tới luồng nào khác trong quá trình. Tuy nhiên, trường hợp
với tín hiệu bất đồng bộ là không rõ ràng. Một số tín hiệu bất đồng bộ - như tín hiệu
kết thúc một quá trình (thí dụ:<control><c>)- nên được gởi tới tất cả luồng. Một số ấn
bản đa luồng của UNIX cho phép một luồng xác định tín hiệu nào sẽ được chấp nhận
và tín hiệu nào sẽ bị khoá. Do đó, một vài tín hiệu bất đồng bộ có thể được phân phát
tới chỉ các luồng không khoá tín hiệu. Tuy nhiên, vì tín hiệu cần được quản lý chỉ một
lần, điển hình một tín hiệu được phân phát chỉ luồng đầu tiên được tìm thấy trong một
luồng mà không nghẽn tín hiệu. Solaris 2 cài đặt bốn tuỳ chọn; nó tạo một luồng xác
định trong mỗi quá trình cho quản lý tín hiệu. Khi một tín hiệu bất đồng bộ được gởi
tới một quá trình, nó được gởi tới luồng xác định, sau đó nó phân phát tín hiệu tới
luồng đầu tiên không khoá tín hiệu.
Mặc dù Windows 2000 không cung cấp rõ sự hỗ trợ tín hiệu, nhưng chúng có
thể được mô phỏng sử dụng lời gọi thủ tục bất đồng bộ (asynchronous produce calls-
APC). Tiện ích APC cho phép luồng người dùng xác định hàm được gọi khi luồng
người dùng nhận thông báo về một sự kiện xác định. Như được hiển thị bởi tên của
nó, một APC rất giống tín hiệu bất đồng bộ trong UNIX. Tuy nhiên, UNIX phải đấu
tranh với cách giải quyết tín hiệu trong môi trường đa luồng, phương tiện APC phức
tạp hơn như một APC được phân phát tới luồng xác định hơn quá trình.
4 Nhóm luồng
Trong phần VI.3, chúng ta mô tả kịch bản đa luồng của một trình phục vụ
web. Trong trường hợp này, bất cứ khi nào trình phục vụ nhận một yêu cầu, nó tạo
một luồng riêng để phục vụ yêu cầu đó. Ngược lại, tạo một luồng riêng thật sự cao
hơn tạo một quá trình riêng, dù sao một trình phục vụ đa luồng có thể phát sinh vấn
đề. Quan tâm đầu tiên là lượng thời gian được yêu cầu để tạo luồng trước khi phục vụ
yêu cầu, và lượng thời gian xoá luồng khi nó hoàn thành. Vấn đề thứ hai là vấn đề khó
giải quyết hơn: nếu chúng ta cho phép tất cả yêu cầu đồng hành được phục vụ trong
một luồng mới, chúng ta không thay thế giới hạn trên số lượng luồng hoạt động đồng
hành trong hệ thống. Những luồng không giới hạn có thể làm cạn kiệt tài nguyên hệ
thống, như thời gian CPU và bộ nhớ. Một giải pháp cho vấn đề này là sử dụng nhóm
luồng.
Ý tưởng chung nằm sau nhóm luồng là tạo số lượng luồng tại thời điểm khởi
động và đặt chúng vào nhóm, nơi chúng ngồi và chờ công việc. Khi một trình phục vụ
nhận một yêu cầu, chúng đánh thức một luồng từ nhóm- nếu một luồng sẳn dùng –
truyền nó yêu cầu dịch vụ. Một khi luồng hoàn thành dịch vụ của nó, nó trả về nhóm
đang chờ công việc kế. Nếu nhóm không chứa luồng sẳn dùng, trình phục vụ chờ cho
tới khi nó rảnh.
Nói cụ thể, các lợi ích của nhóm luồng là:
1) Thường phục vụ yêu cầu nhanh hơn với luồng đã có hơn là chờ để tạo luồng.
2) Một nhóm luồng bị giới hạn số lượng luồng tồn tại bất kỳ thời điểm nào. Điều
này đặc biệt quan trọng trên những hệ thống không hỗ trợ số lượng lớn các
luồng đồng hành.
Số lượng luồng trong nhóm có thể được đặt theo kinh nghiệm (heuristics) dựa trên
các yếu tố như số CPU trong hệ thống, lượng bộ nhớ vật lý và số yêu cầu khách hàng
đồng hành. Kiến trúc nhóm luồng tinh vi hơn có thể tự điều chỉnh số lượng luồng
trong nhóm dựa theo các mẫu sử dụng. Những kiến trúc như thế cung cấp lợi điểm xa
hơn của các nhóm luồng nhỏ hơn-do đó tiêu tốn ít bộ nhớ hơn-khi việc nạp trên hệ
thống là chậm.
5 Dữ liệu đặc tả luồng
Các luồng thuộc một quá trình chia sẻ dữ liệu của quá trình. Thật vậy, chia sẻ
dữ liệu này cung cấp một trong những lợi điểm của lập trình đa luồng. Tuy nhiên, mỗi
luồng có thể cần bản sao dữ liệu xác định của chính nó trong một vài trường hợp.
Chúng ta sẽ gọi dữ liệu như thế là dữ liệu đặc tả luồng. Thí dụ, trong một hệ thống xử
lý giao dịch, chúng ta có thể phục vụ mỗi giao dịch trong một luồng. Ngoài ra, mỗi
giao dịch có thể được gán một danh biểu duy nhất. Để gán mỗi luồng với định danh
duy nhất của nó chúng ta có thể dùng dữ liệu đặc tả dữ liệu. Hầu hết thư viện luồng
gồm Win32 và Pthread – cung cấp một số biểu mẫu hỗ trợ cho dữ liệu đặc tả luồng.
Java cũng cung cấp sự hỗ trợ như thế.
Trong chương trước chúng ta mô tả lời gọi hệ thống fork được dùng để tạo
một quá trình bản sao riêng như thế nào. Trong một chương trình đa luồng, ngữ nghĩa
của các lời gọi hệ thống fork và exec thay đổi. Nếu một luồng trong lời gọi chương
trình fork thì quá trình mới sao chép lại quá trình tất cả luồng hay là một quá trình đơn
luồng mới? Một số hệ thống UNIX chọn hai ấn bản fork, một sao chép lại tất cả luồng
và một sao chép lại chỉ luồng được nạp lên lời gọi hệ thống fork. Lời gọi hệ thống
exec điển hình thực hiện công việc trong cùng một cách như được mô tả trong chương
trước. Nghĩa là, nếu một luồng nạp lời gọi hệ thống exec, chương trình được xác định
trong tham số exec sẽ thay thế toàn bộ quá trình-chứa tất cả luồng và các quá trình tải
nhẹ.
Việc sử dụng hai ấn bản fork phụ thuộc vào ứng dụng. Nếu exec bị hủy tức thì
sau khi phân nhánh (forking) thì sự sao chép lại tất cả luồng là không cần thiết khi
chương trình được xác định trong các tham số exec sẽ thay thế quá trình. Trong
trường hợp này, việc sao chép lại chỉ gọi luồng hợp lý. Tuy nhiên, nếu quá trình riêng
biệt này không gọi exec sau khi phân nhánh thì quá trình riêng biệt này nên sao chép
lại tất cả luồng.
2 Sự hủy bỏ luồng
Hủy một luồng là một tác vụ kết thúc một luồng trước khi nó hoàn thành.Thí
dụ, nếu nhiều luồng đang tìm kiếm đồng thời thông qua một cơ sở dữ liệu và một
luồng trả về kết quả, các luồng còn lại có thể bị hủy. Một trường hợp khác có thể xảy
ra khi người dùng nhấn một nút trên trình duyệt web để dừng trang web đang được
tải. Thường một trang web được tải trong một luồng riêng. Khi người dùng nhấn nút
stop, luồng đang nạp trang bị hủy bỏ.
Một luồng bị hủy thường được xem như luồng đích. Sự hủy bỏ một luồng đích có
thể xảy ra hai viễn cảnh khác nhau:
• Hủy bất đồng bộ: một luồng lập tức kết thúc luồng đích
• Hủy trì hoãn: luồng đích có thể kiểm tra định kỳ nếu nó sắp kết thúc, cho
phép luồng đích một cơ hội tự kết thúc trong một cách có thứ tự.
Sự khó khăn của việc hủy này xảy ra trong những trường hợp khi tài nguyên
được cấp phát tới một luồng bị hủy hay một luồng bị hủy trong khi việc cập nhật dữ
liệu xảy ra giữa chừng, nó đang chia sẻ với các luồng khác. Điều này trở nên đặc biệt
khó khăn với sự hủy bất đồng bộ. Hệ điều hành thường đòi lại tài nguyên hệ thống từ
luồng bị hủy nhưng thường nó sẽ không đòi lại tất cả tài nguyên. Do đó, việc hủy một
luồng bất đồng bộ có thể không giải phóng hết tài nguyên hệ thống cần thiết.
Một chọn lựa khác, sự hủy trì hoãn thực hiện bằng một luồng báo hiệu rằng
một luồng đích bị hủy. Tuy nhiên, sự hủy sẽ xảy ra chỉ khi luồng đích kiểm tra để xác
định nếu nó được hủy hay không. Điều này cho phép một luồng kiểm tra nếu nó sẽ bị
hủy tại điểm nó có thể an toàn bị hủy. Pthreads gọi những điểm như thế là các điểm
hủy (cancellation points).
Hầu hết hệ điều hành cho phép một quá trình hay một luồng bị hủy bất đồng
bộ. Tuy nhiên, Pthread API cung cấp sự hủy trì hoãn. Điều này có nghĩa rằng một hệ
điều hành cài đặt Pthread API sẽ cho phép sự hủy có trì hoãn.
3 Tín hiệu quản lý
Một tín hiệu (signal) được dùng trong hệ điều hành UNIX thông báo một sự
kiện xác định xảy ra. Một tín hiệu có thể được nhận hoặc đồng bộ hoặc bất đồng bộ
phụ thuộc mã và lý do cho sự kiện đang được báo hiệu. Một tín hiệu hoặc đồng bộ
hoặc bất đồng bộ đều theo sau cùng mẫu:
• Tín hiệu được phát sinh bởi sự xảy ra của một sự kiện xác định.
• Tín hiệu được phát sinh được phân phát tới một quá trình.
• Khi được phân phát xong, tín hiệu phải được quản lý.
Một thí dụ của tín hiệu đồng bộ gồm một truy xuất bộ nhớ không hợp lệ hay
chia cho 0. Trong trường hợp này, nếu một chương trình đang chạy thực hiện một
trong các hoạt động này, một tín hiệu được phát sinh. Các tín hiệu đồng bộ được phân
phát tới cùng một quá trình thực hiện thao tác gây ra tín hiệu (do đó lý do chúng được
xem đồng bộ).
Khi một tín hiệu được phát sinh bởi một sự kiện bên ngoài tới một quá trình
đang chạy, quá trình đó nhận tín hiệu bất đồng bộ. Thí dụ, tín hiệu kết thúc quá trình
với phím xác định (như <control><C>) hay thời gian hết hạn. Điển hình, một tín hiệu
bất đồng bộ được gởi tới quá trình khác.
Mỗi tín hiệu có thể được quản lý bởi một trong hai bộ quản lý:
• Bộ quản lý tín hiệu mặc định
• Bộ quản lý tín hiệu được định nghĩa bởi người dùng
Mỗi tín hiệu có một bộ quản lý tín hiệu mặc định được thực thi bởi nhân khi
quản lý tín hiệu. Hoạt động mặc định có thể được ghi đè bởi một hàm quản lý tín hiệu
được định nghĩa bởi người dùng. Trong trường hợp này, hàm được định nghĩa bởi
người dùng được gọi để quản lý tín hiệu hơn là hoạt động mặc định. Cả hai tín hiệu
đồng bộ và bất đồng bộ có thể được quản lý trong các cách khác nhau. Một số tín hiệu
có thể được bỏ qua (như thay đổi kích thước của cửa sổ); các tín hiệu khác có thể
được quản lý bằng cách kết thúc chương trình (như truy xuất bộ nhớ không hợp lệ).
Quản lý tín hiệu trong những chương trình đơn luồng không phức tạp; các tín
hiệu luôn được phân phát tới một quá trình. Tuy nhiên, phân phát tín hiệu là phức tạp
hơn trong những chương trình đa luồng, như một quá trình có nhiều luồng. Một tín
hiệu nên được phân phát ở đâu?
Thông thường, các tuỳ chọn sau tồn tại:
• Phân phát tín hiệu tới luồng mà tín hiệu áp dụng
• Phân phát tín hiệu tới mỗi luồng trong quá trình.
• Phân phát tín hiệu tới các luồng cụ thể trong quá trình.
• Gán một luồng xác định để nhận tất cả tín hiệu cho quá trình.
Phương pháp cho việc phân phát tín hiệu phụ thuộc vào loại tín hiệu được phát
sinh. Thí dụ, các tín hiệu đồng bộ cần được phân phát tới luồng đã phát sinh ra tín
hiệu và không phân phát tới luồng nào khác trong quá trình. Tuy nhiên, trường hợp
với tín hiệu bất đồng bộ là không rõ ràng. Một số tín hiệu bất đồng bộ - như tín hiệu
kết thúc một quá trình (thí dụ:<control><c>)- nên được gởi tới tất cả luồng. Một số ấn
bản đa luồng của UNIX cho phép một luồng xác định tín hiệu nào sẽ được chấp nhận
và tín hiệu nào sẽ bị khoá. Do đó, một vài tín hiệu bất đồng bộ có thể được phân phát
tới chỉ các luồng không khoá tín hiệu. Tuy nhiên, vì tín hiệu cần được quản lý chỉ một
lần, điển hình một tín hiệu được phân phát chỉ luồng đầu tiên được tìm thấy trong một
luồng mà không nghẽn tín hiệu. Solaris 2 cài đặt bốn tuỳ chọn; nó tạo một luồng xác
định trong mỗi quá trình cho quản lý tín hiệu. Khi một tín hiệu bất đồng bộ được gởi
tới một quá trình, nó được gởi tới luồng xác định, sau đó nó phân phát tín hiệu tới
luồng đầu tiên không khoá tín hiệu.
Mặc dù Windows 2000 không cung cấp rõ sự hỗ trợ tín hiệu, nhưng chúng có
thể được mô phỏng sử dụng lời gọi thủ tục bất đồng bộ (asynchronous produce calls-
APC). Tiện ích APC cho phép luồng người dùng xác định hàm được gọi khi luồng
người dùng nhận thông báo về một sự kiện xác định. Như được hiển thị bởi tên của
nó, một APC rất giống tín hiệu bất đồng bộ trong UNIX. Tuy nhiên, UNIX phải đấu
tranh với cách giải quyết tín hiệu trong môi trường đa luồng, phương tiện APC phức
tạp hơn như một APC được phân phát tới luồng xác định hơn quá trình.
4 Nhóm luồng
Trong phần VI.3, chúng ta mô tả kịch bản đa luồng của một trình phục vụ
web. Trong trường hợp này, bất cứ khi nào trình phục vụ nhận một yêu cầu, nó tạo
một luồng riêng để phục vụ yêu cầu đó. Ngược lại, tạo một luồng riêng thật sự cao
hơn tạo một quá trình riêng, dù sao một trình phục vụ đa luồng có thể phát sinh vấn
đề. Quan tâm đầu tiên là lượng thời gian được yêu cầu để tạo luồng trước khi phục vụ
yêu cầu, và lượng thời gian xoá luồng khi nó hoàn thành. Vấn đề thứ hai là vấn đề khó
giải quyết hơn: nếu chúng ta cho phép tất cả yêu cầu đồng hành được phục vụ trong
một luồng mới, chúng ta không thay thế giới hạn trên số lượng luồng hoạt động đồng
hành trong hệ thống. Những luồng không giới hạn có thể làm cạn kiệt tài nguyên hệ
thống, như thời gian CPU và bộ nhớ. Một giải pháp cho vấn đề này là sử dụng nhóm
luồng.
Ý tưởng chung nằm sau nhóm luồng là tạo số lượng luồng tại thời điểm khởi
động và đặt chúng vào nhóm, nơi chúng ngồi và chờ công việc. Khi một trình phục vụ
nhận một yêu cầu, chúng đánh thức một luồng từ nhóm- nếu một luồng sẳn dùng –
truyền nó yêu cầu dịch vụ. Một khi luồng hoàn thành dịch vụ của nó, nó trả về nhóm
đang chờ công việc kế. Nếu nhóm không chứa luồng sẳn dùng, trình phục vụ chờ cho
tới khi nó rảnh.
Nói cụ thể, các lợi ích của nhóm luồng là:
1) Thường phục vụ yêu cầu nhanh hơn với luồng đã có hơn là chờ để tạo luồng.
2) Một nhóm luồng bị giới hạn số lượng luồng tồn tại bất kỳ thời điểm nào. Điều
này đặc biệt quan trọng trên những hệ thống không hỗ trợ số lượng lớn các
luồng đồng hành.
Số lượng luồng trong nhóm có thể được đặt theo kinh nghiệm (heuristics) dựa trên
các yếu tố như số CPU trong hệ thống, lượng bộ nhớ vật lý và số yêu cầu khách hàng
đồng hành. Kiến trúc nhóm luồng tinh vi hơn có thể tự điều chỉnh số lượng luồng
trong nhóm dựa theo các mẫu sử dụng. Những kiến trúc như thế cung cấp lợi điểm xa
hơn của các nhóm luồng nhỏ hơn-do đó tiêu tốn ít bộ nhớ hơn-khi việc nạp trên hệ
thống là chậm.
5 Dữ liệu đặc tả luồng
Các luồng thuộc một quá trình chia sẻ dữ liệu của quá trình. Thật vậy, chia sẻ
dữ liệu này cung cấp một trong những lợi điểm của lập trình đa luồng. Tuy nhiên, mỗi
luồng có thể cần bản sao dữ liệu xác định của chính nó trong một vài trường hợp.
Chúng ta sẽ gọi dữ liệu như thế là dữ liệu đặc tả luồng. Thí dụ, trong một hệ thống xử
lý giao dịch, chúng ta có thể phục vụ mỗi giao dịch trong một luồng. Ngoài ra, mỗi
giao dịch có thể được gán một danh biểu duy nhất. Để gán mỗi luồng với định danh
duy nhất của nó chúng ta có thể dùng dữ liệu đặc tả dữ liệu. Hầu hết thư viện luồng
gồm Win32 và Pthread – cung cấp một số biểu mẫu hỗ trợ cho dữ liệu đặc tả luồng.
Java cũng cung cấp sự hỗ trợ như thế.
Nguyên lý Tập luồng
Tập luồng (Thread Pools):
-Tiến trình cha tạo lập sẵn một tập luồng khi khởi động.
-Các luồng trong tập luồng luôn sẵn sàng chờ công việc.
-Khi tiến trình cha (ví dụ Web Server) nhận thêm một yêu cầu, một luồng được đánh thức và đưa vào vận hành.
-Phục vụ xong, luồng được đưa trả về tập luồng.
-Nếu số yêu cầu lớn hơn số luồng trong tập, tiến trình cha chờ đến khi có luồng được giải phóng.
-Tiến trình cha tạo lập sẵn một tập luồng khi khởi động.
-Các luồng trong tập luồng luôn sẵn sàng chờ công việc.
-Khi tiến trình cha (ví dụ Web Server) nhận thêm một yêu cầu, một luồng được đánh thức và đưa vào vận hành.
-Phục vụ xong, luồng được đưa trả về tập luồng.
-Nếu số yêu cầu lớn hơn số luồng trong tập, tiến trình cha chờ đến khi có luồng được giải phóng.
TranQuyThanh (I11C)
- Tổng số bài gửi : 53
Join date : 30/08/2011
-
Khái niệm luồng và ví dụ minh họa
Luồng: còn được gọi là 1 tiến trình nhẹ, nằm bên trong các tiến trình lớn. Có thể nói chúng là một đơn vị nhò sử dụng cpu. Chúng có thông tin về trạng thái như một tiến trình truyền thống.
VD1: Các bạn đi trên đường một chiều thì một chiều đó chình là một luồng hay 1 tiến trình, nhưng trong luồng đó người ta phân ra các luồng nhỏ hơn đó là các làn xe ô tô, xe tải, xe máy, và đi bộ. Các làn đó được gọi là một luồng hay 1 tiến trình nhẹ.
VD2: Trong công ty các bạn có : Sếp, thư kí, kế toán, nhân viên văn phòng...... Thì công ty là một luồng lớn hay 1 tiến trình để sản xuất là một sản phẩm gì đó vd như cơ khí, còn những cá thể trong công ty sẽ là một luồng hay một tiến trình nhẹ để xử lý các công việc khác nhau.......
Đó là một vài vd về luồng theo cách mình hiểu, hy vong sẽ giúp ích đôi chút cho các bạn.
VD1: Các bạn đi trên đường một chiều thì một chiều đó chình là một luồng hay 1 tiến trình, nhưng trong luồng đó người ta phân ra các luồng nhỏ hơn đó là các làn xe ô tô, xe tải, xe máy, và đi bộ. Các làn đó được gọi là một luồng hay 1 tiến trình nhẹ.
VD2: Trong công ty các bạn có : Sếp, thư kí, kế toán, nhân viên văn phòng...... Thì công ty là một luồng lớn hay 1 tiến trình để sản xuất là một sản phẩm gì đó vd như cơ khí, còn những cá thể trong công ty sẽ là một luồng hay một tiến trình nhẹ để xử lý các công việc khác nhau.......
Đó là một vài vd về luồng theo cách mình hiểu, hy vong sẽ giúp ích đôi chút cho các bạn.
ledinhngankhanh (i11c)
- Tổng số bài gửi : 15
Join date : 26/08/2011
Age : 27
Đến từ : Kiên Giang
-
Lập trình đa luồng + Ghi database
Hi các bạn !
Mình mới nghiên cứu về lập trình đa luồng nên có điểm này mình chưa hiểu, các bạn chỉ hộ nhé:
Để tránh xung đột tài nguyên khi kết nối database trong thread thì cách nào là hiệu quả. Nếu có ví dụ thì càng tốt. Mình có 1 ví dụ về lập trình socket, dùng thread chỗ này
while (true)
{
try
{
if (listener.Pending() == true)
{
if (Test(conn1, conn2)) //Kiểm tra kêt nối có bị hủy không
{
conn1 = new SqlConnection(");
conn2 = new SqlConnection("");
}
ConnectionThread newconnection = new ConnectionThread(listener,conn1,conn2);
Thread newthread = new Thread(new ThreadStart(newconnection.HandleConnection)); //Trong hàm HandleConnection mình khởi tạo kết nối mỗi khi có 1 luồng đi vào bằng cách gán conn1 và conn2
newthread.Start();
numthread++;
Console.WriteLine("Number thread : " + numthread.ToString());
}
Thread.Sleep(60);//Kết thúc 1 thread
}
catch { }
}
Mong các bạn giúp đỡ.
Mình mới nghiên cứu về lập trình đa luồng nên có điểm này mình chưa hiểu, các bạn chỉ hộ nhé:
Để tránh xung đột tài nguyên khi kết nối database trong thread thì cách nào là hiệu quả. Nếu có ví dụ thì càng tốt. Mình có 1 ví dụ về lập trình socket, dùng thread chỗ này
while (true)
{
try
{
if (listener.Pending() == true)
{
if (Test(conn1, conn2)) //Kiểm tra kêt nối có bị hủy không
{
conn1 = new SqlConnection(");
conn2 = new SqlConnection("");
}
ConnectionThread newconnection = new ConnectionThread(listener,conn1,conn2);
Thread newthread = new Thread(new ThreadStart(newconnection.HandleConnection)); //Trong hàm HandleConnection mình khởi tạo kết nối mỗi khi có 1 luồng đi vào bằng cách gán conn1 và conn2
newthread.Start();
numthread++;
Console.WriteLine("Number thread : " + numthread.ToString());
}
Thread.Sleep(60);//Kết thúc 1 thread
}
catch { }
}
Mong các bạn giúp đỡ.
tam.nguyen
- Tổng số bài gửi : 1
Join date : 29/02/2012
-
Mô hình đa luồng
Mô hình đa luồng dễ hiểu nhất là:
Khi làm đề tài hay báo cáo về một vấn đề gì đó, chúng ta thực hiện theo nhóm 2-3 người, có nghĩa là cùng một công việc nhưng nhiều người làm và điểm đến kết thúc giống nhau, đó là đa luồng.
Khi tiến hành từng đoạn trong một đề tài, thì đó không còn là đa luồng. (ai đảm nhận câu đó thì báo cáo về vấn đề đó)
Khi làm đề tài hay báo cáo về một vấn đề gì đó, chúng ta thực hiện theo nhóm 2-3 người, có nghĩa là cùng một công việc nhưng nhiều người làm và điểm đến kết thúc giống nhau, đó là đa luồng.
Khi tiến hành từng đoạn trong một đề tài, thì đó không còn là đa luồng. (ai đảm nhận câu đó thì báo cáo về vấn đề đó)
DangLeHieu(I102C)
- Tổng số bài gửi : 18
Join date : 05/03/2012
-
Sponsored content
-
Trang 6 trong tổng số 6 trang • 1, 2, 3, 4, 5, 6
Trang 6 trong tổng số 6 trang
Permissions in this forum:Bạn không có quyền trả lời bài viết

markdown:
[Thảo luận Bài 5 (Đa luồng)](/t3701-topic)
Trang
**6**trong tổng số **6**trang • [Share](#)• [Trang 6 trong tổng số 6 trang](javascript:Pagination();) • ![ Previous](https://illiweb.com/fa/empty.gif)
![ Previous](https://illiweb.com/fa/empty.gif)
[1](/t3701-topic), [2](/t3701p25-topic), [3](/t3701p50-topic), [4](/t3701p75-topic), [5](/t3701p100-topic), **6**
[Thảo luận bài 5](/t3701p125-topic#16746)
Những thuận lợi của lập trình đa luồng.1. Sự đáp ứng.
- Đa luồng là một ứng dụng giao tiếp cho phép một chương trình tiếp tục chạy thậm chí nếu một phần của nó bị khóa hay đang thực hiện một thao tác dài, do đó gia tăng sự đáp ứng đối với người dùng. Ví dụ, một trình duyết web vẫn có thể đáp ứng người dùng bằng một luồng trong khi một ảnh đang nạp bằng một luồng khác.
2. Chia sẻ tài nguyên.
- Mặc đinh, các luồng chia sẻ bộ nhớ và các tài nguyên của các quá trình mà chúng ta thuộc về. Thuận lợi của việc chia sẻ mã là nó cho phép một ứng dụng có nhiều hoạt động của các luồng khác hau nằm trong không gian địa chỉ.
3. Kinh tế.
- Cấp phát bộ nhớ và các tài nguyên cho việc tạo các quá trình là rất đắt. Vì các luồng chia sẻ tài nguyên của quá trình mà chúng ta thuộc về nên nó kinh tế hơn để tạo và chuyển ngữ cảnh giữa các luồng. Khó để đánh giá theo kinh nghiệm sự khác biệt chi phí cho việc tạo duy trì một quá trình hơn một luồng, nhưng thường nó sẽ mất nhiều thời gian để tạo và quản lý một quá trình hơn một luồng. Trong Solaris 2, tạo một quá trình chậm hơn khoảng 30 lần tạo một luồng và chuyển đổi ngữ cảnh chậm hơn 5 lần.
4. Sử dụng kiến trúc đa xử lý.
- Các lợi điểm của đa luồng có thể phát huy trong kiến trúc đa xử lý, ở đó mỗi luồng thực thi song song trên một bộ xử lý khác nhau. Một quá trình đơn luồng chỉ có thể chạy trên một CPU. Đa luồng trên một máy nhiều CPU gia tăng tính đồng hành. Trong kiến trúc đơn xử lý, CPU thường chuyển đổi qua lại giữa mỗi luồng quá nhanh để tạo ra hình ảnh của sự song song nhưng trong thực tế chỉ một luồng đang chạy tại một thời điểm.
[HuynhVanNhut (I11C)](/u1245)
- Tổng số bài gửi : 12
Join date : 07/09/2011
[Lập trình đa luồng trong C#](/t3701p125-topic#16762)
Trong series bài viết này, mình sẽ cố gắng truyền đạt hết những gì mình đã tìm hiểu được về kỹ thuật lập trình đa luồng trong C#, một cách dễ hiểu nhất. Nội dung series bài viết gồm có:
1.Cái nhìn tổng quan về lập trình đa luồng
2.Viết một chương trình xử lý đa luồng đơn giản
3.Đồng bộ hóa trong lập trình đa luồng
4.Giao tiếp trong một chương trình xử lý đa luồng
Mình dự định sẽ tự viết series này, sẽ mất thời gian và gặp nhiều khó khăn nên rất mong các bạn ủng hộ. Mọi thắc mắc và trao đổi các bạn cứ thoải mái comment để chúng ta cùng tìm hiểu.
Cái nhìn tổng quan về lập trình đa luồng
Nhắc lại một số kiến thức chung
Windows là một hệ điều hành đa nhiệm (multitasking), tức là có thể thực hiện nhiều công việc cùng lúc. Để làm được như vậy, hệ điều hành cần phải phân chia công việc và quản lý chúng sao cho khi có sự cố thì sẽ không ảnh hưởng toàn bộ hệ thống. Hãy xem Windows đã làm thế nào nhé:
Mỗi chương trình được Windows cho phép thực hiện trên một tiến trình (process), trong một tiến trình có ít nhất một Application Domain và trong Application Domain lại có thể có chứa Thread… Ta có thể tưởng tượng như sau: trong khu phố X, ta xây các ngôi nhà A, B, C,… cho các gia đình ở, để họ không phải chung đụng nhau, dễ gây lộn; trong mỗi ngôi nhà lại có các phòng 1,2,3,… để các thành viên có thể làm việc riêng mà không ảnh hưởng lẫn nhau…
=> Như vậy ta có thể nhận xét ý tưởng là khoanh vùng và định ra các ranh giới. ^^!
Khái niệm lập trình đa luồng
Bước vào trong thế giới lập trình, lập trình đa luồng là một kỹ thuật lập trình giúp ta có thể thực hiện nhiều công việc song song cùng lúc. Trong kỹ thuật này ta sẽ tạo ra các luồng (thread) chạy cùng lúc để thực hiện các nhiệm vụ khác nhau mà không gián đoạn chương trình chính.
Một số ưu điểm:
•Giúp chương trình thực hiện nhiều công việc cùng lúc, tiết kiệm thời gian thực thi.
•Giúp tăng sự linh hoạt trong khi thực thi chương trình, tăng hiệu suất.
Và một số hạn chế:
•Đòi hỏi sự cẩn thận trong việc quản lý tài nguyên hệ thống.
•Cần phải có sự đồng bộ hóa giữa các luồng để đảm bảo tính đúng đắn của chương trình.
“Công cụ” lập trình đa luồng
Namespace System.Threading và các thành phần của nó.
1.Cái nhìn tổng quan về lập trình đa luồng
2.Viết một chương trình xử lý đa luồng đơn giản
3.Đồng bộ hóa trong lập trình đa luồng
4.Giao tiếp trong một chương trình xử lý đa luồng
Mình dự định sẽ tự viết series này, sẽ mất thời gian và gặp nhiều khó khăn nên rất mong các bạn ủng hộ. Mọi thắc mắc và trao đổi các bạn cứ thoải mái comment để chúng ta cùng tìm hiểu.
Cái nhìn tổng quan về lập trình đa luồng
Nhắc lại một số kiến thức chung
Windows là một hệ điều hành đa nhiệm (multitasking), tức là có thể thực hiện nhiều công việc cùng lúc. Để làm được như vậy, hệ điều hành cần phải phân chia công việc và quản lý chúng sao cho khi có sự cố thì sẽ không ảnh hưởng toàn bộ hệ thống. Hãy xem Windows đã làm thế nào nhé:
Mỗi chương trình được Windows cho phép thực hiện trên một tiến trình (process), trong một tiến trình có ít nhất một Application Domain và trong Application Domain lại có thể có chứa Thread… Ta có thể tưởng tượng như sau: trong khu phố X, ta xây các ngôi nhà A, B, C,… cho các gia đình ở, để họ không phải chung đụng nhau, dễ gây lộn; trong mỗi ngôi nhà lại có các phòng 1,2,3,… để các thành viên có thể làm việc riêng mà không ảnh hưởng lẫn nhau…
=> Như vậy ta có thể nhận xét ý tưởng là khoanh vùng và định ra các ranh giới. ^^!
Khái niệm lập trình đa luồng
Bước vào trong thế giới lập trình, lập trình đa luồng là một kỹ thuật lập trình giúp ta có thể thực hiện nhiều công việc song song cùng lúc. Trong kỹ thuật này ta sẽ tạo ra các luồng (thread) chạy cùng lúc để thực hiện các nhiệm vụ khác nhau mà không gián đoạn chương trình chính.
Một số ưu điểm:
•Giúp chương trình thực hiện nhiều công việc cùng lúc, tiết kiệm thời gian thực thi.
•Giúp tăng sự linh hoạt trong khi thực thi chương trình, tăng hiệu suất.
Và một số hạn chế:
•Đòi hỏi sự cẩn thận trong việc quản lý tài nguyên hệ thống.
•Cần phải có sự đồng bộ hóa giữa các luồng để đảm bảo tính đúng đắn của chương trình.
“Công cụ” lập trình đa luồng
Namespace System.Threading và các thành phần của nó.
[Những lợi ích của lập trình đa luồng](/t3701p125-topic#16763)
Những lợi ích của lập trình đa luồng
Được chia làm 4 loại như sau:
1.Sự đáp ứng: đa luồng một ứng dụng giao tiếp cho phép một chương trình tiếp tục chạy thậm chí nếu một phần của nó bị khóa hay đang thực hiện một thao tác dài, do đó gia tăng sự đáp ứng đối với người dùng. Thí dụ, một trình duyệt web vẫn có thể đáp ứng người dùng bằng một luồng trong khi một ảnh đang được nạp bằng một luồng khác.
2.Chia sẻ tài nguyên: Mặc định, các luồng chia sẻ bộ nhớ và các tài nguyên của các quá trình mà chúng thuộc về. Thuận lợi của việc chia sẽ mã là nó cho phép một ứng dụng có nhiều hoạt động của các luồng khác nhau nằm trong cùng không gian địa chỉ.
3.Kinh tế: cấp phát bộ nhớ và các tài nguyên cho việc tạo các quá trình là rất
đắt. Vì các luồng chia sẻ tài nguyên của quá trình mà chúng thuộc về nên nó kinh tế hơn để tạo và chuyển ngữ cảnh giữa các luồng. Khó để đánh giá theo kinh nghiệm sự khác biệt chi phí cho việc tạo và duy trì một quá trình hơn một luồng, nhưng thường nó sẽ mất nhiều thời gian để tạo và quản lý một quá trình hơn một luồng. Trong Solaris 2, tạo một quá trình chậm hơn khoảng 30 lần tạo một luồng và chuyển đổi ngữ cảnh chậm hơn 5 lần.
4.Sử dụng kiến trúc đa xử lý: các lợi điểm của đa luồng có thể phát huy trong
kiến trúc đa xử lý, ở đó mỗi luồng thực thi song song trên một bộ xử lý khác nhau. Một quá trình đơn luồng chỉ có thể chạy trên một CPU. Đa luồng trên một máy nhiều CPU gia tăng tính đồng hành. Trong kiến trúc đơn xử lý, CPU thường chuyển đổi qua lại giữa mỗi luồng quá nhanh để tạo ra hình ảnh của sự song song nhưng trong thực tế chỉ một luồng đang chạy tại một thời điểm.
Được chia làm 4 loại như sau:
1.Sự đáp ứng: đa luồng một ứng dụng giao tiếp cho phép một chương trình tiếp tục chạy thậm chí nếu một phần của nó bị khóa hay đang thực hiện một thao tác dài, do đó gia tăng sự đáp ứng đối với người dùng. Thí dụ, một trình duyệt web vẫn có thể đáp ứng người dùng bằng một luồng trong khi một ảnh đang được nạp bằng một luồng khác.
2.Chia sẻ tài nguyên: Mặc định, các luồng chia sẻ bộ nhớ và các tài nguyên của các quá trình mà chúng thuộc về. Thuận lợi của việc chia sẽ mã là nó cho phép một ứng dụng có nhiều hoạt động của các luồng khác nhau nằm trong cùng không gian địa chỉ.
3.Kinh tế: cấp phát bộ nhớ và các tài nguyên cho việc tạo các quá trình là rất
đắt. Vì các luồng chia sẻ tài nguyên của quá trình mà chúng thuộc về nên nó kinh tế hơn để tạo và chuyển ngữ cảnh giữa các luồng. Khó để đánh giá theo kinh nghiệm sự khác biệt chi phí cho việc tạo và duy trì một quá trình hơn một luồng, nhưng thường nó sẽ mất nhiều thời gian để tạo và quản lý một quá trình hơn một luồng. Trong Solaris 2, tạo một quá trình chậm hơn khoảng 30 lần tạo một luồng và chuyển đổi ngữ cảnh chậm hơn 5 lần.
4.Sử dụng kiến trúc đa xử lý: các lợi điểm của đa luồng có thể phát huy trong
kiến trúc đa xử lý, ở đó mỗi luồng thực thi song song trên một bộ xử lý khác nhau. Một quá trình đơn luồng chỉ có thể chạy trên một CPU. Đa luồng trên một máy nhiều CPU gia tăng tính đồng hành. Trong kiến trúc đơn xử lý, CPU thường chuyển đổi qua lại giữa mỗi luồng quá nhanh để tạo ra hình ảnh của sự song song nhưng trong thực tế chỉ một luồng đang chạy tại một thời điểm.
[ so sánh sự khác nhau giữa lập trình đa luồng và đa tiến trình](/t3701p125-topic#16764)
so sánh sự khác nhau giữa lập trình đa luồng và đa tiến trình
Giống nhau :
1. Đều có thông tin trạng thái
2. Luồng còn được gọi là tiến trình nhẹ
3. Nhiều luồng or nhiều tiến trình có thể liên quan đến một chương trình
4. Chia sẻ tài nguyên cho nhau
5. Tăng tốc tính toán
6. Đảm bảo tính đơn thể
Khác nhau
1. Do luồng cùng vận hành trong 1 vùng địa chỉ vì vậy dể dùng chung tài nguyên hơn đa tiến trình
VD : Mượn đồ dùng trong gia đình thì dể hơn mượn của hàng xóm
2. Do luồng chung tài nguyên với cha & các luồng khác nên việc tạo lập & chuyển ngữ cảnh cũng
nhanh hơn tiến trình
VD : Việc ngăn riêng ra một phòng trong nhà thì nhanh hơn là chờ nhà nước cấp cho
một căn hộ
3. Cấp phát bộ nhớ & tài nguyên cho tiến trình thì tốn kém hơn luồng
4. Lập trình đa luồng thì dể hơn đa tiến trình
Giống nhau :
1. Đều có thông tin trạng thái
2. Luồng còn được gọi là tiến trình nhẹ
3. Nhiều luồng or nhiều tiến trình có thể liên quan đến một chương trình
4. Chia sẻ tài nguyên cho nhau
5. Tăng tốc tính toán
6. Đảm bảo tính đơn thể
Khác nhau
1. Do luồng cùng vận hành trong 1 vùng địa chỉ vì vậy dể dùng chung tài nguyên hơn đa tiến trình
VD : Mượn đồ dùng trong gia đình thì dể hơn mượn của hàng xóm
2. Do luồng chung tài nguyên với cha & các luồng khác nên việc tạo lập & chuyển ngữ cảnh cũng
nhanh hơn tiến trình
VD : Việc ngăn riêng ra một phòng trong nhà thì nhanh hơn là chờ nhà nước cấp cho
một căn hộ
3. Cấp phát bộ nhớ & tài nguyên cho tiến trình thì tốn kém hơn luồng
4. Lập trình đa luồng thì dể hơn đa tiến trình
[Đa luồng trong java (multithreading in Java) ](/t3701p125-topic#16765)
Multithread trong java
1.1 Khái niệm
Thread là một đơn vị (nhóm) chứa các dòng điều khiển trong một tiến trình hay ứng dụng, chúng được hệ điều hành sử dụng để thực thi một công việc. Và với cơ chế multithread ứng dụng của ta có thể thực thi nhiều công việc đồng thời. Mỗi thread sẽ có một chỉ số xác định quyền ưu tiên, thread nào có quyền ưu tiên cao sẽ được thực thi trước, và thấp thì thực thi sau. Việc thực thi và đánh chỉ số ưu tiên sẽ được thực hiện tự động bởi hệ điều hành để tránh một thread nào đợi quá lâu mà không được vào xử lý.
Cũng như các ngôn ngữ lập trình khác, java hỗ trợ ngay (trong bản thân ngôn ngữ) khá đầy đủ các yêu cầu, xử lý của một ứng dụng multithread.
1.2 Tạo và quản lý thread
Trong chương trình java luôn có một thread chính được thực thi, các thread khác sẽ được tạo ra như các thread con, việc mở đầu và kết thúc một ứng dụng được thực hiện bởi thread chính đó.
Hình dưới minh họa vòng đời của một thread:
Để tạo và quản lý thread trong java, chúng ta dựa vào lớp Thread và Runnable interface
Runnable là một interface dùng để các lớp khác implement để sử dụng thread. Trong lớp implement đó bắt buộc phải định nghĩa một phương thức là void run().
Ví dụ ta có 2 lớp StringThread implement Runnable, và một lớp TestClass để tạo ra thread và run nó.
public class StringThread implements Runnable {
private String str;
private int num;
StringThread(String s, int n)
{
str = new String(s);
num = n;
}
/* (non-Javadoc)
* @see java.lang.Runnable#run()
*/
@Override
public void run() {
for (int i = 1; i <= num; i++)
System.out.print(str + " ");
}
}
public class TestClass {
/**
* @param args
*/
public static void main(String[] args) {
StringThread t = new StringThread("Thread", 10);
new Thread(t).start();
}
}
Kết quả in ra màn hình sẽ là: Thread Thread Thread Thread Thread Thread Thread Thread Thread Thread
Thread là một lớp viết sẵn trong java, nó implement Runnable interface. Nếu chúng ta muốn sử dụng thread chỉ cần viết một lớp và kế thừa (extends) từ lớp Thread này.
Một số hàm thông dụng của Thread
• start bắt đầu thực thi thread
• sleep đưa trạng thái về trạng thái đợi (không thực thi nữa)
• resume thực thi lại thread từ trạng thái đợi hoặc dead-lock
• join đợi để cho đến khi thread này chết
• isAlive kiểm tra thread còn đang sống hay đã bị kill
• getName lấy tên của thread
• getPriority trả về độ ưu tiên của thread
• setName đặt tên cho thread
• setPriority đặt độ ưu tiên cho thread
• …
Ví dụ tương tự việc sử dụng thread với Runnable interface, chúng ta cũng có thể kế thừa tự lớp có sẵn là Thread như sau:
class StringThread extends Thread {
public void run() {
System.out.println("Inherit successfully from Thread class!");
}
}
public class TestClass {
public static void main(String[] args) {
StringThread t = new StringThread();
new Thread(t).start();
}
}
Kết quả in ra sẽ là: Inherit successfully from Thread class!
1.3 Các lớp Timer, TimerTask và SchedulingTask
Timer là một lớp tiện ích giúp cho việc lập lịch và kiểm soát việc thực thi một task vụ nào đó. Ví dụ bạn muốn dùng việc cập nhật thông trong 5 phút tới, hay thông tin chỉ được tồn tại 10h sau đó sẽ tự động bị xóa đi,… Một số hàm thông dụng trong Timer như:
• schedule lên lịch để thực thi task khi nào bắt đầu, kết thúc hay lặp lại, …
• cancel dừng timer và hủy tất cả các task đã lên lịch trong timer
• purge xóa tất cả các task đã dùng trong hàng đợi timer
TimerTask là một lớp trừu tượng implement Runnable interface, nó giúp cho việc lập lịch thực thi các thread
Ví dụ ta có một lớp MyTimerTask kế thừa từ TimerTask như bên dưới
class MyTimerTask extends TimerTask {
public void run() {
System.out.println("Running the scheduled task by Timer");
System.gc();
}
}
public class TestClass {
public static void main(String[] args) {
Timer timer = new Timer();
MyTimerTask task = new MyTimerTask();
timer.schedule(task, 3000, 2000);//Execute task after 3 senconds and then repeat in 2 seconds period
}
}
Kết quả in ra màn hình sau 5 giây sẽ là: Running the scheduled task by Timer
Running the scheduled task by Timer
1.4 Xử lý các vấn đề chúng trong multithread như synchronized, lock, dead lock, wait, notify …
Đồng bộ (Synchronization) là cơ chế giúp cho đồng bộ việc truy cập cùng tài nguyên của nhiều thread khác nhau. Như chúng ta biết vì cơ chế thread là cơ chế làm việc đồng thời do đó có khả năng một tài nguyên sẽ bị truy cập bởi nhiều thread khác nhau.
Ví dụ ta xây dựng 3 lớp Socola, Producer, Consumer. Lớp Socola chứa thông tin, lớp Producer tạo thông tin và gán vào cho Socola, Lớp Consumer sẽ ăn (tiêu thụ) thông tin đó.
public class TestClass {
public class Socola {
private int contents = 0;
public boolean isRemainingContents;
public synchronized int get() {
//print onto screen
System.out.println("\tConsumer get " + this.contents);
return contents;
}
public synchronized void set(int piContents) {
this.contents = piContents;
//print onto screen
System.out.println("Producer set " + this.contents);
}
}
public class Producer extends Thread {
private Socola socola;
Producer(Socola pSocola) {
socola = pSocola;
}
public void run() {
for(int i = 0; i < 10; i++) {
socola.set(i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
}
}
}
public class Consumer extends Thread {
private Socola socola;
Consumer(Socola pSocola) {
socola = pSocola;
}
public void run() {
for(int i = 0; i < 10; i++) {
socola.get();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
}
}
}
public static void main(String[] args) {
TestClass t = new TestClass();
Socola s = t.new Socola();
Producer p = t.new Producer(s);
Consumer c = t.new Consumer(s);
p.start();
c.start();
}
}
Kết quả in ra màn hinh sẽ không giống nhau tại các lần chạy, ví dụ 1 lần chạy:
Producer set 0
Consumer get 0
Producer set 1
Consumer get 1
Producer set 2
Consumer get 2
Producer set 3
Consumer get 3
Producer set 4
Consumer get 4
Consumer get 4
Producer set 5
Producer set 6
Consumer get 6
Producer set 7
Consumer get 7
Consumer get 7
Producer set 8
Consumer get 8
Producer set 9
Từ ví dụ trên ta thấy có chung một tài nguyên là Socola mà có 2 đối tượng cùng truy cập có thể sinh ra mâu thuẫn khi truy cập đồng thời. Bây giờ ta muốn là chỉ khi Producer tạo ra thông tin thì Consumer mới ăn thông tin đó, không muốn Consumer ăn nhiều lần cùng một thông tin, khi Producer đang tạo ra thông tin thì Consumer đứng chờ, khi nào Producer tạo ra thông tin xong thì Consumer mới vào ăn thông tin. vậy thì ta phải dùng cơ chế đồng bộ trong java với các từ khóa synchronized, wait(), notify(), notifyAll() và chỉnh lại 2 hàm get và set trong lớp Socola như sau:
public synchronized int get() {
while(isRemainingContents == false) {
try {
wait();
} catch (InterruptedException e) {
}
}
isRemainingContents = false;
//print onto screen
System.out.println("\tConsumer get " + this.contents);
//Notify Producer to produce new contents
notifyAll();
return contents;
}
public synchronized void set(int piContents) {
while(isRemainingContents == true) {
try {
wait();
} catch (InterruptedException e) {
}
}
this.contents = piContents;
isRemainingContents = true;
//print onto screen
System.out.println("Producer set " + this.contents);
//Notify Consumer to eat this contents
notifyAll();
}
}
Kết quả in ra luôn luôn như sau:
Producer set 0
Consumer get 0
Producer set 1
Consumer get 1
Producer set 2
Consumer get 2
Producer set 3
Consumer get 3
Producer set 4
Consumer get 4
Producer set 5
Consumer get 5
Producer set 6
Consumer get 6
Producer set 7
Consumer get 7
Producer set 8
Consumer get 8
Producer set 9
Consumer get 9
Có 3 hàm cần để ý:
• wait() đưa thread vào trạng thái chờ khi nào được notify() hoặc notifyAll() thì sẽ thực hiện tiếp
• notify() đánh thức thread đang đợi, nếu có nhiều thread đang đợi, thì một trong những thread đó được đánh thức thôi.
• notifyAll() đánh thức tất cả các thread đang đợi
Note: Nhiều khi chúng ta không cần hoặc không thể đồng bộ nguyên hàm (dùng synchronized) mà chỉ cần đồng bộ một vài dòng code truy cập tài nguyên, ta có thể dùng khối đồng bộ như sau:
synchronized(object) {
// Các cấu lệnh cần đồng bộ
}
Ví dụ: Khi bạn sử dụng lớp Socola là của khách hàng bạn đưa cho, bạn không thể chỉnh sửa định nghĩa hàm get và set trong Socola được vậy thì bạn có thể dùng khối đồng bộ bên trên cũng sẽ cho kết quả tương tự.
Deadlock (khóa chết) trong xử lý đa luồng xảy là khi mà một thread được đưa vào trạng thái chờ tài nguyên và không bao giờ được đánh thức lại vì tại nguyên đó không bao giờ được giải phóng. Để rõ hơn ta ví dụ A, B, C là thread trong trạng thái chờ; rA, rB, rC là tài nguyên dùng chung và chỉ duy nhất một thread sử dụng trong một thời điểm, tài nguyên chỉ được giải phóng khi thread làm xong và không sử dụng nữa.
• A giữ rA, chờ rB
• B giữ rB, chờ rC
• C giữ rC, chờ rA
Từ ví dụ trên ta thấy A, B, C là những thread đang ở trong trạng thái chờ tài nguyên, tuy nhiên tài nguyên của chúng không bao giờ được giải phóng, và mãi mãi chúng cứ chờ thôi. Để hạn chế deadlock thì chúng ta phải đưa ra, cải tiến việc cấp phát, thu hồi tài nguyên cho các thread và hiện này có khá nhiều thuật toán cải tiến việc này, tuy nhiên không có cái nào giúp tránh hoàn toàn deadlock.
1.5 Những lớp tiện ích trong lập trình đa luồng (multithread)
Trong java đã xây dựng một vài lớp giúp hỗ trợ lập trình đa luồng, chúng được đặt trong gói Concurent như: Semaphore, Lock, Condition, ConcurrentMap, ConcurrentLinkedQueue, …
Semaphore là một lớp tiện ích đã xây dựng trong java (java 5) dùng để giới hạn số lượng thread truy cập đến tài nguyên. Đầu tiên thread cần yêu cầu sự chấp nhận truy cập tài nguyên từ semaphore bằng hàm acquire, và thread sẽ về trạng thái đợi (block) cho đến khi được chấp nhận, khi thread được chấp nhận nó thực hiện công việc của nó xong sẽ gọi hàm release để trả về cho semaphore một sự chấp nhận còn trống để semaphore cấp cho thread khác.
Lock là một interface được xây dựng nhằm mở rộng hơn việc quản lý truy cập tài nguyên trong nhiều thread hơn so với cách dùng synchronized chẳng hạn như có thể hỗ trợ nhiều điều kiện (condition object) trong lock, hoặc hỗ trợ nhiều thuật toán cho chain-lock để tránh deadlock tốt hơn, … Khi dùng Lock chúng ta nên theo một template chuẩn như sau:
Lock l = ...;
l.lock();
try {
// Các task truy cập tài nguyên bảo vệ bởi lock đặt ở đây
} finally {
l.unlock();
}
Condition là một interface dùng để nhóm cách xử lý trong các phương thức (wait, notify, notifyAll) của lớp Object vào thành một đối tượng nhằm đưa ra nhiều chọn lựa hơn trong việc đồng bộ, ví dụ chúng ta có thể dùng nhiều condition và khi nào chúng ta muốn đánh thức chính xác điều kiện nào được sử dụng tài nguyên.
Ví dụ ta dùng lại ví dụ Consumer-Producer ở trên, thêm vào 2 biến lock, condition và chỉnh sửa hai làm get và put thế là ta cũng được kết quả như dùng với từ khóa synchronized:
final Lock lock = new ReentrantLock();
final Condition condition = lock.newCondition();
public int get() {
int result;
lock.lock();
try {
while(isRemainingContents == false) {
try {
condition.await();
} catch (InterruptedException e) {
}
}
isRemainingContents = false;
//print onto screen
System.out.println("\tConsumer get " + this.contents);
//get value
result = contents;
//Notify Producer to produce new contents
condition.signalAll();
} finally {
lock.unlock();
}
return result;
}
public void set(int piContents) {
lock.lock();
try {
while(isRemainingContents == true) {
try {
condition.await();
} catch (InterruptedException e) {
}
}
this.contents = piContents;
isRemainingContents = true;
//print onto screen
System.out.println("Producer set " + this.contents);
//Notify Consumer to eat this contents
condition.signalAll();
} finally {
lock.unlock();
}
}
Kết quả luôn luôn là thế này:
Producer set 0
Consumer get 0
Producer set 1
Consumer get 1
Producer set 2
Consumer get 2
Producer set 3
Consumer get 3
Producer set 4
Consumer get 4
Producer set 5
Consumer get 5
Producer set 6
Consumer get 6
Producer set 7
Consumer get 7
Producer set 8
Consumer get 8
Producer set 9
Consumer get 9
ConcurrentMap là một interface kế thừa từ Map dùng để cải tiến, tối ưu hơn khi chạy Map trong ứng dụng đa luồng (multithread)
Ví dụ cách dùng ConcurrentHashMap tương tự dùng Map, tuy nhiên người ta đã thống kê là nếu chương trình có càng nhiều thread truy cập vào cMap này thì dùng ConcurrentMap sẽ cho performance tốt hơn rất nhiều so với dùng Map với đồng bộ kiểu synchronized
public static ConcurrentMap<String,String> cMap =
new ConcurrentHashMap<String, String>(1000);
public static void main(String[] args) {
cMap.putIfAbsent("id", "1");
cMap.putIfAbsent("name", "kien");
cMap.putIfAbsent("email", "nakien2a@yahoo.com");
System.out.println(cMap); //Print to screen
}
Kết quả: {name=kien, email=nakien2a@yahoo.com, id=1}
1.1 Khái niệm
Thread là một đơn vị (nhóm) chứa các dòng điều khiển trong một tiến trình hay ứng dụng, chúng được hệ điều hành sử dụng để thực thi một công việc. Và với cơ chế multithread ứng dụng của ta có thể thực thi nhiều công việc đồng thời. Mỗi thread sẽ có một chỉ số xác định quyền ưu tiên, thread nào có quyền ưu tiên cao sẽ được thực thi trước, và thấp thì thực thi sau. Việc thực thi và đánh chỉ số ưu tiên sẽ được thực hiện tự động bởi hệ điều hành để tránh một thread nào đợi quá lâu mà không được vào xử lý.
Cũng như các ngôn ngữ lập trình khác, java hỗ trợ ngay (trong bản thân ngôn ngữ) khá đầy đủ các yêu cầu, xử lý của một ứng dụng multithread.
1.2 Tạo và quản lý thread
Trong chương trình java luôn có một thread chính được thực thi, các thread khác sẽ được tạo ra như các thread con, việc mở đầu và kết thúc một ứng dụng được thực hiện bởi thread chính đó.
Hình dưới minh họa vòng đời của một thread:
Để tạo và quản lý thread trong java, chúng ta dựa vào lớp Thread và Runnable interface
Runnable là một interface dùng để các lớp khác implement để sử dụng thread. Trong lớp implement đó bắt buộc phải định nghĩa một phương thức là void run().
Ví dụ ta có 2 lớp StringThread implement Runnable, và một lớp TestClass để tạo ra thread và run nó.
public class StringThread implements Runnable {
private String str;
private int num;
StringThread(String s, int n)
{
str = new String(s);
num = n;
}
/* (non-Javadoc)
* @see java.lang.Runnable#run()
*/
@Override
public void run() {
for (int i = 1; i <= num; i++)
System.out.print(str + " ");
}
}
public class TestClass {
/**
* @param args
*/
public static void main(String[] args) {
StringThread t = new StringThread("Thread", 10);
new Thread(t).start();
}
}
Kết quả in ra màn hình sẽ là: Thread Thread Thread Thread Thread Thread Thread Thread Thread Thread
Thread là một lớp viết sẵn trong java, nó implement Runnable interface. Nếu chúng ta muốn sử dụng thread chỉ cần viết một lớp và kế thừa (extends) từ lớp Thread này.
Một số hàm thông dụng của Thread
• start bắt đầu thực thi thread
• sleep đưa trạng thái về trạng thái đợi (không thực thi nữa)
• resume thực thi lại thread từ trạng thái đợi hoặc dead-lock
• join đợi để cho đến khi thread này chết
• isAlive kiểm tra thread còn đang sống hay đã bị kill
• getName lấy tên của thread
• getPriority trả về độ ưu tiên của thread
• setName đặt tên cho thread
• setPriority đặt độ ưu tiên cho thread
• …
Ví dụ tương tự việc sử dụng thread với Runnable interface, chúng ta cũng có thể kế thừa tự lớp có sẵn là Thread như sau:
class StringThread extends Thread {
public void run() {
System.out.println("Inherit successfully from Thread class!");
}
}
public class TestClass {
public static void main(String[] args) {
StringThread t = new StringThread();
new Thread(t).start();
}
}
Kết quả in ra sẽ là: Inherit successfully from Thread class!
1.3 Các lớp Timer, TimerTask và SchedulingTask
Timer là một lớp tiện ích giúp cho việc lập lịch và kiểm soát việc thực thi một task vụ nào đó. Ví dụ bạn muốn dùng việc cập nhật thông trong 5 phút tới, hay thông tin chỉ được tồn tại 10h sau đó sẽ tự động bị xóa đi,… Một số hàm thông dụng trong Timer như:
• schedule lên lịch để thực thi task khi nào bắt đầu, kết thúc hay lặp lại, …
• cancel dừng timer và hủy tất cả các task đã lên lịch trong timer
• purge xóa tất cả các task đã dùng trong hàng đợi timer
TimerTask là một lớp trừu tượng implement Runnable interface, nó giúp cho việc lập lịch thực thi các thread
Ví dụ ta có một lớp MyTimerTask kế thừa từ TimerTask như bên dưới
class MyTimerTask extends TimerTask {
public void run() {
System.out.println("Running the scheduled task by Timer");
System.gc();
}
}
public class TestClass {
public static void main(String[] args) {
Timer timer = new Timer();
MyTimerTask task = new MyTimerTask();
timer.schedule(task, 3000, 2000);//Execute task after 3 senconds and then repeat in 2 seconds period
}
}
Kết quả in ra màn hình sau 5 giây sẽ là: Running the scheduled task by Timer
Running the scheduled task by Timer
1.4 Xử lý các vấn đề chúng trong multithread như synchronized, lock, dead lock, wait, notify …
Đồng bộ (Synchronization) là cơ chế giúp cho đồng bộ việc truy cập cùng tài nguyên của nhiều thread khác nhau. Như chúng ta biết vì cơ chế thread là cơ chế làm việc đồng thời do đó có khả năng một tài nguyên sẽ bị truy cập bởi nhiều thread khác nhau.
Ví dụ ta xây dựng 3 lớp Socola, Producer, Consumer. Lớp Socola chứa thông tin, lớp Producer tạo thông tin và gán vào cho Socola, Lớp Consumer sẽ ăn (tiêu thụ) thông tin đó.
public class TestClass {
public class Socola {
private int contents = 0;
public boolean isRemainingContents;
public synchronized int get() {
//print onto screen
System.out.println("\tConsumer get " + this.contents);
return contents;
}
public synchronized void set(int piContents) {
this.contents = piContents;
//print onto screen
System.out.println("Producer set " + this.contents);
}
}
public class Producer extends Thread {
private Socola socola;
Producer(Socola pSocola) {
socola = pSocola;
}
public void run() {
for(int i = 0; i < 10; i++) {
socola.set(i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
}
}
}
public class Consumer extends Thread {
private Socola socola;
Consumer(Socola pSocola) {
socola = pSocola;
}
public void run() {
for(int i = 0; i < 10; i++) {
socola.get();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
}
}
}
public static void main(String[] args) {
TestClass t = new TestClass();
Socola s = t.new Socola();
Producer p = t.new Producer(s);
Consumer c = t.new Consumer(s);
p.start();
c.start();
}
}
Kết quả in ra màn hinh sẽ không giống nhau tại các lần chạy, ví dụ 1 lần chạy:
Producer set 0
Consumer get 0
Producer set 1
Consumer get 1
Producer set 2
Consumer get 2
Producer set 3
Consumer get 3
Producer set 4
Consumer get 4
Consumer get 4
Producer set 5
Producer set 6
Consumer get 6
Producer set 7
Consumer get 7
Consumer get 7
Producer set 8
Consumer get 8
Producer set 9
Từ ví dụ trên ta thấy có chung một tài nguyên là Socola mà có 2 đối tượng cùng truy cập có thể sinh ra mâu thuẫn khi truy cập đồng thời. Bây giờ ta muốn là chỉ khi Producer tạo ra thông tin thì Consumer mới ăn thông tin đó, không muốn Consumer ăn nhiều lần cùng một thông tin, khi Producer đang tạo ra thông tin thì Consumer đứng chờ, khi nào Producer tạo ra thông tin xong thì Consumer mới vào ăn thông tin. vậy thì ta phải dùng cơ chế đồng bộ trong java với các từ khóa synchronized, wait(), notify(), notifyAll() và chỉnh lại 2 hàm get và set trong lớp Socola như sau:
public synchronized int get() {
while(isRemainingContents == false) {
try {
wait();
} catch (InterruptedException e) {
}
}
isRemainingContents = false;
//print onto screen
System.out.println("\tConsumer get " + this.contents);
//Notify Producer to produce new contents
notifyAll();
return contents;
}
public synchronized void set(int piContents) {
while(isRemainingContents == true) {
try {
wait();
} catch (InterruptedException e) {
}
}
this.contents = piContents;
isRemainingContents = true;
//print onto screen
System.out.println("Producer set " + this.contents);
//Notify Consumer to eat this contents
notifyAll();
}
}
Kết quả in ra luôn luôn như sau:
Producer set 0
Consumer get 0
Producer set 1
Consumer get 1
Producer set 2
Consumer get 2
Producer set 3
Consumer get 3
Producer set 4
Consumer get 4
Producer set 5
Consumer get 5
Producer set 6
Consumer get 6
Producer set 7
Consumer get 7
Producer set 8
Consumer get 8
Producer set 9
Consumer get 9
Có 3 hàm cần để ý:
• wait() đưa thread vào trạng thái chờ khi nào được notify() hoặc notifyAll() thì sẽ thực hiện tiếp
• notify() đánh thức thread đang đợi, nếu có nhiều thread đang đợi, thì một trong những thread đó được đánh thức thôi.
• notifyAll() đánh thức tất cả các thread đang đợi
Note: Nhiều khi chúng ta không cần hoặc không thể đồng bộ nguyên hàm (dùng synchronized) mà chỉ cần đồng bộ một vài dòng code truy cập tài nguyên, ta có thể dùng khối đồng bộ như sau:
synchronized(object) {
// Các cấu lệnh cần đồng bộ
}
Ví dụ: Khi bạn sử dụng lớp Socola là của khách hàng bạn đưa cho, bạn không thể chỉnh sửa định nghĩa hàm get và set trong Socola được vậy thì bạn có thể dùng khối đồng bộ bên trên cũng sẽ cho kết quả tương tự.
Deadlock (khóa chết) trong xử lý đa luồng xảy là khi mà một thread được đưa vào trạng thái chờ tài nguyên và không bao giờ được đánh thức lại vì tại nguyên đó không bao giờ được giải phóng. Để rõ hơn ta ví dụ A, B, C là thread trong trạng thái chờ; rA, rB, rC là tài nguyên dùng chung và chỉ duy nhất một thread sử dụng trong một thời điểm, tài nguyên chỉ được giải phóng khi thread làm xong và không sử dụng nữa.
• A giữ rA, chờ rB
• B giữ rB, chờ rC
• C giữ rC, chờ rA
Từ ví dụ trên ta thấy A, B, C là những thread đang ở trong trạng thái chờ tài nguyên, tuy nhiên tài nguyên của chúng không bao giờ được giải phóng, và mãi mãi chúng cứ chờ thôi. Để hạn chế deadlock thì chúng ta phải đưa ra, cải tiến việc cấp phát, thu hồi tài nguyên cho các thread và hiện này có khá nhiều thuật toán cải tiến việc này, tuy nhiên không có cái nào giúp tránh hoàn toàn deadlock.
1.5 Những lớp tiện ích trong lập trình đa luồng (multithread)
Trong java đã xây dựng một vài lớp giúp hỗ trợ lập trình đa luồng, chúng được đặt trong gói Concurent như: Semaphore, Lock, Condition, ConcurrentMap, ConcurrentLinkedQueue, …
Semaphore là một lớp tiện ích đã xây dựng trong java (java 5) dùng để giới hạn số lượng thread truy cập đến tài nguyên. Đầu tiên thread cần yêu cầu sự chấp nhận truy cập tài nguyên từ semaphore bằng hàm acquire, và thread sẽ về trạng thái đợi (block) cho đến khi được chấp nhận, khi thread được chấp nhận nó thực hiện công việc của nó xong sẽ gọi hàm release để trả về cho semaphore một sự chấp nhận còn trống để semaphore cấp cho thread khác.
Lock là một interface được xây dựng nhằm mở rộng hơn việc quản lý truy cập tài nguyên trong nhiều thread hơn so với cách dùng synchronized chẳng hạn như có thể hỗ trợ nhiều điều kiện (condition object) trong lock, hoặc hỗ trợ nhiều thuật toán cho chain-lock để tránh deadlock tốt hơn, … Khi dùng Lock chúng ta nên theo một template chuẩn như sau:
Lock l = ...;
l.lock();
try {
// Các task truy cập tài nguyên bảo vệ bởi lock đặt ở đây
} finally {
l.unlock();
}
Condition là một interface dùng để nhóm cách xử lý trong các phương thức (wait, notify, notifyAll) của lớp Object vào thành một đối tượng nhằm đưa ra nhiều chọn lựa hơn trong việc đồng bộ, ví dụ chúng ta có thể dùng nhiều condition và khi nào chúng ta muốn đánh thức chính xác điều kiện nào được sử dụng tài nguyên.
Ví dụ ta dùng lại ví dụ Consumer-Producer ở trên, thêm vào 2 biến lock, condition và chỉnh sửa hai làm get và put thế là ta cũng được kết quả như dùng với từ khóa synchronized:
final Lock lock = new ReentrantLock();
final Condition condition = lock.newCondition();
public int get() {
int result;
lock.lock();
try {
while(isRemainingContents == false) {
try {
condition.await();
} catch (InterruptedException e) {
}
}
isRemainingContents = false;
//print onto screen
System.out.println("\tConsumer get " + this.contents);
//get value
result = contents;
//Notify Producer to produce new contents
condition.signalAll();
} finally {
lock.unlock();
}
return result;
}
public void set(int piContents) {
lock.lock();
try {
while(isRemainingContents == true) {
try {
condition.await();
} catch (InterruptedException e) {
}
}
this.contents = piContents;
isRemainingContents = true;
//print onto screen
System.out.println("Producer set " + this.contents);
//Notify Consumer to eat this contents
condition.signalAll();
} finally {
lock.unlock();
}
}
Kết quả luôn luôn là thế này:
Producer set 0
Consumer get 0
Producer set 1
Consumer get 1
Producer set 2
Consumer get 2
Producer set 3
Consumer get 3
Producer set 4
Consumer get 4
Producer set 5
Consumer get 5
Producer set 6
Consumer get 6
Producer set 7
Consumer get 7
Producer set 8
Consumer get 8
Producer set 9
Consumer get 9
ConcurrentMap là một interface kế thừa từ Map dùng để cải tiến, tối ưu hơn khi chạy Map trong ứng dụng đa luồng (multithread)
Ví dụ cách dùng ConcurrentHashMap tương tự dùng Map, tuy nhiên người ta đã thống kê là nếu chương trình có càng nhiều thread truy cập vào cMap này thì dùng ConcurrentMap sẽ cho performance tốt hơn rất nhiều so với dùng Map với đồng bộ kiểu synchronized
public static ConcurrentMap<String,String> cMap =
new ConcurrentHashMap<String, String>(1000);
public static void main(String[] args) {
cMap.putIfAbsent("id", "1");
cMap.putIfAbsent("name", "kien");
cMap.putIfAbsent("email", "nakien2a@yahoo.com");
System.out.println(cMap); //Print to screen
}
Kết quả: {name=kien, email=nakien2a@yahoo.com, id=1}
[ Thuận lợi của lập trình đa luồng](/t3701p125-topic#16766)
Thuận lợi của lập trình đa luồng
Thuận lợi
Những thuận lợi của lập trình đa luồng có thể được chia làm bốn loại:
• Sự đáp ứng: đa luồng một ứng dụng giao tiếp cho phép một chương trình tiếp tục chạy thậm chí nếu một phần của nó bị khóa hay đang thực hiện một thao tác dài, do đó gia tăng sự đáp ứng đối với người dùng. Thí dụ, một trình duyệt web vẫn có thể đáp ứng người dùng bằng một luồng trong khi một ảnh đang được nạp bằng một luồng khác.
• Chia sẻ tài nguyên: Mặc định, các luồng chia sẻ bộ nhớ và các tài nguyên của các quá trình mà chúng thuộc về. Thuận lợi của việc chia sẽ mã là nó cho phép một ứng dụng có nhiều hoạt động của các luồng khác nhau nằm trong cùng không gian địa chỉ.
• Kinh tế: cấp phát bộ nhớ và các tài nguyên cho việc tạo các quá trình là rất đắt. Vì các luồng chia sẻ tài nguyên của quá trình mà chúng thuộc về nên nó kinh tế hơn để tạo và chuyển ngữ cảnh giữa các luồng. Khó để đánh giá theo kinh nghiệm sự khác biệt chi phí cho việc tạo và duy trì một quá trình hơn một luồng, nhưng thường nó sẽ mất nhiều thời gian để tạo và quản lý một quá trình hơn một luồng. Trong Solaris 2, tạo một quá trình chậm hơn khoảng 30 lần tạo một luồng và chuyển đổi ngữ cảnh chậm hơn 5 lần.
• Sử dụng kiến trúc đa xử lý: các lợi điểm của đa luồng có thể phát huy trong kiến trúc đa xử lý, ở đó mỗi luồng thực thi song song trên một bộ xử lý khác nhau. Một quá trình đơn luồng chỉ có thể chạy trên một CPU. Đa luồng trên một máy nhiều CPU gia tăng tính đồng hành. Trong kiến trúc đơn xử lý, CPU thường chuyển đổi qua lại giữa mỗi luồng quá nhanh để tạo ra hình ảnh của sự song song nhưng trong thực tế chỉ một luồng đang chạy tại một thời điểm.
Thuận lợi
Những thuận lợi của lập trình đa luồng có thể được chia làm bốn loại:
• Sự đáp ứng: đa luồng một ứng dụng giao tiếp cho phép một chương trình tiếp tục chạy thậm chí nếu một phần của nó bị khóa hay đang thực hiện một thao tác dài, do đó gia tăng sự đáp ứng đối với người dùng. Thí dụ, một trình duyệt web vẫn có thể đáp ứng người dùng bằng một luồng trong khi một ảnh đang được nạp bằng một luồng khác.
• Chia sẻ tài nguyên: Mặc định, các luồng chia sẻ bộ nhớ và các tài nguyên của các quá trình mà chúng thuộc về. Thuận lợi của việc chia sẽ mã là nó cho phép một ứng dụng có nhiều hoạt động của các luồng khác nhau nằm trong cùng không gian địa chỉ.
• Kinh tế: cấp phát bộ nhớ và các tài nguyên cho việc tạo các quá trình là rất đắt. Vì các luồng chia sẻ tài nguyên của quá trình mà chúng thuộc về nên nó kinh tế hơn để tạo và chuyển ngữ cảnh giữa các luồng. Khó để đánh giá theo kinh nghiệm sự khác biệt chi phí cho việc tạo và duy trì một quá trình hơn một luồng, nhưng thường nó sẽ mất nhiều thời gian để tạo và quản lý một quá trình hơn một luồng. Trong Solaris 2, tạo một quá trình chậm hơn khoảng 30 lần tạo một luồng và chuyển đổi ngữ cảnh chậm hơn 5 lần.
• Sử dụng kiến trúc đa xử lý: các lợi điểm của đa luồng có thể phát huy trong kiến trúc đa xử lý, ở đó mỗi luồng thực thi song song trên một bộ xử lý khác nhau. Một quá trình đơn luồng chỉ có thể chạy trên một CPU. Đa luồng trên một máy nhiều CPU gia tăng tính đồng hành. Trong kiến trúc đơn xử lý, CPU thường chuyển đổi qua lại giữa mỗi luồng quá nhanh để tạo ra hình ảnh của sự song song nhưng trong thực tế chỉ một luồng đang chạy tại một thời điểm.
[Tạo luồng từ lớp Thread trong Java](/t3701p125-topic#16767)
Ngoài việc tạo luồng (thread) bằng cách thi công giao diện Runnable như đã trình bày trong bài trước, bạn còn có thể tạo luồng để hiển thị hình ảnh trong chương trình Slide Show bằng cách kế thừa từ lớp Thread có sẵn trong Java. Bạn hãy chép đoạn mã 1, biên dịch và chạy thử để xem kết quả trên màn hình ra sao (H1). Qua đoạn mã 1, bạn cũng cần chú ý kỹ thuật đồng bộ đa luồng (multi-thread synchronization) trong Java.
Đoạn mã 1
// ThreadSync.java
class MyThread extends Thread {
static String message[]={"I","love","Java."};
public MyThread(String id) {
// Gọi hàm dựng (constructor) của lớp Thread cha
super(id);
}
public void run() {
Sync.displayList(getName(), message);
}
void waiting() {
try {
sleep(2000);
}
catch(InterruptedException e){
System.out.println("Interrupted");
}
}
};
class Sync {
public static synchronized void displayList(String name, String list[]) {
/* Nếu không dùng từ khóa synchronized thì sẽ không
đảm bào một luồng làm xong hết công việc của nó
rồi một luồng khác mới được bắt đầu chạy.
*/
for (int i=0; i< list.length; ++i) {
MyThread thread = (MyThread)Thread.currentThread();
// Hàm waiting đã định nghĩa ở trên
thread.waiting();
System.out.println(name + list[i]);
}
}
};
public class ThreadSync {
public static void main(String args[]) {
MyThread thread1 = new MyThread("Thread 1: ");
MyThread thread2 = new MyThread("Thread 2: ");
thread1.start(); // Hàm run của thread1 được gọi
thread2.start(); // Hàm run của thread2 được gọi
}
};
Đoạn mã 1
// ThreadSync.java
class MyThread extends Thread {
static String message[]={"I","love","Java."};
public MyThread(String id) {
// Gọi hàm dựng (constructor) của lớp Thread cha
super(id);
}
public void run() {
Sync.displayList(getName(), message);
}
void waiting() {
try {
sleep(2000);
}
catch(InterruptedException e){
System.out.println("Interrupted");
}
}
};
class Sync {
public static synchronized void displayList(String name, String list[]) {
/* Nếu không dùng từ khóa synchronized thì sẽ không
đảm bào một luồng làm xong hết công việc của nó
rồi một luồng khác mới được bắt đầu chạy.
*/
for (int i=0; i< list.length; ++i) {
MyThread thread = (MyThread)Thread.currentThread();
// Hàm waiting đã định nghĩa ở trên
thread.waiting();
System.out.println(name + list[i]);
}
}
};
public class ThreadSync {
public static void main(String args[]) {
MyThread thread1 = new MyThread("Thread 1: ");
MyThread thread2 = new MyThread("Thread 2: ");
thread1.start(); // Hàm run của thread1 được gọi
thread2.start(); // Hàm run của thread2 được gọi
}
};
[Đa luồng và Đa tiến trinh](/t3701p125-topic#16768)
Đa luồng và Đa tiến trinh
Đa luồng dùng chung các tiến trình trong hệ thống, quản trị công việc bên trong cấp phát giữa các tiến trình. (vd: tài nguyên dùng chung là bảng lớp học)
Tiến trình truyền thống dùng chung các tiến trình bên ngoài. Lập trình đa luồng dễ hơn lập trình đa tiến trình (như tạo mới luồng, chia sẻ, điều khiển dễ hơn..)
Giống nhau:
Đều có thông tin trạng thái
Luồng còn được gọi là tiến trình nhẹ
Nhiều luồng hoặc nhiều tiến trình có thể liên quan đến một chu trình
Chia sẻ tài nguyên cho nhau, tăng tốc tính toán, đảm bảo tính đơn thể
Khác nhau:
Do luồng cùng vận hành trong 1 vùng địa chỉ, vì vậy dễ dùng chung tài nguyên hơn đa tiến trình ví dụ mượn đồ trong gia đình dễ mượn đồ hơn của hàng xóm
Do luồng chung tài nguyên với luồng cha nên việc tạo lập và chuyển đổi ngữ cảnh cũng nhanh hơn tiến trình
Ví dụ: việc ngăn riêng ra một phòng trong nhà thì dễ hơn được nhà nước cấp cho một căn hộ
Cấp phát tài nguyên cho bộ nhớ thì tốn kém
Đa luồng dùng chung các tiến trình trong hệ thống, quản trị công việc bên trong cấp phát giữa các tiến trình. (vd: tài nguyên dùng chung là bảng lớp học)
Tiến trình truyền thống dùng chung các tiến trình bên ngoài. Lập trình đa luồng dễ hơn lập trình đa tiến trình (như tạo mới luồng, chia sẻ, điều khiển dễ hơn..)
Giống nhau:
Đều có thông tin trạng thái
Luồng còn được gọi là tiến trình nhẹ
Nhiều luồng hoặc nhiều tiến trình có thể liên quan đến một chu trình
Chia sẻ tài nguyên cho nhau, tăng tốc tính toán, đảm bảo tính đơn thể
Khác nhau:
Do luồng cùng vận hành trong 1 vùng địa chỉ, vì vậy dễ dùng chung tài nguyên hơn đa tiến trình ví dụ mượn đồ trong gia đình dễ mượn đồ hơn của hàng xóm
Do luồng chung tài nguyên với luồng cha nên việc tạo lập và chuyển đổi ngữ cảnh cũng nhanh hơn tiến trình
Ví dụ: việc ngăn riêng ra một phòng trong nhà thì dễ hơn được nhà nước cấp cho một căn hộ
Cấp phát tài nguyên cho bộ nhớ thì tốn kém
[Lập Trình Đa Luồng Trong JAVA](/t3701p125-topic#16769)
Thread có một định nghĩa về thread như sau :
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
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
[ Đa Luồng là gì ?](/t3701p125-topic#16770)
Đa Luồng là gì ?
Luồng - Thread: Một dòng các lệnh mà CPU phải thực thi.
Các hệ điều hành mới cho phép nhiều luồng được thực thi đồng thời. Chúng ta đã quen với việc mở nhiều ứng dụng trong một lần làm việc với máy tính.
- Một luồng là một chuỗi các lệnh nằm trong bộ nhớ (chương trình đã được nạp).
- 1 application thông thường khi thực thi là một luồng.
- Trong 1 application có thể có nhiều luồng. Thí dụ chuyển động của 10 đối tượng hiện hành trong 1 trò chơi là một luồng.
Lợi ích của đa luồng:
- Tăng hiệu suất sử dụng CPU: Phần lớn thời gian thực thi của 1 ứng dụng là chờ đợi nhập liệu từ user -> hiệu suất sử dụng CPU chưa hiệu quả.
- Tạo được sự đồng bộ giữa các đối tượng: thí dụ như trong 1 trò chơi, các nhân vật cùng nhau chuyển động.
- Quản lý được thời gian trong các ứng dụng nhu thi online, thời gian chơi một trò chơi.
Luồng - Thread: Một dòng các lệnh mà CPU phải thực thi.
Các hệ điều hành mới cho phép nhiều luồng được thực thi đồng thời. Chúng ta đã quen với việc mở nhiều ứng dụng trong một lần làm việc với máy tính.
- Một luồng là một chuỗi các lệnh nằm trong bộ nhớ (chương trình đã được nạp).
- 1 application thông thường khi thực thi là một luồng.
- Trong 1 application có thể có nhiều luồng. Thí dụ chuyển động của 10 đối tượng hiện hành trong 1 trò chơi là một luồng.
Lợi ích của đa luồng:
- Tăng hiệu suất sử dụng CPU: Phần lớn thời gian thực thi của 1 ứng dụng là chờ đợi nhập liệu từ user -> hiệu suất sử dụng CPU chưa hiệu quả.
- Tạo được sự đồng bộ giữa các đối tượng: thí dụ như trong 1 trò chơi, các nhân vật cùng nhau chuyển động.
- Quản lý được thời gian trong các ứng dụng nhu thi online, thời gian chơi một trò chơi.
[Luồng người dùng và luồng nhân &Mô hình đa luồng ](/t3701p125-topic#16771)
Chúng ta vừa mới thảo luận là xem xét luồng như một chiều hướng chung. Tuy
nhiên, hỗ trợ luồng được cung cấp hoặc ở cấp người dùng, cho các luồng người dùng
hoặc ở cấp nhân, cho các luồng nhân.
• Luồng người dùng: được hỗ trợ dưới nhân và được cài đặt bởi thư viện luồng
tại cấp người dùng. Thư viện cung cấp hỗ trợ cho việc tạo luồng, lập thời biểu,
và quản lý mà không có sự hỗ trợ từ nhân. Vì nhân không biết các luồng cấp
người dùng, tất cả việc tạo luồng và lập thời biểu được thực hiện trong không
gian người dùng mà không cần sự can thiệp của nhân. Do đó, các luồng cấp
người dùng thường tạo và quản lý nhanh, tuy nhiên chúng cũng có những trở
ngại. Thí dụ, nếu nhân là đơn luồng thì bất cứ luồng cấp người dùng thực hiện
một lời gọi hệ thống nghẽn sẽ làm cho toàn bộ quá trình bị nghẽn, thậm chí
nếu các luồng khác sẳn dùng để chạy trong ứng dụng. Các thư viện luồng
người dùng gồm các luồng POSIX Pthreads, Mach C-threads và Solaris 2 UI-
threads.
• Luồng nhân: được hỗ trợ trực tiếp bởi hệ điều hành. Nhân thực hiện việc tạo
luồng, lập thời biểu, và quản lý không gian nhân. Vì quản lý luồng được thực
hiện bởi hệ điều hành, luồng nhân thường tạo và quản lý chậm hơn luồng
người dùng. Tuy nhiên, vì nhân được quản lý các luồng nếu một luồng thực
hiện lời gọi hệ thống nghẽn, nhân có thể lập thời biểu một luồng khác trong
ứng dụng thực thi. Trong môi trường đa xử lý, nhân có thể lập thời biểu luồng
trên một bộ xử lý khác. Hầu hết các hệ điều hành hiện nay như Windows NT,
Windows 2000, Solaris 2, BeOS và Tru64 UNIX (trước Digital UNIX)-hỗ trợ
các luồng nhân
Nhiều hệ thống cung cấp sự hỗ trợ cả hai luồng nhân và luồng người dùng nên
tạo ra nhiều mô hình đa luồng khác nhau. Chúng ta sẽ xem xét ba loại cài đặt luồng
thông thường :
1. Mô hình nhiều-một
Mô hình nhiều-một (như hình IV.2) ánh xạ nhiều luồng cấp người dùng tới
một luồng cấp nhân. Quản lý luồng được thực hiện trong không gian người dùng vì
thế nó hiệu quả nhưng toàn bộ quá trình sẽ bị khóa nếu một luồng thực hiện lời gọi hệ
thống khóa. Vì chỉ một luồng có thể truy xuất nhân tại một thời điểm nên nhiều luồng
không thể chạy song song trên nhiều bộ xử lý. Green threads-một thư viện luồng được
cài đặt trên các hệ điều hành không hỗ trợ luồng nhân dùng mô hình nhiều-một.
2. Mô hình một-một
Mô hình một-một (hình IV.3) ánh xạ mỗi luồng người dùng tới một luồng
nhân. Nó cung cấp khả năng đồng hành tốt hơn mô hình nhiều-một bằng cách cho một
luồng khác chạy khi một luồng thực hiện lời gọi hệ thống nghẽn; nó cũng cho phép
nhiều luồng chạy song song trên các bộ xử lý khác nhau. Chỉ có một trở ngại trong
mô hình này là tạo luồng người dùng yêu cầu tạo một luồng nhân tương ứng. Vì chi
phí cho việc tạo luồng nhân có thể đè nặng lên năng lực thực hiện của ứng dụng, các
cài đặt cho mô hình này giới hạn số luồng được hỗ trợ bởi hệ thống. Windows NT,
Windows 2000 và OS/2 cài đặt mô hình một-một này.
3 Mô hình nhiều-nhiều
Mô hình nhiều-nhiều (như hình VI.4) đa hợp nhiều luồng cấp người dùng tới
số lượng nhỏ hơn hay bằng các luồng nhân. Số lượng các luồng nhân có thể được xác
định hoặc một ứng dụng cụ thể hay một máy cụ thể (một ứng dụng có thể được cấp
nhiều luồng nhân trên một bộ đa xử lý hơn trên một bộ đơn xử lý). Trong khi mô hình
nhiều-một cho phép người phát triển tạo nhiều luồng người dùng như họ muốn, thì
đồng hành thật sự là không đạt được vì nhân có thể lập thời biểu chỉ một luồng tại một
thời điểm. Mô hình một-một cho phép đồng hành tốt hơn nhưng người phát triển phải
cẩn thận không tạo ra quá nhiều luồng trong một ứng dụng. Mô hình nhiều-nhiều gặp
phải một trong hai vấn đề khiếm khuyết: người phát triển có thể tạo nhiều luồng
người dùng khi cần thiết và các luồng nhân tương ứng có thể chạy song song trên một
bộ đa xử lý. Khi một luồng thực hiện một lời gọi hệ thống khóa, nhân có thể lập thời
biểu một luồng khác thực thi. Solaris 2, IRIX, HP-UX, và Tru64 UNIX hỗ trợ mô
hình này.
nhiên, hỗ trợ luồng được cung cấp hoặc ở cấp người dùng, cho các luồng người dùng
hoặc ở cấp nhân, cho các luồng nhân.
• Luồng người dùng: được hỗ trợ dưới nhân và được cài đặt bởi thư viện luồng
tại cấp người dùng. Thư viện cung cấp hỗ trợ cho việc tạo luồng, lập thời biểu,
và quản lý mà không có sự hỗ trợ từ nhân. Vì nhân không biết các luồng cấp
người dùng, tất cả việc tạo luồng và lập thời biểu được thực hiện trong không
gian người dùng mà không cần sự can thiệp của nhân. Do đó, các luồng cấp
người dùng thường tạo và quản lý nhanh, tuy nhiên chúng cũng có những trở
ngại. Thí dụ, nếu nhân là đơn luồng thì bất cứ luồng cấp người dùng thực hiện
một lời gọi hệ thống nghẽn sẽ làm cho toàn bộ quá trình bị nghẽn, thậm chí
nếu các luồng khác sẳn dùng để chạy trong ứng dụng. Các thư viện luồng
người dùng gồm các luồng POSIX Pthreads, Mach C-threads và Solaris 2 UI-
threads.
• Luồng nhân: được hỗ trợ trực tiếp bởi hệ điều hành. Nhân thực hiện việc tạo
luồng, lập thời biểu, và quản lý không gian nhân. Vì quản lý luồng được thực
hiện bởi hệ điều hành, luồng nhân thường tạo và quản lý chậm hơn luồng
người dùng. Tuy nhiên, vì nhân được quản lý các luồng nếu một luồng thực
hiện lời gọi hệ thống nghẽn, nhân có thể lập thời biểu một luồng khác trong
ứng dụng thực thi. Trong môi trường đa xử lý, nhân có thể lập thời biểu luồng
trên một bộ xử lý khác. Hầu hết các hệ điều hành hiện nay như Windows NT,
Windows 2000, Solaris 2, BeOS và Tru64 UNIX (trước Digital UNIX)-hỗ trợ
các luồng nhân
Nhiều hệ thống cung cấp sự hỗ trợ cả hai luồng nhân và luồng người dùng nên
tạo ra nhiều mô hình đa luồng khác nhau. Chúng ta sẽ xem xét ba loại cài đặt luồng
thông thường :
1. Mô hình nhiều-một
Mô hình nhiều-một (như hình IV.2) ánh xạ nhiều luồng cấp người dùng tới
một luồng cấp nhân. Quản lý luồng được thực hiện trong không gian người dùng vì
thế nó hiệu quả nhưng toàn bộ quá trình sẽ bị khóa nếu một luồng thực hiện lời gọi hệ
thống khóa. Vì chỉ một luồng có thể truy xuất nhân tại một thời điểm nên nhiều luồng
không thể chạy song song trên nhiều bộ xử lý. Green threads-một thư viện luồng được
cài đặt trên các hệ điều hành không hỗ trợ luồng nhân dùng mô hình nhiều-một.
2. Mô hình một-một
Mô hình một-một (hình IV.3) ánh xạ mỗi luồng người dùng tới một luồng
nhân. Nó cung cấp khả năng đồng hành tốt hơn mô hình nhiều-một bằng cách cho một
luồng khác chạy khi một luồng thực hiện lời gọi hệ thống nghẽn; nó cũng cho phép
nhiều luồng chạy song song trên các bộ xử lý khác nhau. Chỉ có một trở ngại trong
mô hình này là tạo luồng người dùng yêu cầu tạo một luồng nhân tương ứng. Vì chi
phí cho việc tạo luồng nhân có thể đè nặng lên năng lực thực hiện của ứng dụng, các
cài đặt cho mô hình này giới hạn số luồng được hỗ trợ bởi hệ thống. Windows NT,
Windows 2000 và OS/2 cài đặt mô hình một-một này.
3 Mô hình nhiều-nhiều
Mô hình nhiều-nhiều (như hình VI.4) đa hợp nhiều luồng cấp người dùng tới
số lượng nhỏ hơn hay bằng các luồng nhân. Số lượng các luồng nhân có thể được xác
định hoặc một ứng dụng cụ thể hay một máy cụ thể (một ứng dụng có thể được cấp
nhiều luồng nhân trên một bộ đa xử lý hơn trên một bộ đơn xử lý). Trong khi mô hình
nhiều-một cho phép người phát triển tạo nhiều luồng người dùng như họ muốn, thì
đồng hành thật sự là không đạt được vì nhân có thể lập thời biểu chỉ một luồng tại một
thời điểm. Mô hình một-một cho phép đồng hành tốt hơn nhưng người phát triển phải
cẩn thận không tạo ra quá nhiều luồng trong một ứng dụng. Mô hình nhiều-nhiều gặp
phải một trong hai vấn đề khiếm khuyết: người phát triển có thể tạo nhiều luồng
người dùng khi cần thiết và các luồng nhân tương ứng có thể chạy song song trên một
bộ đa xử lý. Khi một luồng thực hiện một lời gọi hệ thống khóa, nhân có thể lập thời
biểu một luồng khác thực thi. Solaris 2, IRIX, HP-UX, và Tru64 UNIX hỗ trợ mô
hình này.
[ Cấp phát luồng ](/t3701p125-topic#16772)
1 Lời gọi hệ thống fork và exec
Trong chương trước chúng ta mô tả lời gọi hệ thống fork được dùng để tạo
một quá trình bản sao riêng như thế nào. Trong một chương trình đa luồng, ngữ nghĩa
của các lời gọi hệ thống fork và exec thay đổi. Nếu một luồng trong lời gọi chương
trình fork thì quá trình mới sao chép lại quá trình tất cả luồng hay là một quá trình đơn
luồng mới? Một số hệ thống UNIX chọn hai ấn bản fork, một sao chép lại tất cả luồng
và một sao chép lại chỉ luồng được nạp lên lời gọi hệ thống fork. Lời gọi hệ thống
exec điển hình thực hiện công việc trong cùng một cách như được mô tả trong chương
trước. Nghĩa là, nếu một luồng nạp lời gọi hệ thống exec, chương trình được xác định
trong tham số exec sẽ thay thế toàn bộ quá trình-chứa tất cả luồng và các quá trình tải
nhẹ.
Việc sử dụng hai ấn bản fork phụ thuộc vào ứng dụng. Nếu exec bị hủy tức thì
sau khi phân nhánh (forking) thì sự sao chép lại tất cả luồng là không cần thiết khi
chương trình được xác định trong các tham số exec sẽ thay thế quá trình. Trong
trường hợp này, việc sao chép lại chỉ gọi luồng hợp lý. Tuy nhiên, nếu quá trình riêng
biệt này không gọi exec sau khi phân nhánh thì quá trình riêng biệt này nên sao chép
lại tất cả luồng.
2 Sự hủy bỏ luồng
Hủy một luồng là một tác vụ kết thúc một luồng trước khi nó hoàn thành.Thí
dụ, nếu nhiều luồng đang tìm kiếm đồng thời thông qua một cơ sở dữ liệu và một
luồng trả về kết quả, các luồng còn lại có thể bị hủy. Một trường hợp khác có thể xảy
ra khi người dùng nhấn một nút trên trình duyệt web để dừng trang web đang được
tải. Thường một trang web được tải trong một luồng riêng. Khi người dùng nhấn nút
stop, luồng đang nạp trang bị hủy bỏ.
Một luồng bị hủy thường được xem như luồng đích. Sự hủy bỏ một luồng đích có
thể xảy ra hai viễn cảnh khác nhau:
• Hủy bất đồng bộ: một luồng lập tức kết thúc luồng đích
• Hủy trì hoãn: luồng đích có thể kiểm tra định kỳ nếu nó sắp kết thúc, cho
phép luồng đích một cơ hội tự kết thúc trong một cách có thứ tự.
Sự khó khăn của việc hủy này xảy ra trong những trường hợp khi tài nguyên
được cấp phát tới một luồng bị hủy hay một luồng bị hủy trong khi việc cập nhật dữ
liệu xảy ra giữa chừng, nó đang chia sẻ với các luồng khác. Điều này trở nên đặc biệt
khó khăn với sự hủy bất đồng bộ. Hệ điều hành thường đòi lại tài nguyên hệ thống từ
luồng bị hủy nhưng thường nó sẽ không đòi lại tất cả tài nguyên. Do đó, việc hủy một
luồng bất đồng bộ có thể không giải phóng hết tài nguyên hệ thống cần thiết.
Một chọn lựa khác, sự hủy trì hoãn thực hiện bằng một luồng báo hiệu rằng
một luồng đích bị hủy. Tuy nhiên, sự hủy sẽ xảy ra chỉ khi luồng đích kiểm tra để xác
định nếu nó được hủy hay không. Điều này cho phép một luồng kiểm tra nếu nó sẽ bị
hủy tại điểm nó có thể an toàn bị hủy. Pthreads gọi những điểm như thế là các điểm
hủy (cancellation points).
Hầu hết hệ điều hành cho phép một quá trình hay một luồng bị hủy bất đồng
bộ. Tuy nhiên, Pthread API cung cấp sự hủy trì hoãn. Điều này có nghĩa rằng một hệ
điều hành cài đặt Pthread API sẽ cho phép sự hủy có trì hoãn.
3 Tín hiệu quản lý
Một tín hiệu (signal) được dùng trong hệ điều hành UNIX thông báo một sự
kiện xác định xảy ra. Một tín hiệu có thể được nhận hoặc đồng bộ hoặc bất đồng bộ
phụ thuộc mã và lý do cho sự kiện đang được báo hiệu. Một tín hiệu hoặc đồng bộ
hoặc bất đồng bộ đều theo sau cùng mẫu:
• Tín hiệu được phát sinh bởi sự xảy ra của một sự kiện xác định.
• Tín hiệu được phát sinh được phân phát tới một quá trình.
• Khi được phân phát xong, tín hiệu phải được quản lý.
Một thí dụ của tín hiệu đồng bộ gồm một truy xuất bộ nhớ không hợp lệ hay
chia cho 0. Trong trường hợp này, nếu một chương trình đang chạy thực hiện một
trong các hoạt động này, một tín hiệu được phát sinh. Các tín hiệu đồng bộ được phân
phát tới cùng một quá trình thực hiện thao tác gây ra tín hiệu (do đó lý do chúng được
xem đồng bộ).
Khi một tín hiệu được phát sinh bởi một sự kiện bên ngoài tới một quá trình
đang chạy, quá trình đó nhận tín hiệu bất đồng bộ. Thí dụ, tín hiệu kết thúc quá trình
với phím xác định (như <control><C>) hay thời gian hết hạn. Điển hình, một tín hiệu
bất đồng bộ được gởi tới quá trình khác.
Mỗi tín hiệu có thể được quản lý bởi một trong hai bộ quản lý:
• Bộ quản lý tín hiệu mặc định
• Bộ quản lý tín hiệu được định nghĩa bởi người dùng
Mỗi tín hiệu có một bộ quản lý tín hiệu mặc định được thực thi bởi nhân khi
quản lý tín hiệu. Hoạt động mặc định có thể được ghi đè bởi một hàm quản lý tín hiệu
được định nghĩa bởi người dùng. Trong trường hợp này, hàm được định nghĩa bởi
người dùng được gọi để quản lý tín hiệu hơn là hoạt động mặc định. Cả hai tín hiệu
đồng bộ và bất đồng bộ có thể được quản lý trong các cách khác nhau. Một số tín hiệu
có thể được bỏ qua (như thay đổi kích thước của cửa sổ); các tín hiệu khác có thể
được quản lý bằng cách kết thúc chương trình (như truy xuất bộ nhớ không hợp lệ).
Quản lý tín hiệu trong những chương trình đơn luồng không phức tạp; các tín
hiệu luôn được phân phát tới một quá trình. Tuy nhiên, phân phát tín hiệu là phức tạp
hơn trong những chương trình đa luồng, như một quá trình có nhiều luồng. Một tín
hiệu nên được phân phát ở đâu?
Thông thường, các tuỳ chọn sau tồn tại:
• Phân phát tín hiệu tới luồng mà tín hiệu áp dụng
• Phân phát tín hiệu tới mỗi luồng trong quá trình.
• Phân phát tín hiệu tới các luồng cụ thể trong quá trình.
• Gán một luồng xác định để nhận tất cả tín hiệu cho quá trình.
Phương pháp cho việc phân phát tín hiệu phụ thuộc vào loại tín hiệu được phát
sinh. Thí dụ, các tín hiệu đồng bộ cần được phân phát tới luồng đã phát sinh ra tín
hiệu và không phân phát tới luồng nào khác trong quá trình. Tuy nhiên, trường hợp
với tín hiệu bất đồng bộ là không rõ ràng. Một số tín hiệu bất đồng bộ - như tín hiệu
kết thúc một quá trình (thí dụ:<control><c>)- nên được gởi tới tất cả luồng. Một số ấn
bản đa luồng của UNIX cho phép một luồng xác định tín hiệu nào sẽ được chấp nhận
và tín hiệu nào sẽ bị khoá. Do đó, một vài tín hiệu bất đồng bộ có thể được phân phát
tới chỉ các luồng không khoá tín hiệu. Tuy nhiên, vì tín hiệu cần được quản lý chỉ một
lần, điển hình một tín hiệu được phân phát chỉ luồng đầu tiên được tìm thấy trong một
luồng mà không nghẽn tín hiệu. Solaris 2 cài đặt bốn tuỳ chọn; nó tạo một luồng xác
định trong mỗi quá trình cho quản lý tín hiệu. Khi một tín hiệu bất đồng bộ được gởi
tới một quá trình, nó được gởi tới luồng xác định, sau đó nó phân phát tín hiệu tới
luồng đầu tiên không khoá tín hiệu.
Mặc dù Windows 2000 không cung cấp rõ sự hỗ trợ tín hiệu, nhưng chúng có
thể được mô phỏng sử dụng lời gọi thủ tục bất đồng bộ (asynchronous produce calls-
APC). Tiện ích APC cho phép luồng người dùng xác định hàm được gọi khi luồng
người dùng nhận thông báo về một sự kiện xác định. Như được hiển thị bởi tên của
nó, một APC rất giống tín hiệu bất đồng bộ trong UNIX. Tuy nhiên, UNIX phải đấu
tranh với cách giải quyết tín hiệu trong môi trường đa luồng, phương tiện APC phức
tạp hơn như một APC được phân phát tới luồng xác định hơn quá trình.
4 Nhóm luồng
Trong phần VI.3, chúng ta mô tả kịch bản đa luồng của một trình phục vụ
web. Trong trường hợp này, bất cứ khi nào trình phục vụ nhận một yêu cầu, nó tạo
một luồng riêng để phục vụ yêu cầu đó. Ngược lại, tạo một luồng riêng thật sự cao
hơn tạo một quá trình riêng, dù sao một trình phục vụ đa luồng có thể phát sinh vấn
đề. Quan tâm đầu tiên là lượng thời gian được yêu cầu để tạo luồng trước khi phục vụ
yêu cầu, và lượng thời gian xoá luồng khi nó hoàn thành. Vấn đề thứ hai là vấn đề khó
giải quyết hơn: nếu chúng ta cho phép tất cả yêu cầu đồng hành được phục vụ trong
một luồng mới, chúng ta không thay thế giới hạn trên số lượng luồng hoạt động đồng
hành trong hệ thống. Những luồng không giới hạn có thể làm cạn kiệt tài nguyên hệ
thống, như thời gian CPU và bộ nhớ. Một giải pháp cho vấn đề này là sử dụng nhóm
luồng.
Ý tưởng chung nằm sau nhóm luồng là tạo số lượng luồng tại thời điểm khởi
động và đặt chúng vào nhóm, nơi chúng ngồi và chờ công việc. Khi một trình phục vụ
nhận một yêu cầu, chúng đánh thức một luồng từ nhóm- nếu một luồng sẳn dùng –
truyền nó yêu cầu dịch vụ. Một khi luồng hoàn thành dịch vụ của nó, nó trả về nhóm
đang chờ công việc kế. Nếu nhóm không chứa luồng sẳn dùng, trình phục vụ chờ cho
tới khi nó rảnh.
Nói cụ thể, các lợi ích của nhóm luồng là:
1) Thường phục vụ yêu cầu nhanh hơn với luồng đã có hơn là chờ để tạo luồng.
2) Một nhóm luồng bị giới hạn số lượng luồng tồn tại bất kỳ thời điểm nào. Điều
này đặc biệt quan trọng trên những hệ thống không hỗ trợ số lượng lớn các
luồng đồng hành.
Số lượng luồng trong nhóm có thể được đặt theo kinh nghiệm (heuristics) dựa trên
các yếu tố như số CPU trong hệ thống, lượng bộ nhớ vật lý và số yêu cầu khách hàng
đồng hành. Kiến trúc nhóm luồng tinh vi hơn có thể tự điều chỉnh số lượng luồng
trong nhóm dựa theo các mẫu sử dụng. Những kiến trúc như thế cung cấp lợi điểm xa
hơn của các nhóm luồng nhỏ hơn-do đó tiêu tốn ít bộ nhớ hơn-khi việc nạp trên hệ
thống là chậm.
5 Dữ liệu đặc tả luồng
Các luồng thuộc một quá trình chia sẻ dữ liệu của quá trình. Thật vậy, chia sẻ
dữ liệu này cung cấp một trong những lợi điểm của lập trình đa luồng. Tuy nhiên, mỗi
luồng có thể cần bản sao dữ liệu xác định của chính nó trong một vài trường hợp.
Chúng ta sẽ gọi dữ liệu như thế là dữ liệu đặc tả luồng. Thí dụ, trong một hệ thống xử
lý giao dịch, chúng ta có thể phục vụ mỗi giao dịch trong một luồng. Ngoài ra, mỗi
giao dịch có thể được gán một danh biểu duy nhất. Để gán mỗi luồng với định danh
duy nhất của nó chúng ta có thể dùng dữ liệu đặc tả dữ liệu. Hầu hết thư viện luồng
gồm Win32 và Pthread – cung cấp một số biểu mẫu hỗ trợ cho dữ liệu đặc tả luồng.
Java cũng cung cấp sự hỗ trợ như thế.
Trong chương trước chúng ta mô tả lời gọi hệ thống fork được dùng để tạo
một quá trình bản sao riêng như thế nào. Trong một chương trình đa luồng, ngữ nghĩa
của các lời gọi hệ thống fork và exec thay đổi. Nếu một luồng trong lời gọi chương
trình fork thì quá trình mới sao chép lại quá trình tất cả luồng hay là một quá trình đơn
luồng mới? Một số hệ thống UNIX chọn hai ấn bản fork, một sao chép lại tất cả luồng
và một sao chép lại chỉ luồng được nạp lên lời gọi hệ thống fork. Lời gọi hệ thống
exec điển hình thực hiện công việc trong cùng một cách như được mô tả trong chương
trước. Nghĩa là, nếu một luồng nạp lời gọi hệ thống exec, chương trình được xác định
trong tham số exec sẽ thay thế toàn bộ quá trình-chứa tất cả luồng và các quá trình tải
nhẹ.
Việc sử dụng hai ấn bản fork phụ thuộc vào ứng dụng. Nếu exec bị hủy tức thì
sau khi phân nhánh (forking) thì sự sao chép lại tất cả luồng là không cần thiết khi
chương trình được xác định trong các tham số exec sẽ thay thế quá trình. Trong
trường hợp này, việc sao chép lại chỉ gọi luồng hợp lý. Tuy nhiên, nếu quá trình riêng
biệt này không gọi exec sau khi phân nhánh thì quá trình riêng biệt này nên sao chép
lại tất cả luồng.
2 Sự hủy bỏ luồng
Hủy một luồng là một tác vụ kết thúc một luồng trước khi nó hoàn thành.Thí
dụ, nếu nhiều luồng đang tìm kiếm đồng thời thông qua một cơ sở dữ liệu và một
luồng trả về kết quả, các luồng còn lại có thể bị hủy. Một trường hợp khác có thể xảy
ra khi người dùng nhấn một nút trên trình duyệt web để dừng trang web đang được
tải. Thường một trang web được tải trong một luồng riêng. Khi người dùng nhấn nút
stop, luồng đang nạp trang bị hủy bỏ.
Một luồng bị hủy thường được xem như luồng đích. Sự hủy bỏ một luồng đích có
thể xảy ra hai viễn cảnh khác nhau:
• Hủy bất đồng bộ: một luồng lập tức kết thúc luồng đích
• Hủy trì hoãn: luồng đích có thể kiểm tra định kỳ nếu nó sắp kết thúc, cho
phép luồng đích một cơ hội tự kết thúc trong một cách có thứ tự.
Sự khó khăn của việc hủy này xảy ra trong những trường hợp khi tài nguyên
được cấp phát tới một luồng bị hủy hay một luồng bị hủy trong khi việc cập nhật dữ
liệu xảy ra giữa chừng, nó đang chia sẻ với các luồng khác. Điều này trở nên đặc biệt
khó khăn với sự hủy bất đồng bộ. Hệ điều hành thường đòi lại tài nguyên hệ thống từ
luồng bị hủy nhưng thường nó sẽ không đòi lại tất cả tài nguyên. Do đó, việc hủy một
luồng bất đồng bộ có thể không giải phóng hết tài nguyên hệ thống cần thiết.
Một chọn lựa khác, sự hủy trì hoãn thực hiện bằng một luồng báo hiệu rằng
một luồng đích bị hủy. Tuy nhiên, sự hủy sẽ xảy ra chỉ khi luồng đích kiểm tra để xác
định nếu nó được hủy hay không. Điều này cho phép một luồng kiểm tra nếu nó sẽ bị
hủy tại điểm nó có thể an toàn bị hủy. Pthreads gọi những điểm như thế là các điểm
hủy (cancellation points).
Hầu hết hệ điều hành cho phép một quá trình hay một luồng bị hủy bất đồng
bộ. Tuy nhiên, Pthread API cung cấp sự hủy trì hoãn. Điều này có nghĩa rằng một hệ
điều hành cài đặt Pthread API sẽ cho phép sự hủy có trì hoãn.
3 Tín hiệu quản lý
Một tín hiệu (signal) được dùng trong hệ điều hành UNIX thông báo một sự
kiện xác định xảy ra. Một tín hiệu có thể được nhận hoặc đồng bộ hoặc bất đồng bộ
phụ thuộc mã và lý do cho sự kiện đang được báo hiệu. Một tín hiệu hoặc đồng bộ
hoặc bất đồng bộ đều theo sau cùng mẫu:
• Tín hiệu được phát sinh bởi sự xảy ra của một sự kiện xác định.
• Tín hiệu được phát sinh được phân phát tới một quá trình.
• Khi được phân phát xong, tín hiệu phải được quản lý.
Một thí dụ của tín hiệu đồng bộ gồm một truy xuất bộ nhớ không hợp lệ hay
chia cho 0. Trong trường hợp này, nếu một chương trình đang chạy thực hiện một
trong các hoạt động này, một tín hiệu được phát sinh. Các tín hiệu đồng bộ được phân
phát tới cùng một quá trình thực hiện thao tác gây ra tín hiệu (do đó lý do chúng được
xem đồng bộ).
Khi một tín hiệu được phát sinh bởi một sự kiện bên ngoài tới một quá trình
đang chạy, quá trình đó nhận tín hiệu bất đồng bộ. Thí dụ, tín hiệu kết thúc quá trình
với phím xác định (như <control><C>) hay thời gian hết hạn. Điển hình, một tín hiệu
bất đồng bộ được gởi tới quá trình khác.
Mỗi tín hiệu có thể được quản lý bởi một trong hai bộ quản lý:
• Bộ quản lý tín hiệu mặc định
• Bộ quản lý tín hiệu được định nghĩa bởi người dùng
Mỗi tín hiệu có một bộ quản lý tín hiệu mặc định được thực thi bởi nhân khi
quản lý tín hiệu. Hoạt động mặc định có thể được ghi đè bởi một hàm quản lý tín hiệu
được định nghĩa bởi người dùng. Trong trường hợp này, hàm được định nghĩa bởi
người dùng được gọi để quản lý tín hiệu hơn là hoạt động mặc định. Cả hai tín hiệu
đồng bộ và bất đồng bộ có thể được quản lý trong các cách khác nhau. Một số tín hiệu
có thể được bỏ qua (như thay đổi kích thước của cửa sổ); các tín hiệu khác có thể
được quản lý bằng cách kết thúc chương trình (như truy xuất bộ nhớ không hợp lệ).
Quản lý tín hiệu trong những chương trình đơn luồng không phức tạp; các tín
hiệu luôn được phân phát tới một quá trình. Tuy nhiên, phân phát tín hiệu là phức tạp
hơn trong những chương trình đa luồng, như một quá trình có nhiều luồng. Một tín
hiệu nên được phân phát ở đâu?
Thông thường, các tuỳ chọn sau tồn tại:
• Phân phát tín hiệu tới luồng mà tín hiệu áp dụng
• Phân phát tín hiệu tới mỗi luồng trong quá trình.
• Phân phát tín hiệu tới các luồng cụ thể trong quá trình.
• Gán một luồng xác định để nhận tất cả tín hiệu cho quá trình.
Phương pháp cho việc phân phát tín hiệu phụ thuộc vào loại tín hiệu được phát
sinh. Thí dụ, các tín hiệu đồng bộ cần được phân phát tới luồng đã phát sinh ra tín
hiệu và không phân phát tới luồng nào khác trong quá trình. Tuy nhiên, trường hợp
với tín hiệu bất đồng bộ là không rõ ràng. Một số tín hiệu bất đồng bộ - như tín hiệu
kết thúc một quá trình (thí dụ:<control><c>)- nên được gởi tới tất cả luồng. Một số ấn
bản đa luồng của UNIX cho phép một luồng xác định tín hiệu nào sẽ được chấp nhận
và tín hiệu nào sẽ bị khoá. Do đó, một vài tín hiệu bất đồng bộ có thể được phân phát
tới chỉ các luồng không khoá tín hiệu. Tuy nhiên, vì tín hiệu cần được quản lý chỉ một
lần, điển hình một tín hiệu được phân phát chỉ luồng đầu tiên được tìm thấy trong một
luồng mà không nghẽn tín hiệu. Solaris 2 cài đặt bốn tuỳ chọn; nó tạo một luồng xác
định trong mỗi quá trình cho quản lý tín hiệu. Khi một tín hiệu bất đồng bộ được gởi
tới một quá trình, nó được gởi tới luồng xác định, sau đó nó phân phát tín hiệu tới
luồng đầu tiên không khoá tín hiệu.
Mặc dù Windows 2000 không cung cấp rõ sự hỗ trợ tín hiệu, nhưng chúng có
thể được mô phỏng sử dụng lời gọi thủ tục bất đồng bộ (asynchronous produce calls-
APC). Tiện ích APC cho phép luồng người dùng xác định hàm được gọi khi luồng
người dùng nhận thông báo về một sự kiện xác định. Như được hiển thị bởi tên của
nó, một APC rất giống tín hiệu bất đồng bộ trong UNIX. Tuy nhiên, UNIX phải đấu
tranh với cách giải quyết tín hiệu trong môi trường đa luồng, phương tiện APC phức
tạp hơn như một APC được phân phát tới luồng xác định hơn quá trình.
4 Nhóm luồng
Trong phần VI.3, chúng ta mô tả kịch bản đa luồng của một trình phục vụ
web. Trong trường hợp này, bất cứ khi nào trình phục vụ nhận một yêu cầu, nó tạo
một luồng riêng để phục vụ yêu cầu đó. Ngược lại, tạo một luồng riêng thật sự cao
hơn tạo một quá trình riêng, dù sao một trình phục vụ đa luồng có thể phát sinh vấn
đề. Quan tâm đầu tiên là lượng thời gian được yêu cầu để tạo luồng trước khi phục vụ
yêu cầu, và lượng thời gian xoá luồng khi nó hoàn thành. Vấn đề thứ hai là vấn đề khó
giải quyết hơn: nếu chúng ta cho phép tất cả yêu cầu đồng hành được phục vụ trong
một luồng mới, chúng ta không thay thế giới hạn trên số lượng luồng hoạt động đồng
hành trong hệ thống. Những luồng không giới hạn có thể làm cạn kiệt tài nguyên hệ
thống, như thời gian CPU và bộ nhớ. Một giải pháp cho vấn đề này là sử dụng nhóm
luồng.
Ý tưởng chung nằm sau nhóm luồng là tạo số lượng luồng tại thời điểm khởi
động và đặt chúng vào nhóm, nơi chúng ngồi và chờ công việc. Khi một trình phục vụ
nhận một yêu cầu, chúng đánh thức một luồng từ nhóm- nếu một luồng sẳn dùng –
truyền nó yêu cầu dịch vụ. Một khi luồng hoàn thành dịch vụ của nó, nó trả về nhóm
đang chờ công việc kế. Nếu nhóm không chứa luồng sẳn dùng, trình phục vụ chờ cho
tới khi nó rảnh.
Nói cụ thể, các lợi ích của nhóm luồng là:
1) Thường phục vụ yêu cầu nhanh hơn với luồng đã có hơn là chờ để tạo luồng.
2) Một nhóm luồng bị giới hạn số lượng luồng tồn tại bất kỳ thời điểm nào. Điều
này đặc biệt quan trọng trên những hệ thống không hỗ trợ số lượng lớn các
luồng đồng hành.
Số lượng luồng trong nhóm có thể được đặt theo kinh nghiệm (heuristics) dựa trên
các yếu tố như số CPU trong hệ thống, lượng bộ nhớ vật lý và số yêu cầu khách hàng
đồng hành. Kiến trúc nhóm luồng tinh vi hơn có thể tự điều chỉnh số lượng luồng
trong nhóm dựa theo các mẫu sử dụng. Những kiến trúc như thế cung cấp lợi điểm xa
hơn của các nhóm luồng nhỏ hơn-do đó tiêu tốn ít bộ nhớ hơn-khi việc nạp trên hệ
thống là chậm.
5 Dữ liệu đặc tả luồng
Các luồng thuộc một quá trình chia sẻ dữ liệu của quá trình. Thật vậy, chia sẻ
dữ liệu này cung cấp một trong những lợi điểm của lập trình đa luồng. Tuy nhiên, mỗi
luồng có thể cần bản sao dữ liệu xác định của chính nó trong một vài trường hợp.
Chúng ta sẽ gọi dữ liệu như thế là dữ liệu đặc tả luồng. Thí dụ, trong một hệ thống xử
lý giao dịch, chúng ta có thể phục vụ mỗi giao dịch trong một luồng. Ngoài ra, mỗi
giao dịch có thể được gán một danh biểu duy nhất. Để gán mỗi luồng với định danh
duy nhất của nó chúng ta có thể dùng dữ liệu đặc tả dữ liệu. Hầu hết thư viện luồng
gồm Win32 và Pthread – cung cấp một số biểu mẫu hỗ trợ cho dữ liệu đặc tả luồng.
Java cũng cung cấp sự hỗ trợ như thế.
[Nguyên lý Tập luồng](/t3701p125-topic#16778) **Tập luồng (Thread Pools):** **-** **Tiến trình cha tạo lập sẵn một tập luồng khi khởi động.** **-** **Các luồng trong tập luồng luôn sẵn sàng chờ công việc.** **-** **Khi tiến trình cha (ví dụ Web Server) nhận thêm một yêu cầu, một luồng được đánh thức và đưa vào vận hành.** **-** **Phục vụ xong, luồng được đưa trả về tập luồng.** **-** **Nếu số yêu cầu lớn hơn số luồng trong tập, tiến trình cha chờ đến khi có luồng được giải phóng.** [TranQuyThanh (I11C)](/u1185)
- Tổng số bài gửi : 53
Join date : 30/08/2011
[Khái niệm luồng và ví dụ minh họa](/t3701p125-topic#16847)
Luồng: còn được gọi là 1 tiến trình nhẹ, nằm bên trong các tiến trình lớn. Có thể nói chúng là một đơn vị nhò sử dụng cpu. Chúng có thông tin về trạng thái như một tiến trình truyền thống.
VD1: Các bạn đi trên đường một chiều thì một chiều đó chình là một luồng hay 1 tiến trình, nhưng trong luồng đó người ta phân ra các luồng nhỏ hơn đó là các làn xe ô tô, xe tải, xe máy, và đi bộ. Các làn đó được gọi là một luồng hay 1 tiến trình nhẹ.
VD2: Trong công ty các bạn có : Sếp, thư kí, kế toán, nhân viên văn phòng...... Thì công ty là một luồng lớn hay 1 tiến trình để sản xuất là một sản phẩm gì đó vd như cơ khí, còn những cá thể trong công ty sẽ là một luồng hay một tiến trình nhẹ để xử lý các công việc khác nhau.......
Đó là một vài vd về luồng theo cách mình hiểu, hy vong sẽ giúp ích đôi chút cho các bạn.
VD1: Các bạn đi trên đường một chiều thì một chiều đó chình là một luồng hay 1 tiến trình, nhưng trong luồng đó người ta phân ra các luồng nhỏ hơn đó là các làn xe ô tô, xe tải, xe máy, và đi bộ. Các làn đó được gọi là một luồng hay 1 tiến trình nhẹ.
VD2: Trong công ty các bạn có : Sếp, thư kí, kế toán, nhân viên văn phòng...... Thì công ty là một luồng lớn hay 1 tiến trình để sản xuất là một sản phẩm gì đó vd như cơ khí, còn những cá thể trong công ty sẽ là một luồng hay một tiến trình nhẹ để xử lý các công việc khác nhau.......
Đó là một vài vd về luồng theo cách mình hiểu, hy vong sẽ giúp ích đôi chút cho các bạn.
[ledinhngankhanh (i11c)](/u1103)
- Tổng số bài gửi : 15
Join date : 26/08/2011
Age : 27
Đến từ : Kiên Giang
[Lập trình đa luồng + Ghi database](/t3701p125-topic#18302)
Hi các bạn !
Mình mới nghiên cứu về lập trình đa luồng nên có điểm này mình chưa hiểu, các bạn chỉ hộ nhé:
Để tránh xung đột tài nguyên khi kết nối database trong thread thì cách nào là hiệu quả. Nếu có ví dụ thì càng tốt. Mình có 1 ví dụ về lập trình socket, dùng thread chỗ này
while (true)
{
try
{
if (listener.Pending() == true)
{
if (Test(conn1, conn2)) //Kiểm tra kêt nối có bị hủy không
{
conn1 = new SqlConnection(");
conn2 = new SqlConnection("");
}
ConnectionThread newconnection = new ConnectionThread(listener,conn1,conn2);
Thread newthread = new Thread(new ThreadStart(newconnection.HandleConnection)); //Trong hàm HandleConnection mình khởi tạo kết nối mỗi khi có 1 luồng đi vào bằng cách gán conn1 và conn2
newthread.Start();
numthread++;
Console.WriteLine("Number thread : " + numthread.ToString());
}
Thread.Sleep(60);//Kết thúc 1 thread
}
catch { }
}
Mong các bạn giúp đỡ.
![ Very Happy](https://illiweb.com/fa/i/smiles/icon_biggrin.png)
Mình mới nghiên cứu về lập trình đa luồng nên có điểm này mình chưa hiểu, các bạn chỉ hộ nhé:
Để tránh xung đột tài nguyên khi kết nối database trong thread thì cách nào là hiệu quả. Nếu có ví dụ thì càng tốt. Mình có 1 ví dụ về lập trình socket, dùng thread chỗ này
while (true)
{
try
{
if (listener.Pending() == true)
{
if (Test(conn1, conn2)) //Kiểm tra kêt nối có bị hủy không
{
conn1 = new SqlConnection(");
conn2 = new SqlConnection("");
}
ConnectionThread newconnection = new ConnectionThread(listener,conn1,conn2);
Thread newthread = new Thread(new ThreadStart(newconnection.HandleConnection)); //Trong hàm HandleConnection mình khởi tạo kết nối mỗi khi có 1 luồng đi vào bằng cách gán conn1 và conn2
newthread.Start();
numthread++;
Console.WriteLine("Number thread : " + numthread.ToString());
}
Thread.Sleep(60);//Kết thúc 1 thread
}
catch { }
}
Mong các bạn giúp đỡ.
![ Very Happy](https://illiweb.com/fa/i/smiles/icon_biggrin.png)
[tam.nguyen](/u1467)
- Tổng số bài gửi : 1
Join date : 29/02/2012
[Mô hình đa luồng](/t3701p125-topic#19428)
Mô hình đa luồng dễ hiểu nhất là:
Khi làm đề tài hay báo cáo về một vấn đề gì đó, chúng ta thực hiện theo nhóm 2-3 người, có nghĩa là cùng một công việc nhưng nhiều người làm và điểm đến kết thúc giống nhau, đó là đa luồng.
Khi tiến hành từng đoạn trong một đề tài, thì đó không còn là đa luồng. (ai đảm nhận câu đó thì báo cáo về vấn đề đó)
Khi làm đề tài hay báo cáo về một vấn đề gì đó, chúng ta thực hiện theo nhóm 2-3 người, có nghĩa là cùng một công việc nhưng nhiều người làm và điểm đến kết thúc giống nhau, đó là đa luồng.
Khi tiến hành từng đoạn trong một đề tài, thì đó không còn là đa luồng. (ai đảm nhận câu đó thì báo cáo về vấn đề đó)
[DangLeHieu(I102C)](/u1479)
- Tổng số bài gửi : 18
Join date : 05/03/2012
**Sponsored content**
-
[Trang 6 trong tổng số 6 trang](javascript:Pagination();) • ![ Previous](https://illiweb.com/fa/empty.gif)
![ Previous](https://illiweb.com/fa/empty.gif)
[1](/t3701-topic), [2](/t3701p25-topic), [3](/t3701p50-topic), [4](/t3701p75-topic), [5](/t3701p100-topic), **6**
Trang
**6**trong tổng số **6**trang
Bạn
**Permissions in this forum:** **không có quyền**trả lời bài viết

X-XSS-Protection: 1; mode=block

Cache-Control: post-check=0, pre-check=0

X-Content-Type-Options: nosniff

Vary: User-Agent

P3P: CP="IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT"