Diễn Đàn Tin Học
 Trang chủ      Tutorial Room      Diễn đàn      Liên hệ - Góp ý
Diễn Đàn Tin Học
Thông tin
Download
Tutorial Room
Sản phẩm
Thống kê
Hiện có 13 người đang trực tuyến.
Google


Diễn Đàn Tin Học » Tutorial Room » Lập trình » Software Engineering » Craftsman-Truyện dài nhiều tập

Craftsman 6 - Socket Service
Phải dè chừng trường hợp dồn đuổi (race condition) khi mình đối phó với nhiều threads.

 
  Thông tin      
  Chuyên đề   Lập trình - Software Engineering  
  Tác giả   hnd  
  Bài gốc   http://www.vninformatics.com/forum/?action=msg&msg=1023492790#1023492790  
  Tựa gốc   Craftsman 6 -  Socket Service  
 

Xin được thay mặt diễn đàn cảm ơn bạn về bài viết này.

 

Mời các bạn xem tiếp phần craftsman6


Robert C. Martin
16 tháng Chín 2002

Sự kiện ngày hôm qua làm tôi lả người. Jerry và tôi giải quyết xong vấn đề tạo thừa số nguyên tố bằng cách tuồn qua mỗi lần một test case tí hon. Ðây là một cách giải quyết vấn đề kỳ lạ nhất mà tôi từng thấy nhưng nó lại làm việc ngon lành hơn giải pháp nguyên thủy của tôi.

Tôi lẩn quẩn vô định hướng trong các hành lang, ngẫm nghĩ đến chuyện này mãi. Tôi chẳng còn nhớ đến bữa tối hay ở đâu nữa. Tôi ngủ thiếp đi sớm hơn ngày thường và chiêm bao về những phân đoạn của mấy cái test bé nhỏ kia.

Sáng nay khi tôi trình diện Jerry, gã nói:

"Chào Alphonse. Mày đã sẵn sàng cho một chương trình thật chưa?"

"Ông thừa biết như thế! Thích quá, vâng, tôi sẵn sàng! Tôi quá mệt mấy cái trò thử nghiệm này lắm rồi."

"Tốt lắm! Tụi mình có một chương trình gọi là SMC dùng để biên dịch trạng thái ngữ pháp hữu hạn của máy vào môi trường Java. Ông C muốn tụi mình biến chương trình ấy thành một dịch vụ trên mạng."

"Ý ông là sao?", tôi hỏi.

Jerry xoay qua bản phác thảo rồi bắt đầu cùng một lúc giảng giải và minh hoạ.


  DDTHCode::Image


"Mình sẽ viết hai chương trình. Một cái gọi là SMCR Client và cái kia gọi là SMCR Server. Người dùng muốn biên dịch trạng thái ngữ pháp hữu hạn của máy sẽ dùng tên của hồ sơ để gọi SMCR Client. SMCR Client sẽ gởi hồ sơ đó đến một máy đặc biệt nơi SMCR Server đang hoạt động. SMCR Server sẽ chạy phần biên dịch SMC và gởi kết quả biên dịch về SMCR Client. SMCR Client sẽ viết các dữ kiện này vào thư mục của người dùng. Ðối với người dùng, cơ chế này không khác gì họ đang dùng SMC trực tiếp."

"OK, tôi nghĩ là tôi hiểu vấn đề." Tôi nói. "Nghe khá đơn giản."

"Nó khá đơn giản thật." Jerry đáp. "Nhưng đụng đến sockets lúc nào cũng thú vị hơn một chút."

Chúng tôi ngồi xuống máy và, như thường lệ, chuẩn bị tư thế viết cái unit test đầu tiên. Jerry suy nghĩ một lúc rồi trở lại bản phác thảo và phác hoạ ra một biểu đồ như sau:
 


DDTHCode::Image


"Ðây là ý nghĩ của tao về SMCR Server." Gã nói. "Chúng ta sẽ đặt mã quản lý socket vào class SocketService. Class này sẽ đón và quản trị các truy cập từ bên ngoài vào. Khi serve(port) được gọi, nó sẽ tạo một dịch vụ socket với port đã ấn định và bắt đầu tiếp nhận truy cập. Bất cứ khi nào có một truy cập xảy ra nó sẽ tạo một thread mới và chuyển giao nhiệm vụ điều tác sang method serve(socket) thuộc interface SocketServer. Với cách đó, mình tách rời mã quản trị socket ra khỏi phần mã mình muốn dùng để thao tác các dịch vụ khác."

