4 Kiểm chứng thử nghiệm với PAT
3.1 Kiểm chứng mô hình cổ điển và hiện đại
Quy trình phát triển phần mềm theo mô hình thác nước và mối quan hệ với kiểm chứng mô hình cổ điển cũng như kiểm chứng mô hình hiện đại được biểu diễn trong Hình 3.1.
Kiểm chứng mô hình trên mã nguồn phải đối mặt với rất nhiều khó khăn [6]. Trạng thái của chương trình bao gồm các biến toàn cục, biến địa phương phải được biểu diễn một cách hiệu quả nhằm tránh vấn đề bùng nổ trạng thái [8]. Chuyển các loại mã nguồn về dạng assembly và mô hình hóa mỗi dòng mã bằng một sự kiện đôi khi không hiệu quả, cần một cách trừu tượng hóa tốt hơn và tối ưu hóa theo từng ngôn ngữ lập trình [12]. Đề xuất của luận văn dựa trên ngôn ngữ lập trình C# và trích xuất mô hình theo phạm trù bài toán xử lý tương tranh trong lập trình đa luồng [15]. Quá trình trích xuất mô hình là trực tiếp, sử dụng ngữ nghĩa tương ứng giữa ngôn ngữ C# và ngôn ngữ đặc tả mô hình CSP# mà không chuyển qua ngôn ngữ trung gian như một số cách tiếp cận trước đây. Kết quả là mô hình được trích xuất dưới dạng một đặc tả bằng CSP#.
Hiện nay vấn đề kiểm tra trực tiếp trên mã nguồn đang nhận được rất nhiều sự quan tâm, đặc biệt là trong phát triển các hệ thống nhúng. Ví dụ, có một số công cụ kiểm tra trực tiếp trên mã nguồn C như BLAST, SLAM, CBMC [6] [8]. Các công cụ này có thể chuyển mã nguồn về chương trình dạng Boolean hoặc ngôn ngữ trung gian (CIL - C Intermediate Language) để kiểm tra với các bộ chứng minh định lý (Theorem provers), các bộ giải SAT solvers [24]. Một số công cụ khác chuyển mã nguồn C về dạng đầu vào của bộ kiểm tra mô hình như SPIN.
3.3.1. Xử lý đa luồng trong C#
So với C, các ngôn ngữ như Java và C# được cung cấp thêm cơ chế xử lý đa luồng (multi thread). Có một số công cụ được xây dựng để kiểm chứng mô hình cho vấn đề xử lý đa luồng trên hai ngôn ngữ này như Java PathFinder [19] và MoonWalker [21]. Tuy nhiên, hai công cụ này vẫn sử dụng cách tiếp cận truyền thống là chuyển mã nguồn về dạng byte-code trung gian.
Trong lập trình đa luồng của C#, một luồng là một thực thi độc lập, được xử lý dạng tương tranh với các luồng khác. Chương trình C# (Console, WPF, hoặc Windows Forms) bắt đầu với một luồng gọi làmain, luồng này được tạo tự động bởi CLR và hệ điều hành. Từ thực thi của luồngmain các luồng khác được thêm vào và xử lý tương tranh (Hình 3.2). Ví dụ một chương trình đa luồng xử lý tương tranh:
using System;
using System.Threading; class ThreadTest
{
static void Main() {
Thread t = new Thread (WriteY); t.Start();
for (int i = 0; i < 1000; i++) Console.Write ("x");
}
static void WriteY() {
for (int i = 0; i < 1000; i++) Console.Write ("y");
} }
Trong luồngmain tạo ra và khởi chạy một luồng t thực thi lặp lại việc in ra màn hình ký tựy. Đồng thời, luồngmain cũng lặp lại việc in ra màn hình ký tự x. Kết quả xử lý tương tranh hai luồngmainvà tnhư sau:
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxyyyyyyyyyyyyyyyyyyyxxxxxxxx xxxxyxxyyyyyyyyyyyyyyyyyyyyyxxxxxx ...
Hình 3.2 minh họa cơ chế xử lý trong lập trình đa luồng của C#. CLR gán mỗi luồng tới một ngăn xếp bộ nhớ riêng, đó đó các biến địa phương của mỗi luồng
Hình 3.2:Xử lý đa luồng trong C#
được giữ riêng biệt. Tính riêng biệt của biến địa phương được thể hiện qua ví dụ sau:
static void Main() {
new Thread (Go).Start(); Go();
}
static void Go() {
for (int cycles = 0; cycles < 5; cycles++) Console.Write (’?’);
}
??????????
Một bản sao tách biệt của biến cyclesđược tạo trên mỗi ngăn xếp bộ nhớ của các luồng, do đó đầu ra là mười dấu hỏi. Các luồng chia sẻ dữ liệu nếu chúng có chung tham chiếu tới cùng một đối tượng như sau:
class ThreadTest {
bool done;
static void Main() {
ThreadTest tt = new ThreadTest(); new Thread (tt.Go).Start();
tt.Go(); } void Go() { if (!done) {
done = true;
Console.WriteLine (" Done"); }
} }
Bởi vì cả hai luồng đều gọi phương thứcGo()trên cùng một thể hiện của đối tượng
ThreadTest, chúng chia sẻ biến done. Điều này dẫn đến chuỗi "Done" được in ra một lần thay vì hai lần. Các trường tĩnh (khai báo với từ khóastatic) cung cấp một cách khác để chia sẻ dữ liệu giữa các luồng. Ví dụ sau tương tự như ví dụ trước nhưng sử dụng trường tĩnh:
class ThreadTest {
static bool done; //done được khai báo là trường tĩnh static void Main()
{
ThreadTest tt = new ThreadTest(); new Thread (tt.Go).Start();
tt.Go(); } void Go() { if (!done) { done = true; Console.WriteLine (" Done"); } } }
Cả hai ví dụ trên đều cho thấy một vấn đề: tính an toàn. Đầu ra của chương trình có thể là không đơn định khi xử lý tương tranh trong lập trình đa luồng. Chuỗi
"Done" có thể được in ra một hoặc hai lần. Nếu đổi trật tự của các lệnh trong phương thứcGo(), tỷ lệ chuỗi"Done"được in ra hai lần tăng lên đáng kể.
static void Go() { if (!done) { Console.WriteLine ("Done"); done = true; }
}
Nguyên nhân là do một luồng có thể đánh giá lệnhif khi luồng khác vừa thực thi lệnhWriteLinemà chưa thực thi lệnh gán giá trị biến donethành True. Biện pháp khắc phục vấn đề này là sử dụng một khóa riêng trong khi đọc và ghi vào các trường thông tin. C# cung cấp lệnhlock cho mục đích này:
class ThreadSafe {
static bool done;
static readonly object locker = new object(); static void Main()
{
new Thread (Go).Start(); Go();
}
static void Go() { lock (locker) { if (!done) { Console.WriteLine ("Done"); done = true; } } } }
Khi hai luồng xử lý tương tranh tranh nhau một khóa, một luồng phải chờ cho đến khi khóa được giải phóng bởi luồng kia và có thể sử dụng.
3.3.2. Trích xuất mô hình từ mã nguồn C#
Mục tiêu của luận văn là tự động trích xuất mô hình từ mã nguồn C# cho bài toán xử lý tương tranh trong lập trình đa luồng. Điều này giúp giảm thiểu thời gian cũng như sai sót khi phải mô hình hóa hệ thống một cách thủ công. Trong quá trình trích xuất, việc quyết định tính chi tiết của mô hình rất quan trọng vì nó ảnh hưởng trực tiếp tới hiệu năng xử lý. Nếu mã nguồn C# được chuyển đổi dưới dạng byte-code, độ chi tiết sẽ rất cao nhưng đôi khi không cần thiết. Trích xuất mô hình trực tiếp dựa trên ngữ nghĩa tương ứng giữa C# và CSP# là một giải pháp cho vấn đề này.
3.3.2.1. Cơ chế xử lý mã C# trong mô hình CSP#
Mã nguồn C# có thể được gắn vào mô hình đặc tả bằng CSP# thông qua các sự kiện dưới dạng sau:
event{code1; code2;}
đây là cơ chế chính để xử lý mã nguồn C# trong CSP# và được hỗ trợ bởi bộ công cụ PAT (trình bày trong chương 4). Code1 và code2 là các đoạn mã chuẩn trong thư viện C#.
Các đoạn mã xử lý liên tiếp nếu không truy cập vào dữ liệu bên ngoài dữ liệu địa phương của luồng, được nhóm chung vào một khối mã và đính vào một sự kiện ẩntaucùng với các biến địa phương.
Nếu mã nguồn truy cập tới phương thức của các đối tượng khác (nằm trong số các biến chia sẻ toàn cục), lời gọi phương thức có thể được mô hình hóa dưới dạng một sự kiện hoặc một tiến trình. Ví dụ đoạn mã C#:
{codeblock1; codeblock2; methodcall(); codeblock3;} có thể được mô hình hóa bằng CSP# dạng như sau:
tau{codeblock1; codeblock2;} -> methodcall(); tau{codeblock3;} -> Skip;
hoặc dạng:
tau{codeblock1; codeblock2; methodcall; codeblock3;} -> Skip;
Do đó lựa chọn cách mô hình hóa sẽ ảnh hưởng tới tính nguyên tử của mô hình. Trong ví dụ trên cách mô hình thứ hai rõ ràng có tính nguyên tử lớn hơn cách mô hình hóa thứ nhất.
Tính đa hình trong C# được ẩn đi đối với thư viện C# chuẩn khi đưa vào mô hình CSP#. Điều này tránh việc phải duy trì các quan kệ kế thừa phức tạp và do đó đơn giản hóa mô hình. Cụ thể hơn, thư viện C# (dạng tệp tin .dll) bao gồm các định nghĩa lớp và quan hệ giữa chúng. Đối với các phương thức của một lớp nhất định, nếu phương thức này mang tính nguyên tử, mã nguồn được chuyển đổi dễ dàng. Ngược lại, phương thức cần được mô hình hóa bởi các cơ chế thực thi (đan xen, song song đồng bộ...) trong CSP#.
Các khóa được sử dụng trong C# để điều khiển truy nhập tài nguyên chung. Trong CSP# có thể sử dụng các kênh truyền và tiến trình được bảo vệ để đạt kết quả tương tự.
3.3.2.2. Trích xuất mô hình
Trong phần 3.3.1 và 3.3.2.1 luận văn đã trình bày một số khái niệm cơ bản về xử lý tương tranh trong lập trình đa luồng với C#, cơ chế xử lý mã nguồn C# trong đặc tả mô hình bằng CSP#. Dựa trên ngữ nghĩa tương ứng giữa hai ngôn ngữ, luận văn đề xuất phương pháp trích xuất mô hình từ mã nguồn C# với bài toán xử lý tương tranh trong lập trình đa luồng. Bảng 3.1 biểu diễn các luật tương đương được dùng trong quá trình trích xuất mô hình. Từ mã nguồn C#, tiến hành phân tích cấu trúc, sau đó sử dụng các luật tương đương để trích xuất ra mô hình đặc tả bằng CSP#.
Bảng 3.1: Các luật tương đương trong trích xuất mô hình
Luật C# CSP#
1 System Process
2 Process Process
3 Thread Process
4 Local variable Local variable
5 Global variable Global variable
6 Method Process
7 Atomic code block Statement Block inside Events 8 Passing Data to a Thread Global variable
9 Thread priority Atomic process
10 Synchronization Parallel composition, Channel (0) 11 Asynchronization Interleaving, Channel (1..)
12 Locking Guarded process
13 Thread Interrupt Interrupt
14 Condition choice Condition choice
15 Abort Stop
16 Thuộc tính cần kiểm tra Khẳng định (Assertion)
Một ví dụ về trích xuất mô hình áp dụng các luật tương đương trong bảng 3.1 như sau:
Mã nguồn C#: class ThreadSafe {
static bool done; //Global variable static void Main() //System
//Thread "main"
new Thread (Go).Start(); //Thread "Go" //Asynchronization
Go(); }
static void Go() //Method {
if (!done) { //Condition choice
Console.WriteLine("Done"); //Atomic code block done = true; //Atomic code block
} } }
Đặc tả mô hình CSP# đạt được khi áp dụng các luật tương đương: //@@Model@@
//Luật 5
var done = false; // Luật 6 + 7 + 14
Go = if(done == false) {e.(!done) -> tau{done = true;} -> Skip}; //Luật 11 + 3
ThreadSafe = Go ||| Go; // Luật 1
System = ThreadSafe;
Quan sát về sự kiện e.(!done) trong mô hình sẽ biểu diễn kết quả của lệnh Con- sole.WriteLine ("Done")trong mã nguồn. Tức là nếu quan sát được sự kiệne.True
trong hành vi của mô hình thì khi thực thi mã nguồn chuỗi "Done" được in ra màn hình, ngược lại nếu quan sát được sự kiệne.Falsethì chuỗi "Done" không được in ra khi thực thi mã nguồn.
Mô hình kết quả đạt được sau quá trình trích xuất được sử dụng để tiến hành mô phỏng và kiểm chứng trong PAT. Quá trình mô phỏng và kiểm chứng được hỗ trợ tự động. Chi tiết về kiểm chứng mô hình trong PAT sẽ được trình bày trong chương 4. Hình 3.3 biểu diễn đồ thị trạng thái đầy đủ (sinh bởi PAT) của mô hình kết quả ứng với ví dụ trên.
Theo Hình 3.3 có thể thấy, tùy vào đường dẫn thực thi mà sự kiệne.(!done)có thể xuất hiện 1 hoặc 2 lần, tức là chuỗi "Done" trong chương trình C# có thể được in ra 1 hoặc 2 lần. Như vậy mô hình mô tả đúng hành vi xử lý tương tranh của hệ thống thật.
So sánh với các cách tiếp cận khác, đề xuất của luận văn sử dụng thư viện C# chuẩn để cung cấp thêm các xử lý hiệu quả trên các biến của chương trình. Trong
[if((done == false))]
[if((done == false))] e.true
[if((done == false))] τ [if!((done == false))] terminate τ e.true τ τ e.false e.true 3 4 6 7 8 9 2 1 5 11 10