Tải bản đầy đủ (.pdf) (14 trang)

C# và các lớp cơ sở Thread ( luồng ) – Phần 1 doc

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (122.41 KB, 14 trang )

C# và các lớp cơ sở
Thread ( luồng ) – Phần 1


1 thread là 1 chuỗi liên tiếp những sự thực thi trong chương trình. trong 1
chương trình C# ,việc thực thi bắt đầu bằng phương thức main() và tiếp tục
cho đến khi kết thúc hàm main().
Cấu trúc này rất hay cho những chương trình có 1 chuỗi xác định những
nhiệm vụ liên tiếp . nhưng thường thì 1 chương trình cần làm nhiều công
việc hơn vào cùng một lúc.ví dụ trong internet explorer khi ta đang tải 1
trang web thì ta nhấn nút back hay 1 link nào đó , để làm việc này Internet
Explorer sẽ phải làm ít nhất là 3 việc :
- Lấy dữ liệu đưọc trả về từ internet cùng với các tập tin đi kèm .
- Thể hiện trang web
- Xem người dùng có nhập để làm thứ gì khác không
Để đơn giản vấn đề ta giả sử Internet Ecplorer chỉ làm 2 công việc :
- Trình bày trang web
- Xem người dùng có nhập gì không
Để thực hành việc này ta sẽ viết 1 phương thức dùng để lấy và thể hiện trang
web .giả sử rằng việc trình bày trang web mất nhiều thời gian ( do phải thi
hành các đoạn javascript hay các hiệu ứng nào đó ) .vì thế sau một khoảng
thời gian ngắn khoảng 1/12 giây , phương thức sẽ kiểm tra xem người dùng
có nhập gì không .nếu có thì nó sẽ đuơc xử lí, nếu không thì việc trình bày
trang sẽ được tiếp tục. và sau 1/12 giây việc kiểm tra sẽ được lặp lại.tuy
nhiên viết phương thức này thì rất phức tạp do đó ta sẽ dùng kiến trúc event
trong window nghĩa là khi việc nhập xảy ra hệ thống sẽ thông báo cho ứng
dụng bằng cách đưa ra một event . ta sẽ cập nhật phương thức để cho phép
dùng các event :
Ta sẽ viết 1 bộ xử lí event để đáp ứng đối với việc nhập của người dùng.
Ta sẽ viết 1 phương thức để lấy và trình bày dữ liệu . phương thức này được
thực thi khi ta không làm bất cứ điều gì khác.


Ta hãy xem cách phương thức lấy và trình bày trang web làm việc : đầu tiên
nó sẽ tự định thời gian. trong khi nó đang chạy, máy tính không thể đáp ứng
việc nhập của người dùng . do đó nó phải chú ý đến việc định thời gian để
gọi phương thức kiểm tra việc nhập của người dùng ,nghĩa là phương thức
vừa chạy vừa quan sát thời gian.bên cạnh đó nó còn phải quan tâm đến việc
lưu trữ trạng thái trước khi nó gọi phương thức khác để sau khi phương thức
khác thực hiện xong nó sẽ trả về đúng chỗ nó đã dừng.Vào thời window 3.1
đây thực sự là những gì phải làm để xử lí tình huống này. tuy nhiên ở NT3.1
và sau đó là windows 95 đã có việc xử lí đa luồng điều này làm việc giải
quyết vấn đề tiện lợi hơn.
Các ứng dụng với đa luồng
Trong ví dụ trên minh hoạ tình huống 1 ứng dụng cần làm nhiều hơn 1 công
việc.vì vậy giải pháp rõ ràng là cho ứng dụng thực thi nhiều luồng. như ta đã
nói ,1luồng đaị diện cho 1 chuỗi liên tiếp các lệnh mà máy tính thực thi . do
đó không có lí do nào 1 ứng dụng lại chỉ có 1 luồng.thực vậy ta có thể có
nhiều luồng nếu ta muốn. tất cả điều cần là mỗi lần ta tạo 1 luồng thực thi
mới, ta chỉ định 1 phương thức mà thực thi nên bắt đầu với. luồng đầu tiên
trong ứng dụng luôn được thực thi trong main() vì đây là phương thức mà
.NET runtime luôn lấy để bắt đầu.các luồng sau sẽ được bắt đầu bên trong
ứng dụng.
Công việc này làm như thế nào ?
- Một bộ xử lí chỉ có thể làm một việc vào một lúc,nếu có một hệ thống đa
xử lí , theo lí thuyết có thể có nhiều hơn một lệnh được thi hành đồng bộ,
mỗi lệnh trên một bộ xử lí, tuy nhiên ta chỉ làm việc trên một bộ xử lí do đó
các công việc không thể xảy ra cùng lúc.thực sự thì hệ điều hành window
làm điều này bằng một thủ tục gọi là pre emptive multitasking
- Thủ tục này nghĩa là window lấy 1 luồng vào trong vài tiến trình và cho
phép luồng đó chạy 1 khoảng thời gian ngắn .gọi là time slice. khi thời gian
này kế thúc. window lấy quyền điều khiển lại và lấy 1 luồng khác và lại cấp
1 khoảng thời gian time slice . vì khoảng thời gian này quá ngắn nên ta có