Không nắm được lối khai triển này hiệu quả hay không, tôi chỉ gật đầu. Rõ ràng gã có lý do để nghĩ như thế. Tôi chỉ theo đuôi mà thôi.

Kế tiếp gã viết cái test như sau:

  public void testOneConnection() throws Exception {
    SocketService ss = new SocketService();
    ss.serve(999);
    connect(999);
    ss.close();
    assertEquals(1, ss.connections());
  }
 


"Chuyện tao làm ở đây có cái tên Intentional Programming (lập trình có chủ định). Jerry nói. "Tao gọi cái đoạn mã trong lúc nó chưa tồn tại. Làm như thế để diễn đạt chủ ý của mình về phương diện mã nguồn sẽ ra sao, làm việc như thế nào."

"OK." Tôi đáp. "Ông tạo cái SocketService rồi ông chỉ định nó tiếp nhận các truy cập trên port 999. Kế tiếp có vẻ như ông truy cập vào dịch vụ mới vừa được tạo ra trên port 999. Cuối cùng ông đóng SocketService và 'assert' rằng nó có một truy cập."

"Ðúng như thế." Jerry xác nhận.

"Nhưng làm sao ông biết SocketService sẽ cần các connections method?"

"Ô, có lẽ nó không cần. Tao chỉ đặt nó ở đó để có thể test nó."

"Như vậy không phí sao?" Tôi dạm hỏi.

Jerry nghiêm khắc nhìn tôi và trả lời: "Không có gì làm cho một cái test được dễ dàng lại là phung phí cả Alphonse. Tụi tao thường thêm các methods và các classes đơn giản để tạo điều kiện test các classes dễ dàng hơn."

Tôi không khoái cái connnections() method nhưng cứ làm thinh.

Chúng tôi chỉ viết vừa đủ phần contructor SocketService và các methods serve, close và connect để có thể biên dịch trọn bộ. Các functions này đều trống nên khi chúng tôi chạy thử, cái test bị hỏng như dự đoán.

Kế tiếp Jerry viết method connect như một phần của cái class cho test case
 

  private void connect(int port) {
    try {
       Socket s = new Socket("localhost", port);
       s.close();
    } catch (IOException e) {
       fail("could not connect");
    }
}
 


Chạy test này báo lỗi như sau:
testOneConnection: could not connect

Tôi nói: "Nó hỏng vì không thể tìm ra port 999 ở đâu hết, đúng không?"

"Ðúng vậy!" Jerry trả lời. "Nhưng chuyện đó dễ thôi. Ðây, sao mày không sửa nó đi?"

Trước giờ tôi chưa bao giờ viết mã cho socket nên không biết phải làm tiếp những gì. Jerry chỉ tôi đến phần ServerSocket trong Javadocs. Các ví dụ ở đây xem ra rất đơn giản nên tôi ngoáy thêm trong các methods của SocketService như sau:
 

  public class SocketService {
    private ServerSocket serverSocket = null;

    public void serve(int port) throws Exception {
       serverSocket = new ServerSocket(port);
    }

    public void close() throws Exception {
       serverSocket.close();
    }

    public int connections() {
       return 0;
    }
}
 

Chạy phần mã này nó báo: testOneConnection: expected: <1> but was: <0>

"À ha!" Tôi nói: "Nó tìm ra port 999. Quá đã! nhưng mình cần đếm số lần truy cập!"

Nên tôi đổi SocketService class như sau:

  import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

public class SocketService {
    private ServerSocket serverSocket = null;
    private int connections = 0;

    public void serve(int port) throws Exception {
       serverSocket = new ServerSocket(port);
       try {
           Socket s = serverSocket.accept();
           s.close();
           connections++;
       } catch (IOException e) {
       }
    }

    public void close() throws Exception {
       serverSocket.close();
    }
    public int connections() {
       return connections;
    }
}
 

