
Nó sẽ là:
p–; // chuyển đến địa chỉ trước đó (địa chỉ sẽ trừ đi size của kiểu mà p trỏ đến).
x = *p;
Bài toán đồng bộ
Đối với các chương trình single-thread, ta không cần quan tâm đến việc đồng bộ, bởi chương trình sẽ thực thi theo thứ tự từ trên xuống dưới từng câu lệnh một, trạng thái của chương trình sẽ chỉ bị thay đổi bởi chính nó. Tuy nhiên với các chương trình multi-thread, sẽ xảy ra trường hợp có nhiều hơn một thread cố gắng truy cập vào cùng một biến và gây ra sai sót.
Xét ví dụ sau khi tráo đổi giá trị hai biến x và y (swap):
x = 10;
y = 20;
t = x;
x = y; (*)
y = t;
Tại vị trí (*), giá trị của x và y bằng nhau, và bằng 20. Đây có thể coi là một trạng thái sai, bởi chỉ có thể x = 10, y = 20 hoặc x = 20, y = 10 sau khi tráo đổi xong. Trước hay sau khi tráo lại thì x * y cũng phải bằng 200, tuy nhiên tại vị trí (*), x * y sẽ là 400.Nếu có một thread thứ hai đọc giá trị của x và y để xử lý công việc, sẽ có một lúc nào đó giá trị nó đọc được sẽ bị sai.Cũng với ví dụ trên, giả sử có 2 thread cùng thực thi, bởi các câu lệnh có thể ngắt quãng và thực thi xen kẽ bởi các thead khác nhau, nên có thể xảy ra trường hợp sau:(thread 1 ký hiệu t1, thread 2 ký hiệu t2)
Continue reading “VẤN ĐỀ ĐỒNG BỘ TRUY CẬP DỮ LIỆU”Cách đây mấy hôm tôi có đưa ra một khảo sát xem mọi người sẽ theo hướng FP hay OOP, vì hay gặp cảnh tranh cãi giữa hai trường phái này. Và thường các cuộc tranh cãi đều không có hồi kết, đơn giản vì ai cũng có lý lẽ của mình, và quan trọng nhất là không có ai có thể đưa ra được những lý do xác đáng để bên kia tin theo.
Trong bài này tôi sẽ mặc nhiên coi các bạn đã hiểu về OOP, và sẽ nói một chút về FP (vì trong blog này chưa có bài nào nói về nó). Nếu như bạn chưa thực sự biết rõ cả OOP lẫn FP thì tất cả những lý lẽ đưa ra sẽ đều là cảm tính, do đó quan điểm đưa ra sẽ dễ trở nên phiến diện và không thuyết phục.
Nhớ tìm đọc lại các bài OOP nếu bạn vẫn chưa nắm rõ nhé
.
Vậy Functional Programming là gì?
Trong các ứng dụng single thread, ta chỉ có duy nhất một thread chạy trong một không gian địa chỉ (1), do vậy ta có thể đảm bảo không ai thay đổi dữ liệu trong suốt quá trình chạy. Tuy nhiên trong các ứng dụng multi-thread sẽ có 2 hoặc nhiều hơn thread chạy đồng thời và chia sẻ chung không gian địa chỉ, do vậy một trong những bài toán quan trọng nhất là đồng bộ dữ liệu dùng chung giữa các thread.
Nên lưu ý khái niệm chạy đồng thời ở đây là tương đối, vì một máy tính có thể chỉ có rất ít CPU, nên nó phải chia sẻ giữa nhiều thread khác nhau. Trong thực tế, số thread luôn lớn hơn số CPU có trong hệ thống, trong một thời điểm có vài trăm đến vài ngàn thread chạy đồng thời là điều bình thường, mỗi thread sẽ chỉ được cấp một khoảng thời gian để chạy (gọi là time slide), sau đó HĐH sẽ lấy lại quyền điều khiển và chuyển sang thread khác.
Continue reading “NÓI THÊM MỘT CHÚT VỀ THREAD”Khi mới học viết các ứng dụng multi thread, mỗi khi cần xử lý một công việc song song, ta thường làm theo cách sau:
– Tạo một thread mới.
– Truyền tham số khởi tạo và cho thread chạy để xử lý công việc.
– Kết thúc thread và nhận kết quả.
Một trong những bài toán phổ biến nhất cho người mới bắt đầu là viết ứng dụng chat(*). Và cách làm cũng tương tự như trên:
– Mở server socket và listen trên server socket đó.
– Mỗi khi có một client kết nối mới, bạn sẽ tạo một thread để chờ dữ liệu trên kết nối đó, nếu có thì xử lý, hoặc gửi dữ liệu cho đầu bên kia thông qua socket.
– Khi hai bên hoàn thành công việc, ngắt kết nối và kết thúc thread.
Cách làm này khá đơn giản, nhưng có một số nhược điểm như sau:
Vậy là ta đã xong OOP, con trỏ và các phần nói về quản lý bộ nhớ, giờ tiếp đến một chủ đề nữa: lập trình đa luồng – multi thread programming.Trong bài này tôi chỉ giới thiệu qua các khái niệm cơ bản và các từ chuyên ngành liên quan, để dễ nhất các bạn nên đọc lại những bài viết về chủ đề quản lý bộ nhớ (stack, heap) và bài về từ khóa virtual trong OOP.
Trước tiên ta cần hiểu thread là gì
Về kỹ thuật, một thread là một chuỗi các lệnh cần được thực thi bởi CPU, hay ta có thể tưởng tượng một thread là một function, trong đó chứa các lệnh thực thi, và quan trọng nhất – nó sẽ chạy ĐỒNG THỜI với chương trình chính.Bạn có thể thấy, chương trình sẽ bắt đầu bằng hàm main, Main, hay với nhiều ngôn ngữ là từ câu lệnh đầu tiên. Mỗi khi bạn gọi một function, bạn sẽ phải chờ nó kết thúc, giờ mỗi khi gọi một hàm, nó sẽ được thực thi bởi một CPU khác, vì ta có 2 CPU nên hàm chính và hàm được gọi sẽ chạy đồng thời với nhau.
Bạn sẽ đặt câu hỏi: Nếu máy của tôi chỉ có 1 CPU, vậy làm sao tôi có thể chạy đa luồng được?
Đó là nhiệm vụ của hệ điều hành, với các hệ điều hành đa nhiệm (multi tasking OS), nó sẽ có những cách sau để cho phép bạn chạy 2 thread cùng lúc.
Giải thích về ghi chú 2: Trong hình minh họa ta sẽ thấy Stack nằm lộn ngược từ trên xuống, đó cũng là cách bộ nhớ Stack được tổ chức trong thực tế, nhờ phát triển từ địa chỉ cao xuống địa chỉ thấp mà CPU có thể phát hiện ra lỗi Stack overflow một cách dễ dàng (so sánh với 0).
Continue reading “NÓI THÊM VỀ BÀI “BỘ NHỚ STACK LÀ GÌ?””Khi học về con trỏ, bạn sẽ thường xuyên phải xin cấp phát và giải phóng bộ nhớ, vậy những vùng nhớ này được quản lý bởi ai và như thế nào?
Về cơ bản, khi khởi động lên, hệ điều hành sẽ nắm quyền kiểm soát toàn bộ bộ nhớ, bởi bộ nhớ có thể coi như một sân chơi chung nên phải có phải có người cầm trịch, quản lý xem chỗ nào đã có người dùng, chỗ nào còn trống, tránh việc dữ liệu của một chương trình này bị ghi đè bởi một chương trình khác.Khi bạn gọi hàm malloc, hoặc new, trình quản lý bộ nhớ sẽ tìm xem chỗ nào chưa có ai sử dụng, nó lấy một phần vừa đủ với kích thước bạn xin cấp phát, đánh dấu vùng nhớ đó đã được cấp cho chương trình (process) của bạn và trả về địa chỉ. Nhiệm vụ của bạn là lưu lại địa chỉ (vào một biến con trỏ) và chỉ sử dụng trong phạm vi đã được cấp.Khi bạn yêu cầu delete, trình quản lý bộ nhớ sẽ gỡ bỏ đánh dấu. Lúc này nếu tiếp tục sử dụng, chương trình của bạn có thể bị lỗi truy cập vùng nhớ không được phép và bị kết thúc.
Tất cả các thao tác xin cấp phát và giải phóng này đều làm việc trên một vùng nhớ gọi là HEAP. Ta hiểu đơn giản heap là vùng nhớ còn lại sau khi đã trừ đi các phần khác của chương trình như code, dữ liệu tĩnh, stack…Việc cấp phát/sử dụng/giải phóng một vùng nhớ luôn phải làm cẩn thận vì:
Tính trừu tượng – mới nghe đã thấy mệt mỏi rồi
.
Như trong những bài viết trước, ta đã thấy trừu tượng là khái niệm mà chúng ta luôn cố gắng hiện thực khi xây dựng phần mềm.
Nói một cách đơn giản, trừu tượng cho phép ta làm việc với các thành phần khác mà không cần quan tâm chúng được xây dựng như thế nào, không cần biết bên trong một lớp người ta viết gì và chúng ta không cần phải quan tâm đằng sau chúng có những thành phần nào để chạy.
Trong OOP, với bài toán quản lý sinh viên, ta tạo ra một lớp cha StudentStore, trong đó có 2 phương thức Find and Save, dùng để tìm và lưu trữ thông tin về các sinh viên.Trong phiên bản đầu tiên, Find và Save sẽ đọc và lưu lại thông tin vào các file trên đĩa. Nhưng đến khi bạn học sang phần Sql Server, bạn muốn lưu trữ vào cơ sở dữ liệu thay cho file, vì tất nhiên làm việc với dữ liệu trên file sẽ cực nhọc và kém hiệu quả hơn nhiều. Lúc đó bạn sẽ làm gì?
Continue reading “TÍNH TRỪU TƯỢNG”Con trỏ chưa bao giờ là dễ dàng với người học, vì sự trừu tượng của nó, và vì có nhiều khái niệm liên quan đến bộ nhớ, địa chỉ… vốn xa lạ với người mới. Lời khuyên trước tiên là bạn hãy đọc qua bài này: https://daohainam.com/2021/08/13/cau-chuyen-ve-con-tro-pointer/
Sau khi đọc xong, ta sẽ điểm thêm vài điều có thể bạn chưa biết:
Trong ngôn ngữ máy không có khái niệm con trỏ:
Đúng vậy, con trỏ là một khái niệm của ngôn ngữ bậc cao. Trong ngôn ngữ máy hoặc assembly, bạn chỉ có khái niệm địa chỉ, vì đối với nó, địa chỉ cũng chỉ là một số nguyên nên chẳng có lý do gì lại phải có thêm một kiểu dữ liệu mới.Trong các ngôn ngữ bậc cao, người ta cần con trỏ để giúp xác định “kiểu của vùng nhớ mà con trỏ trỏ đến”.
Continue reading “CON TRỎ (nâng cao)”