cảm tưởng như mọi thứ đều xảy ra cùng lúc.
- Thậm chí khi ứng dụng ta chỉ có một luồng thì thủ tục này vẫn đưọc dùng
vì có thể có nhiều tiến trình khác đang chạy trên hệ thống mỗi tiến trình cần
các time slice cho mỗi luồng của nó.vì vậy khi ta có nhiều cửa sổ trên màn
hình mỗi cái đại diện cho một tiến trình khác nhau, ta vẫn có thể nhấn vào
bất kì của sổ nào
Và nó đáp ứng ngay lập tức.thực sự việc đáp ứng nay không phải ngay lập
tức- nó xảy ra vào sau khoảng thời gian time slice của luồng đương thời.vì
thời gia này quá ngắn nên ta cảm thấy như nó đáp ứng ngay lập tức.
Thao tác luồng
Luồng được thao tác bằng cách dùng lớp Thread nằm trong namespace
System.Threading . 1 thể hiện của luồng đaị diện cho 1 luồng.ta có thể tạo
các luồng khác bằng cách khởi tạo 1 đối tượng luồng
Bắt đầu 1 luồng
Giả sử rằng ta đang viết 1 trình biên tập hình ảnh đồ hoạ, và người dùng yêu
cầu thay đổi độ sâu của màu trong ảnh. ta bắt đầu khởi tạo 1 đối tượng luồng
như sau :
// entryPoint được khai báo trước là 1 delegate kiểu ThreadStart
Thread depthChangeThread = new Thread(entryPoint);
Đoạn mã trên biểu diễn 1 hàm dựng của Thread 1 thông số chỉ định điểm
nhập của 1 luồng. - đó là phương thức nơi luồng bắt đầu thi hành.trong tình
huống này ta dùng thông số là delegate, 1 delegate đã được định nghĩa trong
System.Threading gọi là aThreadStart , chữ kí của nó như sau :
public delegate void ThreadStart();
Thông số ta truyền cho hàm dựng phải là 1 delegate kiểu này .
Ta bắt đầu luồng bằng cách gọi phương thức Thread.Start() , giả sử rằng ta
có phương thức ChangeColorDepth() :
void ChangeColorDepth()
{
// xử lí để thay đổi màu

}
Sắp xếp lại ta có đoạn mã sau :
ThreadStart entryPoint = new ThreadStart(ChangeColorDepth);
Thread depthChangeThread = new Thread(entryPoint);
depthChangeThread.Name = "Depth Change Thread";
depthChangeThread.Start();
Sau điểm này ,cả hai luồng sẽ chạy đồng bộ
Trong mã này ta đăng kí tên cho luồng bằng cách dùng thuộc tính
Thread.Name. không cần thiết làm điều này nhưng nó có thể hữu ích :