Nhưng đoạn mã này không chạy, nó cũng chẳng báo lỗi. Khi tôi chạy phần test, nó bị treo.

"Chuyện gì đây cà?" Tôi thắc mắc.

Jerry mỉm cười. "Thử xem mày có thể mò ra không Alphonse. Dò thử đi."

"OK, xem thử. Cái chương trình test gọi serve để tạo ra socket và tiếp tục gọi accept. Ồ! accept không trả về cho đến khi nó có được một truy cập, và vì serve không hề trả lại nên mình không hề có cơ hội gọi connect."

Jerry gật đầu. "Vậy thì mày định sửa nó thế nào Alphonse?"

Tôi nghĩ ngợi một chút. Tôi cần gọi function connect sau khi gọi accept nhưng khi mình gọi accept nó không trả về cho đến khi mình gọi connect. Nhìn qua thì có vẻ không thể được.

"Không phải là không được đâu Alphonse." Jerry cất tiếng. "Mày chỉ cần tạo ra một cái thread."

Tôi lại ngẫm nghĩ thêm một chút nữa. Ðúng rồi, tôi có thể đặt phần gọi cho việc tiếp nhận truy cập trong một thread khác rồi mới bắt lấy thread đó và gọi bước truy cập.

Tôi nói: "Tôi biết lý do tại sao ông nói tạo mã nguồn cho socket thú vị hơn một chút rồi đó." và tôi thay đổi đoạn mã như sau:
 

  private Thread serverThread = null;
public void serve(int port) throws Exception {
     serverSocket = new ServerSocket(port);
     serverThread = new Thread(
        new Runnable() {
           public void run() {
             try {
                Socket s = serverSocket.accept();
                s.close();
                connections++;
             } catch (IOException e) {
             }
          }
       }
   );
   serverThread.start();
}
 


"Sử dụng cái anonymous inner class hay lắm đó Alphonse." Jerry nói.

"Cám ơn." Tôi cảm thấy sương sướng khi được gã khen. "Nhưng e nó tạo một chùm đuôi khỉ ở cuối cái function."

"Mình refactor nó sau, đầu tiên cứ chạy cái test cái đã."

Cái test chạy ổn nhưng Jerry có vẻ đăm chiêu, như thể gã vừa bị ai nói dối.

"Chạy cái test lần nữa xem Alphonse."

Tôi vui vẻ nhấn nút run và cái test lại chạy ngon lành.

"Lần nữa." Gã nói.

Tôi nhìn gã một giây xem thử gã có đùa không. Rõ ràng gã không đùa. Mắt gã dán chặt trên màn hình như thể gã đang săn lùng "dribin". Thế nên tôi nhấn nút một lần nữa và thấy:

testOneConnection: expected:<1> but was:<0>

"Hẵng đã!" tôi rú lên. "Không thể nào!"

"Ồ, có thể chớ." Jerry nói. "Tao đang đợi nó xảy ra."

Tôi nhấn nút liên tục. Trong mười lần có đến ba lần hỏng. Không biết tôi có loạn trí không? làm sao chương trình lại giở trò như vậy?

"Làm sao ông biết được vậy Jerry? Ông có liên hệ gì đến sấm truyền Aldebran hở?"

"Không, tao có viết loại mã này trước đây nên biết đôi điều cần dự phỏng. Mày có thể minh giải chuyện gì xảy ra không? Suy nghĩ cho thấu đáo và kỹ càng đó."

Ðúng là đau đầu nhưng tôi bắt đầu ráp từng phần lại với nhau. Tôi đến bản phác thảo và vẽ ra:



DDTHCode::Image



Khi đã minh giải xong, tôi tường trình sự vụ cho Jerry. "TestSocketServer gởi thông điệp serve(999) đến SocketService. SocketService tạo ServerSocket và serverThread rồi trả về. Sau đó TestSocketServer gọi connect phân đoạn đã tạo nên client socket. Hai sockets này hẳn đã tìm thấy nhau bởi vì chúng ta không nhận được lỗi 'could not connect'. ServerSocket hẳn đã tiếp nhận truy cập nhưng có lẽ serverThread chưa có cơ hội để chạy. Và trong khi serverThread bị cản, function connect đóng client socket lại. Kế tiếp TestSocketServer gởi thông điệp đóng cửa đến SocketService và phân đoạn này đóng serverSocket. Khi serverThread có cơ hội gọi function accept thì server socket đã đóng mất."