Lưu ý rằng bởi vì điểm đột nhập luồng ( trong ví dụ này là
ChangeColorDepth() ) không thể lấy bất kì thông số nào.ta sẽ phải tìm 1
cách nào đó để truyền thông số cho phương thức nếu cần.cách tốt nhất là
dùng các trường thành viên của lớp mà phương thức này là thành viên.cũng
vậy phương thức không thể trả về bất cứ thứ gì .
Mỗi lần ta bắt đầu 1 luồng khác ,ta cũng có thể đình chỉ, hồi phục hay bỏ
qua nó.đình chỉ nghĩa là cho luồng đó ngủ ( sleep) - nghĩa là không chạy
trong 1 khoảng thời gian. sau đó nó thể đưọc phục hồi ,nghĩa là trả nó về
thời diểm mà nó bị định chỉ. nếu luồng đưọc bỏ , nó dừng chạy. window sẽ
huỷ tất cả dữ liệu mà liên hệ đến luồng đó, để luồng không thể được bắt đầu
lại.
tiếp tục ví dụ trên , ta giả sử vì lí do nào đó luồng giao diện người dùng trình
bày 1 hộp thoại cho người dùng cơ hội để đình chỉ tạm thời sự đổi tiến trình
. ta sẽ soạn mã đáp ứng trong luồng main :
depthChangeThread.Suspend();
Và nếu người dùng được yêu cầu cho tiến trình được phục hồi :
depthChangeThread.Resume();
Cuối cùng nếu người dùng muốn huỷ luồng :
depthChangeThread.Abort();
Phương thức Suspend() có thể không làm cho luồng bị định chỉ tức thời mà