"Tao nghĩ mày đúng đó." Jerry nói. "Hai biến cố - tiếp nhận và đóng - thiếu đồng bộ và hệ thống này dễ hỏng với các trình tự xảy ra. Cái này mình gọi là trường hợp dồn đuổi (race condition). Chúng ta phải bảo đảm thắng cuộc đuổi chạy này."

Chúng tôi quyết định thử nghiệm giả thuyết của tôi bằng cách đưa vào các print statement trong khối 'catch' sau khi accept được gọi. Hẳn vậy, trong mười lần test, chúng tôi thấy thông điệp này ba lần.

Jerry hỏi tôi: "Thế thì làm sao mình cho unit test chạy đây?"

"Theo tôi nghĩ, dường như cái test không thể chỉ mở client socket rồi đóng lại ngay lập tức." Tôi đáp "Nó cần phải đợi bước tiếp nhận."

Gã nói: "Mình có thể đợi 100ms trước khi đóng client socket."

"Ừa, chắc là được nhưng hơi ẹ." Tôi trả lời.

"Hãy xem thử mình làm cho nó chạy được hay không cái đã rồi tính chuyện refector sau."

Nên tôi thay đổi method connect như sau:

  private void connect(int port) {
      try {
         Socket s = new Socket("localhost", port);
         try {
             Thread.sleep(100);
         } catch (InterruptedException e) {
         }
        s.close();
      } catch (IOException e) {
         fail("could not connect");
      }
    }
 

Phần thay đổi cho kết quả test 10 trên 10.

"Gớm thật." Tôi nói. "Khi mình đối phó với nhiều threads thì phải dè chừng trường hợp dồn đuổi (race condition). Nhấn nút test nhiều lần là một thói quen tốt nên tập."

"Hên là mình khám phá ra nó trong mấy cái test case." Tôi nói. "Không thì khó mà kiếm ra nó sau khi hệ thống đã chạy."

Jerry chỉ gật đầu.

<đón xem phần kế tiếp>
 

Các bài viết mới nhất
Bạn không được phép truy cập vào địa chỉ này!
[Download] - NT Password Recovery Bootdisk
Craftsman 18 - Slow and Steady
Quản lý MySQL Server sử dụng lệnh trên console
Memory-RAM - Một số thuật ngữ và kỹ thuật
Đưa chương trình vào đường dẫn hệ thống
Thay thế BIND với djbdns - phần 1
Phương pháp khôi phục lại password trong hệ thống Win2k/XP/2K3 (Support NTFS)
Cài đặt ActivePython 2.4 trên IIS
Sử Dụng Tiếng Việt Với LaTeX
Cài đặt 1 SMTP server tại nhà với Microsoft IIS
Linux - Vì sao sáng trên bầu trời CNTT
Biên dịch Linux kernel - phần 4
Tự học lập trình Borland Delphi
Thiết kế và Lập trình Web bằng ASP
Cài đặt PHP 4 trên IIS
Giới thiệu về XML-RPC
Sử dụng CSDL MySQL
Một chương trình download manager đơn giản
Giới thiệu - Sơ lược về ngôn ngữ PHP
Các bài viết liên quan
Craftsman 18 - Slow and Steady
Craftsman 17 - Call the Guards
Craftsman 16 - Excess Politesse
Craftsman 15 - Ess Are Pee
Craftsman 14 - Transaction Actions
Craftsman 13 - A Better Solution
Craftsman 12 - Three Ugly Lines
Craftsman 11 - Forget the Main()
Craftsman 10 - Iterations Unbound
Craftsman 9 - Dangerous Threads
Craftsman - 8 : Testing in Synch
Craftsman 7 - Socket Service2
Craftsman 6 - Socket Service
Craftsman 05 - Baby Steps
Craftsman 04 - A Test of Patience
Craftsman 03 - Clarity and Collaboration
Craftsman 02 - Crash Diet
Craftsman 01 - Opening Disaster
Quảng cáo
HOME | TUTORIAL ROOM | FORUM | CONTACT