có thể là sau 1 vài lệnh, điều này là để luồng được đình chỉ an toàn. đối với
phương thức Abort() nó làm việc bằng cách ném ra biệt lệ
ThreadAbortException. ThreadAbortException là 1 lớp biệt lệ đặc biệt mà
không bao giờ được xử lí.nếu luồng đó thực thi mã bên trong khối try, bất kì
khối finally sẽ được thực thi trước khi luồng bị huỷ.
Sau khi huỷ luồng ta có thể muốn đợi cho đến khi luồng thực sự bị huỷ trước
khi tiếp tục luồng khác ta có thể đợi bằng cách dùng phương thức join() :
depthChangeThread.Abort();
depthChangeThread.Join();
Join() cũng có 1 số overload khác chỉ định thời gian đợi . nếu hết thời gian
này việc thi hành sẽ được tiếp tục.
nếu một luồng chính muốn thi hành 1 vài hành động trên nó , nó cần 1 tham
chiếu đến đối tượng luồng mà đại diện cho luồng riêng. nó có thể lấy 1 tham
chiếu sử dụng thuộc tính static, CurrentThread ,của lớp Thread :
Thread myOwnThread = Thread.CurrentThread;
Có hai cách khác nhau mà ta có thể thao tác lớp Thread:
- Ta có thể khởi tạo 1 đối tượng luồng , mà sẽ đại diện cho luồng đang
chạy và các thành viên thể hiện của nó áp dụng đến luồng đang chạy
- Ta có thể gọi 1 số phương thức static . những phương thức này sẽ áp
dụng đến luồng mà ta thực sự đang gọi phương thức từ nó.
1 phương thức static mà ta muốn gọi là Sleep(), đơn giản đặt luồng đang
chạy ngủ 1 khoảng thời gian, sau đó nó sẽ tiếp tục.
Ví dụ ThreadPlayaround
Phần chính của ví dụ này là 1 phương thức ngắn , DisplayNumber() ,
phương thức này cũng bắt đầu bằng việc trình bày tên và bản điạ ( culture)
của luồng mà phương thức đang chạy :
static void DisplayNumbers()
{
Thread thisThread = Thread.CurrentThread;
string name = thisThread.Name;

Console.WriteLine("Starting thread: " + name);
Console.WriteLine(name + ": Current Culture = " +
thisThread.CurrentCulture);
for (int i=1 ; i<= 8*interval ; i++)
{
if (i%interval == 0)
Console.WriteLine(name + ": count has reached " + i);
}
}
Việc trình bày số tuỳ thuộc vào interval,là 1 trường mà giá trị được gõ bởi
người dùng .nếu người dùng gõ 100 sẽ trình bày đến 800, trình bày giá trị
100,200,300,400,500,600,700,800. nếu ngươì dùng gõ 1000 thì sẽ trình bày
đến 8000, trình bày giá trị 1000,2000,3000,4000,5000,6000,7000,8000 .
Những gì ThreadPlayaround() làm là bắt đầu luồng công việc thứ hai,mà
chạy DisplayNumber(), nhưng ngay sau khi bắt đầu luồng công việc, luồng
chính bắt đầu thi hành cùng phương thức. nghĩa là ta thấy cả hai việc đếm
xảy ra cùng lúc.
Phương thức main() cho ThreadPlayaround và lớp chứa đựng của nó là :
class EntryPoint
{
static int interval;
static void Main()
{
Console.Write("Interval to display results at?> ");
interval = int.Parse(Console.ReadLine());
Thread thisThread = Thread.CurrentThread;
thisThread.Name = "Main Thread";
ThreadStart workerStart = new ThreadStart(StartMethod);
Thread workerThread = new Thread(workerStart);
workerThread.Name = "Worker";

workerThread.Start();
DisplayNumbers();
Console.WriteLine("Main Thread Finished");
Console.ReadLine();
}
Trong phương thức main() đầu tiên ta hỏi người dùng interval, sau đó ta lấy
1 tham chiếu đến đối tượng luồng mà đại diện cho luồng chính .
Kế tiếp ta tạo ra luồng công việc , đặt cùng tên , và bắt đầu nó ,truyền cho nó
1 delegate mà chỉ định phương thức mà nó phải bắt đầu trong đó , phương
thức workerStart. cuối cùng , ta gọi phương thức DisplayNumber() để bắt
đầu đếm. điểm đột nhập của luồng này là :
static void StartMethod()
{
DisplayNumbers();
Console.WriteLine("Worker Thread Finished");
}
Lưu ý rằng tất cả các phương thức này đều là phương thức static trong cùng
1 lớp,EntryPoint. biến i trong phương thức DisplayNumber() được dùng để
đếm là 1 biến cục bộ. biến này không chỉ có phạm vi đối với phương thức
mà được định nghĩa mà còn khả kiến đối với luồng đang thực thi phương
thức đó. nếu 1 luồng khác bắt đầu thi hành cùng 1 phương thức , thì luồng
đó sẽ lấy 1 bản sao chép riêng của biến điạ phương . ta sẽ bắt đầu bằng việc
chạy đoạn mã cho giá trị interval là 100 :
ThreadPlayaround
Interval to display results at?> 100
Starting thread: Main Thread
Main Thread: Current Culture = en-GB
Main Thread: count has reached 100
Main Thread: count has reached 200
Main Thread: count has reached 300

Main Thread: count has reached 400
Main Thread: count has reached 500
Main Thread: count has reached 600
Main Thread: count has reached 700
Main Thread: count has reached 800
Main Thread Finished
Starting thread: Worker
Worker: Current Culture = en-GB
Worker: count has reached 100
Worker: count has reached 200
Worker: count has reached 300
Worker: count has reached 400
Worker: count has reached 500
Worker: count has reached 600
Worker: count has reached 700
Worker: count has reached 800
Worker Thread Finished
Qua kết quả ta thấy luồng chính bắt đầu đếm đến 800 và sau đó hoàn
thành.sau đó luồng worker bắt đầu và chạy riêng rẻ.
vấn đề ở đây là việc bắt đầu 1 luồng là 1 tiến trình lớn .sau khi khởi tạo 1
luồng mới ,luồng chính gặp dòng mã :
workerThread.Start();
Cái này sẽ gọi đến Thread.Call() thông báo cho window rằng luồng mới
được bắt đầu, sau đó trả về ngay lập tức. torng khi ta đang đếm đến 800, thì
window đang bận rộn trong việc sắp xếp cho các luồng được bắt đầu .vào
lúc luồn mới thực sự bắt đầu thì luồng chính vừa mới hoàn thành công việc
của nó.
Ta có thể giải quyết vấn đề này bằng cách chọn interval lớn hơn ,để cả hai
luồng chạy lâu hơn trong phương thức DisplayNumbes() , ta thử cho interval
là 1 triệu :


×