- Bất đồng bộ và đồng thời là những khái niệm thường bị nhầm lẫn, nhưng chúng mang ý nghĩa khác nhau
- Bất đồng bộ là khả năng các tác vụ có thể được thực thi mà không phụ thuộc vào thứ tự
- Đồng thời là năng lực của hệ thống trong việc triển khai nhiều tác vụ cùng lúc
- Việc thiếu sự phân biệt rõ ràng giữa hai khái niệm trong hệ sinh thái ngôn ngữ và thư viện dẫn tới kém hiệu quả và phức tạp
- Trong ngôn ngữ Zig, việc tách biệt bất đồng bộ và đồng thời cho phép mã đồng bộ và bất đồng bộ cùng tồn tại mà không bị lặp mã
Mở đầu: cần phân biệt giữa bất đồng bộ và đồng thời
Nhờ bài nói chuyện nổi tiếng của Rob Pike, câu "đồng thời không phải là song song" đã trở nên quen thuộc, nhưng còn có một điểm thực tế quan trọng hơn. Đó chính là sự cần thiết của khái niệm "bất đồng bộ". Theo định nghĩa trên Wikipedia,
- Đồng thời: năng lực của hệ thống trong việc xử lý nhiều tác vụ cùng lúc bằng chia lát thời gian hoặc song song
- Tính toán song song: việc thực sự chạy nhiều tác vụ cùng lúc ở cấp độ vật lý
Ngoài ra, còn có một khái niệm quan trọng mà chúng ta đang bỏ sót, đó là "bất đồng bộ".
Ví dụ 1: lưu hai tệp
Khi lưu hai tệp (A, B) mà thứ tự không quan trọng,
io.async(saveFileA, .{io})
io.async(saveFileB, .{io})
- Lưu A trước hay B trước đều không sao, và ngay cả việc xen kẽ quá trình lưu giữa hai tệp cũng không có vấn đề
- Thậm chí nếu lưu xong toàn bộ tệp A rồi mới bắt đầu tệp B thì về mặt mã nguồn vẫn là đúng
Ví dụ 2: hai socket (server, client)
Khi cần tạo TCP server và kết nối client trong cùng một chương trình,
io.async(Server.accept, .{server, io})
io.async(Client.connect, .{client, io})
- Trong trường hợp này, hai tác vụ bắt buộc phải tiến hành chồng lấp lên nhau
- Nghĩa là trong lúc server đang chấp nhận kết nối thì client cũng phải thử kết nối
- Nếu xử lý tuần tự như ví dụ lưu tệp đầu tiên thì sẽ không cho ra hành vi như mong muốn
Tổng hợp khái niệm
Các khái niệm bất đồng bộ, đồng thời và song song được định nghĩa như sau
- Bất đồng bộ (asynchrony): tính chất mà trong đó các tác vụ vẫn cho ra kết quả đúng ngay cả khi được thực thi ngoài thứ tự
- Đồng thời (concurrency): năng lực triển khai nhiều tác vụ cùng lúc, dù là bằng thực thi song song hay chia lát
- Song song (parallelism): năng lực nhiều tác vụ thực sự được chạy đồng thời theo thời gian thực ở cấp độ vật lý
Hai ví dụ lưu tệp và kết nối socket đều là bất đồng bộ, nhưng ví dụ thứ hai (server-client) thì đồng thời là điều bắt buộc
Lợi ích thực tế của việc tách biệt bất đồng bộ và đồng thời
Nếu không phân biệt hai khái niệm này, sẽ phát sinh những vấn đề sau
- Tác giả thư viện phải viết hai phiên bản mã cho bất đồng bộ/đồng bộ (ví dụ: redis-py vs asyncio-redis)
- Người dùng gặp bất tiện khi mã bất đồng bộ có tính "lây lan": chỉ cần phụ thuộc vào một thư viện bất đồng bộ cũng có thể buộc cả dự án phải chuyển sang bất đồng bộ
- Để tránh điều đó, người ta thường tạo ra các cách lách vòng vo, và điều này thường gây ra *deadlock* cùng sự kém hiệu quả
Vì vậy, việc tách bạch rõ hai khái niệm mang lại lợi ích lớn cho cả tác giả thư viện lẫn người dùng
Zig: tách biệt bất đồng bộ và đồng thời
Ngôn ngữ Zig dùng io.async để sử dụng bất đồng bộ, nhưng điều đó không đảm bảo đồng thời
- Nghĩa là dù dùng
io.async, bên trong vẫn có thể chạy ở chế độ đơn luồng, blocking - Ví dụ
đoạn mã này trong môi trường blocking có thể hoạt động giống hệtio.async(saveFileA, .{io}) io.async(saveFileB, .{io})saveFileA(io) saveFileB(io) - Tức là ngay cả khi tác giả thư viện dùng
io.async, người dùng vẫn có được sự linh hoạt để chạy bằng IO blocking tuần tự nếu muốn
Cơ chế đưa đồng thời vào và chuyển đổi tác vụ (scheduling)
Trong các trường hợp cần đồng thời, để vận hành thực sự hiệu quả thì cần
- Dùng IO theo hướng sự kiện, không blocking (epoll, io_uring, v.v.)
- Cần primitive chuyển đổi tác vụ (switching) như
yield
- Ví dụ, Zig dùng kỹ thuật hoán đổi stack trong môi trường green thread để chuyển tác vụ
- Tương tự như lập lịch luồng ở cấp OS, nó lưu/khôi phục trạng thái như thanh ghi CPU và stack để chuyển đổi giữa nhiều task
- Phải có cơ chế chuyển đổi như vậy thì mã bất đồng bộ mới có thể được lập lịch đồng thời trong thực tế
- Các triển khai coroutine không stack (ví dụ: suspend, resume) cũng dựa trên cùng nguyên lý
Cùng tồn tại giữa mã đồng bộ và mã bất đồng bộ
Khi chạy hai lần saveData bằng io.async như sau,
io.async(saveData, .{io, "a", "b"})
io.async(saveData, .{io, "c", "d"})
- Vì hai tác vụ này mang tính bất đồng bộ với nhau, nên ngay cả khi hàm bên trong được viết theo kiểu đồng bộ, hệ thống vẫn có thể tự nhiên lập lịch chúng trong ngữ cảnh đồng thời
- Người dùng hay tác giả thư viện đều có thể dùng chung hàm đồng bộ/bất đồng bộ mà không cần lặp mã
Biểu đạt rõ các tình huống mà đồng thời là 'bắt buộc'
Với một số hàm cụ thể (ví dụ: accept của TCP server), cần thể hiện trong mã rằng khi chạy chúng thì đồng thời là điều bắt buộc
- Trong Zig, điều này được tách riêng bằng các hàm tường minh như
io.asyncConcurrent - Cách làm này sẽ phát sinh lỗi nếu môi trường thực thi không hỗ trợ đồng thời cho tác vụ đó
- Khác với
io.asyncdùng cho mục đích bất đồng bộ, việc đảm bảo đồng thời là bắt buộc nên nó được triển khai như một hàm có thể thất bại
Kết luận
- Bất đồng bộ và đồng thời là hai khái niệm hoàn toàn khác nhau và cần được phân biệt rõ
- Có thể để mã đồng bộ và mã bất đồng bộ cùng tồn tại
- Mô hình bất đồng bộ/đồng thời của Zig cho phép tận dụng cả hai thế giới mà không bị lặp mã
- Cấu trúc này cũng đã được áp dụng ở các ngôn ngữ khác như Go, đồng thời gợi mở cách vượt qua tính lây lan của async/await
- Với thiết kế async I/O mới của Zig, có thể kỳ vọng vào một môi trường lập trình đồng thời/bất đồng bộ trực quan hơn trong tương lai
1 bình luận
Ý kiến trên Hacker News
Thật sự rất khó để cảm thấy định nghĩa
asynclà rõ ràng; bản thân tôi cũng là một trong nhiều người từng tham gia thiết kếasynctrong JavaScript, và tôi không đồng ý với định nghĩa được nêu trong bài này. Chỉ vì làasynckhông có nghĩa là nó sẽ hoạt động đúng; ngay cả trong mãasync, nhiều dạng race condition ở mức người dùng vẫn có thể xảy ra, bất kể ngôn ngữ có hỗ trợasync/awaithay không. Định nghĩa gần đây tôi đưa ra làasynclà “mã được cấu trúc một cách tường minh để phục vụ concurrency”. Cách nhìn này cũng vẫn cần được gọt giũa thêm. Tôi cũng có một bài tự tổng hợp về chủ đề này: Quite a few words about asyncTôi nghĩ điều quan trọng là phải phân biệt giữa khái niệm trừu tượng
asynchronismvà cách triển khai thực tế; vế sau bao gồm cả trừu tượng ở cấp ngôn ngữ lẫn các cơ chế điều phối mang tính cơ học. Ở mức trừu tượng cao nhất, đối nghĩa củasynchronismchính làasynchronism. Khi nhiều tác nhân phải cùng hoạt động với nhau — chẳng hạn một tác vụ phải xong thì tác vụ khác mới tiếp tục — cốt lõi củaasynchronismlà thời điểm điều đó xảy ra không biết trước hoặc không được xác định. Bản thân định nghĩa này không khó. Vấn đề nằm ở gánh nặng nhận thức khi thiết kế kiểu trừu tượng đó ở cấp ngôn ngữ.Tôi không quá đào sâu chủ đề này, nhưng theo tôi, mã
asynclà thứ biến các tác vụ vốn chặn (blocking) thành không chặn (non-blocking), từ đó cho phép các tác vụ khác tiến triển đồng thời. Với tôi điều này đặc biệt hiển nhiên trong các vòng lặp nhúng, nơi mã bị chặn quá lâu có thể làm hỏng I/O và gây ra lỗi nhìn thấy được hoặc nghe thấy được.Tôi còn nghi ngờ cả chuyện có cần phải định nghĩa
asynchay không. Thực tế việc định nghĩa khó là vì không có gì khớp hoàn toàn vào một khái niệm duy nhất. Tôi cũng nghi ngờ việc có nhất thiết phải định nghĩaasynchayevent loopkhông. Ở tầng chip vật lý có thể thực hiện xử lý song song thật sự, chắc hẳn còn rất nhiều khái niệm mà tôi không biết. Với tôi, chỉ cần biết về “user finger” (chỉ các thao tác chạm của người dùng), “quickies” (các công việc chạy rất ngắn), job queue, và API blocking/non-blocking là đủ. Để đạt mục tiêu của mình thì API non-blocking là tốt nhất, vì các công việc tốn thời gian có thể giao cho hệ con bên dưới, còn tôi chỉ viết các “quicky” như lưu dữ liệu mình muốn, rồi định nghĩa thêm các quicky khác nhau cho trường hợp thành công/thất bại. Bản thân việc phân biệt sync và async không giúp ích nhiều. Tất nhiên tôi vẫn phải hiểu khái niệm đó khi người khác nói tới. Về bản chất, tôi xemasynclà API non-blocking. Mô hình lập trìnhasyncthực chất là viết các công việc blocking nhỏ và có tính nguyên tử (xét theo thời gian thực thi) để phản ứng với các sự kiện “hỗn loạn và không xác định”. Dù hệ thống bên trong làm gì, tôi tin rằng trình duyệt, OS và bản thân thiết bị đều cung cấp nhiều execution unit và một scheduler đủ tốt. Với tôiasynclà một khái niệm được định nghĩa mơ hồ; kể cả có định nghĩa được thì cũng chưa chắc hữu ích. Ngược lại, những khái niệm như event, tính blocking của công việc tôi viết, function closure, và việc điều gì sẽ được tách thành job khác khi dùng API lại thực dụng hơn nhiều. Ngay cả thuật ngữ “callback” ban đầu cũng khiến tôi rất rối; tôi từng nghĩ mã sẽ dừng lại ở đó, nhưng thực ra bạn phải hiểu thật chính xác sau khi đoạn đó chạy đến hết thì khi “callback” được gọi, đoạn mã nào sẽ chạy và có thể nhìn thấy thông tin gì. Thành thật mà nói, đây vừa là hỗn loạn vừa là một ý tưởng thiên tài. So với bản thân từ “async”, mô hình nền tảng — tức event, tác vụ blocking, job queue, API non-blocking — đơn giản hơn nhiều. Và hiểu rõ mình làm gì, còn trình duyệt/OS v.v. làm gì, cũng rất quan trọng. Ví dụ, trongcppbạn khai báo mô hình concurrent còn OS mới là bên thực thi thật sự; trong JS, bạn dùng API non-blocking để khai báo với trình duyệt hay Node rằng “có lẽ” cần concurrency, rồi chúng sẽ xử lý concurrent ở bên trong. Quan trọng nhất là giữ từng công việc ngắn (<50ms) và chỉ cần có thể biểu đạt ý định qua API non-blocking.cpphayrustbáo cho OS chạy các task một cách concurrent, nên kể cả về mặt vật lý chỉ có một thread thì UI vẫn giữ được độ phản hồi. Rốt cuộc việc của lập trình viênasynclà tạo ra một “mô hình UX đẹp” và ánh xạ event vào các quickies thật tốt.Có vẻ tác giả đã lấy “khái niệm yield” ra khỏi định nghĩa concurrency rồi đưa nó vào một thuật ngữ mới là “asynchrony”, đồng thời cho rằng nếu thiếu khái niệm này thì toàn bộ concurrency sẽ sụp đổ. Theo tôi, yield vốn dĩ đã là yếu tố thiết yếu của concurrency nên nó là khái niệm nội tại của chính concurrency. Đây là một khái niệm quan trọng, nhưng tách nó ra thành một thuật ngữ mới chỉ làm tăng thêm nhầm lẫn.
Tôi cho rằng song song 1:1 là một dạng concurrency không có yield. Ngoài trường hợp đó, mọi dạng concurrency không song song đều phải yield việc thực thi theo một nhịp nào đó, kể cả ở mức lệnh. Ví dụ, trong CUDA, các thread rẽ nhánh trong cùng một warp sẽ xen kẽ thực thi lệnh của nhau, nên một nhánh có thể chặn nhánh còn lại.
Tôi muốn nhấn mạnh rằng bài được trích dẫn thực ra còn nêu rõ rằng “yield là một khái niệm của concurrency”.
Concurrency không nhất thiết phải đồng nghĩa với yield. Logic đồng bộ cần sự đồng bộ hóa rõ ràng, còn yield chỉ là một phương tiện đồng bộ hóa. Cái tôi gọi là logic bất đồng bộ là concurrency hoạt động mà không cần đồng bộ hóa hay yield. Từ góc nhìn thực tế, concurrency hay logic bất đồng bộ đều không tồn tại trọn vẹn trên máy Von Neumann.
Trong ngữ cảnh này,
asynclà một lớp trừu tượng tách biệt việc chuẩn bị/gửi request và việc thu thập kết quả. Nó cho phép gửi nhiều request rồi chỉ sau đó mới kiểm tra kết quả. Điều này cho phép có triển khai concurrent nhưng không bắt buộc. Dù vậy, mục đích của lớp trừu tượng này là để có concurrency; nếu không có concurrency thì lợi ích cần đạt được cũng biến mất. Một số trừu tượngasyncthậm chí không thể triển khai nếu không có mức concurrency tối thiểu. Ví dụ, mô hình callback có thể giả lập trên single-thread, nhưng sẽ có giới hạn như deadlock khi đang giữ mộtmutexkhông đệ quy. Nói cách khác, trừu tượngasynckhông có concurrency cuối cùng sẽ thất bại. Người gọi gửi request trong lúc đang giữmutex, và nếu callback chạy trước khi unlock thì thao tác unlock có thể chẳng bao giờ được thực thi. Ít nhất phải có một thread tách biệt để người gọi có thể đi tới bước unlock.async, bạn luôn phải đảm bảo có concurrency.“Cooperative multitasking không phải là preemptive”. Thuật ngữ “async” thường được dùng để chỉ “single-thread, cooperative multitasking (yield tường minh) và dựa trên event”, trong đó các phép toán bên ngoài chạy concurrent và báo cáo kết quả thông qua event. Trong mô hình multi-thread hoặc thực thi concurrent,
asynckhông còn mang nhiều ý nghĩa; dù thread đó có bị block thì chương trình vẫn tiếp tục, nên yield point cũng không nhất thiết phải tường minh.async. OS thread phù hợp với tác vụ CPU-bound, cònasyncphù hợp với tác vụ IO-bound. Ưu điểm lớn nhất củaasynchoặc kiểu lập lịch M:N như Go là nếu còn đủ bộ nhớ thì có thể tăng số lượng task/goroutine khá tự do. Còn với OS thread, do chi phí context switch, cạn thread/bộ nhớ v.v., chỉ cần vượt khỏi bài toán IO-bound là cũng có thể vấp phải deadlock.Ý tưởng IO mới của Zig có vẻ mới mẻ cho phát triển ứng dụng thông thường; nó tối ưu cho những ai không cần stackless coroutine. Nhưng tôi nghĩ khi viết thư viện thì lỗi sẽ dễ phát sinh hơn. Tác giả thư viện khó biết được IO được cấp cho mình là single-thread hay multi-thread, là IO dựa trên event hay không. Mã liên quan đến concurrency/async/parallel vốn đã khó viết ngay cả khi biết toàn bộ IO stack, nên trong cấu trúc mà IO được cấp từ bên ngoài thì độ khó còn tăng gấp bội. Nếu giao diện IO trở nên đồ sộ như một “OS thu nhỏ” thì số lượng kịch bản cần test cũng bùng nổ. Tôi không chắc chỉ với các primitive
asyncmà giao diện cung cấp thì có xử lý được mọi edge case thật sự hay không. Muốn hỗ trợ nhiều cách triển khai IO khác nhau thì mã sẽ phải rất “phòng thủ” và luôn giả định IO có mức song song hóa cao nhất. Đặc biệt, tôi đoán việc trộn stackless coroutine với cách này sẽ không dễ. Muốn giảm việc spawn coroutine không cần thiết thì phải có explicit polling cho coroutine, mà đa số lập trình viên có lẽ sẽ không tự viết kiểu mã như vậy. Cuối cùng tôi nghĩ nó sẽ quay về cấu trúc tương tự mãasync/awaitthông thường. Tính thêm cả dynamic dispatch và xu hướng thiết kế bottom-up của Zig thì rốt cuộc có vẻ nó sẽ trở thành một ngôn ngữ khá high-level. Chưa có trường hợp áp dụng thực tế nào, nên gọi đây là cách tiếp cận “không thỏa hiệp” thì hơi mạnh tay; phải sau vài năm sử dụng mới có thể đánh giá đúng.Stackless coroutine dù sao cũng đã nằm trong kế hoạch hỗ trợ; vì cần cho target WASM nên chắc chắn sẽ có. Dynamic dispatch chỉ được dùng khi có từ hai triển khai IO trở lên; nếu chỉ dùng một thì sẽ được thay bằng direct call. Vì chưa được kiểm chứng ngoài thực tế nên tôi cũng nghĩ nói “không thỏa hiệp” lúc này là hơi sớm. Tôi có nghe nói ngôn ngữ Jai dùng thành công một mô hình tương tự — khác ở chỗ dùng implicit IO context thay vì truyền context tường minh — nhưng cũng khó mà coi đó là thứ đã thật sự được kiểm chứng ngoài hiện trường.
Tôi đồng ý với ý rằng để hỗ trợ cả thực thi đồng bộ lẫn bất đồng bộ thì mã phải luôn giả định IO có mức song song hóa cao nhất. Tuy vậy, nếu ở tầng thấp của trình xử lý event IO, phần bất đồng bộ đã được triển khai tốt, thì chỉ cần áp dụng cùng một nguyên tắc ở mọi nơi. Trong trường hợp xấu nhất, mã chỉ đơn giản chạy tuần tự hơn (và chậm hơn), chứ sẽ không rơi vào race/deadlock.
Tôi thấy ý tưởng của Zig rất hay ở chỗ không cần dùng hai thư viện riêng. Dù vậy, việc test mã bất đồng bộ lúc nào cũng làm tôi lo. Tôi không biết làm sao có thể chắc rằng bài test hôm nay chạy qua được thì đã tái hiện hết mọi kịch bản/thứ tự có thể xảy ra trong thực tế hay chưa. Chương trình dùng thread cũng có cùng vấn đề, nhưng mã multi-thread luôn khó viết và debug hơn. Tôi thường tránh dùng thread nếu có thể. Vấn đề thật sự là ‘làm sao để lập trình viên hiểu chính xác môi trường async/thread’. Gần đây tôi làm việc với một team từng dùng nửa JS nửa Python trong một hệ thống Python, rồi họ chuyển cả đống mã sang
async,threaded; nhưng họ thậm chí còn không biếtGlobal Interpreter Lock (GIL)là gì. Những gì tôi nói chắc chỉ giống như càm ràm. Chưa kể test của họ lúc nào cũng pass, kể cả khi bạn phá mã thật sự.mangumép các tác vụ background vàasyncphải hoàn tất khi HTTP request kết thúc, nhưng họ lại không biết điều đó. Dù bạn có nói cho họ biết thì mọi người vẫn khá thờ ơ. Không chỉ chuyện biết là quan trọng; quan trọng hơn là người khác có chịu xem trọng điều đó hay không.Trong Zig có kế hoạch đưa vào một triển khai
Iodành cho test. Nhờ đó có thể làm fuzz test và các dạng stress test dưới mô hình thực thi song song. Nhưng điểm cốt lõi là phần lớn mã thư viện có lẽ sẽ không cần gọi trực tiếpio.asynchayio.asyncConcurrent. Ví dụ, đa số thư viện cơ sở dữ liệu chỉ cần mã đồng bộ thuần túy là đủ; lập trình viên ứng dụng có thể dễ dàng biến nó thành bất đồng bộ bằng kiểuio.async(writeToDb),io.async(doOtherThing). Cách này ít dễ lỗi hơn và dễ hiểu hơn nhiều so với việc rắcasync/awaitkhắp cả codebase.Đồng cảm. Việc kiểm thử mọi interleaving trong mã bất đồng bộ và đa luồng vốn nổi tiếng là cực khó; kể cả dùng fuzzer hay framework test concurrency thì vẫn khó có thể tự tin nếu chưa có bài học từ production. Trong hệ thống phân tán thì chuyện này còn tệ hơn. Ví dụ, khi thiết kế hạ tầng webhook, không chỉ có
asynctrong mã của chính bạn mà còn có retry mạng, timeout, partial failure và đủ loại sự cố bên ngoài khác chồng lên nhau. Trong môi trường concurrency cao, retry,deduplication, bảo đảmidempotencyv.v. tự chúng đã trở thành các vấn đề kỹ thuật. Đó là lý do người ta cần đến những dịch vụ chuyên biệt như Vartiq.com (tôi làm ở đó). Các dịch vụ kiểu này có thể trừu tượng hóa một phần độ phức tạp concurrency trong vận hành để giảm blast radius, nhưng vấn đề testasynctrong mã của tôi thì vẫn còn nguyên. Tóm lại,async, threading và concurrency phân tán khuếch đại rủi ro của nhau, nên giao tiếp và thiết kế hệ thống quan trọng hơn bất kỳ cú pháp hay thư viện nào.Tôi cho rằng tác giả đang có sự nhầm lẫn trong cách định nghĩa concurrency. Có thể tham khảo bài báo của Lamport.
Làm ơn đừng chỉ để lại link bài báo mà hãy giải thích thêm. Theo tôi thì bản thân định nghĩa cũng ổn mà. Chẳng hạn: bất đồng bộ là khi các tác vụ không cần chạy theo thứ tự mà vẫn đúng; concurrency là đặc tính của hệ thống cho phép nhiều tác vụ tiến triển đồng thời, dù là bằng song song thật sự hay chuyển đổi task; còn parallelism là khi từ hai tác vụ trở lên thực sự chạy cùng lúc ở mức vật lý.
Vì lý do này mà tôi bỏ hẳn không dùng các thuật ngữ đó nữa. Nói chuyện với ai cũng thấy mỗi người hiểu một kiểu, đến mức bản thân thuật ngữ không còn ý nghĩa giao tiếp gì nữa.
Tác giả cũng biết rằng đã có các định nghĩa sẵn có cho thuật ngữ đó trong bài blog. Anh ấy chỉ đang tự đề xuất một định nghĩa mới, và nếu định nghĩa đó nhất quán thì như vậy là đủ. Khác biệt chỉ nằm ở chỗ độc giả có chấp nhận hay không.
Một nửa nội dung trong bài của Lamport về mặt khái niệm không thể biểu đạt được trong hầu hết ngôn ngữ. Chỉ vì tạo thread không có nghĩa là bạn sẽ đi bàn về thứ tự toàn phần hay thứ tự bộ phận. Chỉ khi thiết kế giao thức bằng TLA+ thì mấy thảo luận như vậy mới cần thiết. Trong API
asynccủa Zig, việc một hàm “chỉ hoạt động trong môi trường thực thi bất đồng bộ” gây lỗi biên dịch không nhất thiết phải được gọi là một lý thuyết mới.Một cách hay để đánh giá xem thuật ngữ “asynchrony” có thực sự cần thiết hay không là xem nó có hữu ích vượt ra ngoài một ngôn ngữ/mô hình duy nhất, tức có dùng được trong nhiều mô hình concurrency khác nhau hay không. Ví dụ, nếu nó cần thiết chung cho Haskell, Erlang, OCaml, Scheme, Rust, Go v.v. thì giá trị của nó sẽ cao hơn. Nói chung, khi dùng lịch cooperative thì cả hệ thống phải để tâm nhiều hơn đến chuyện chỉ một đoạn mã có thể làm toàn bộ hệ thống lockup hay gây trễ; còn với lịch preemptive thì rất nhiều vấn đề như vậy biến mất. Vì không thể xảy ra chuyện toàn hệ thống bị lockup, số nhóm vấn đề cần lo giảm đi rất nhiều.
Asynchronytrong trường hợp này là một từ không phù hợp; đã có một thuật ngữ toán học được định nghĩa tốt làcommutativity. Có những phép toán mà thứ tự không quan trọng (cộng, nhân v.v.), và cũng có những phép toán mà thứ tự quan trọng (trừ, chia v.v.). Thông thường thứ tự phép toán trong mã được thể hiện bằng số dòng (từ trên xuống dưới), nhưng trong mãasyncthì thứ tự này bị phá vỡ. Vì vậy kiểuasyncConcurrent(...)được viết như thế thì đương nhiên sẽ khá rối; nếu chưa tiêu hóa hoàn toàn nội dung bài blog thì rất khó biết nó có nghĩa gì. Zig (và cả Rust mà tôi cũng thích) dường như hay đưa ra những cách tiếp cận “hipster” như vậy. Hoặc là triển khai hệ thống tính giao hoán/thứ tự mang tính thủ tục (dựa trênasync) kiểu như lifetime của Rust, hoặc là cứ dùng những thứ mọi người đã quen thuộc.Tôi không đồng ý với ý “
asyncConcurrent(...)gây bối rối”. Nếu đã nội tại hóa ý chính của bài blog thì nó không hề gây rối. Việc ý tưởng đó có đáng để học hay không lại là chuyện khác. Trên thực tế, sẽ có nhiều người thực hành với nó, và theo thời gian ta sẽ biết ngoài hiện trường ý tưởng này có tốt hay không. Còn việc thaycommutativitybằng từ khác, trong Zig lại càng dễ gây nhầm hơn, vì ở đó thực sự có các toán tử mang tính giao hoán. Vớif() + g(), do phép cộng có tính giao hoán, liệu Zig có được phép chạy song song hai hàm này hay không — kiểu nhầm lẫn đó rất dễ xảy ra. Trật tự thực thi và tính giao hoán là hai chuyện hoàn toàn khác nhau, nên cần tách bạch.Nói một cách chặt chẽ thì
commutativitylà thuộc tính áp dụng cho các phép toán (nhị phân). Khi nói hai câu lệnhasyncnhưconnect/acceptlà hoán đổi được, thì câu hỏi là “đối với phép toán nào?”. Hiện tại, toán tửbind(>>=) hoặc.then(...)có lẽ là thứ gần nhất với vai trò đó, nhưng lúc này vẫn còn khá trực giác.Bất đồng bộ còn cho phép cả thứ tự bộ phận. Dù hai phép toán phải được retire theo cùng một thứ tự, điều đó vẫn độc lập với thứ tự thực thi thực tế. Ví dụ, phép trừ không có tính giao hoán, nhưng bạn vẫn có thể chạy song song hai truy vấn để tính số dư và tính khoản cần trừ, rồi áp dụng kết quả theo đúng thứ tự thích hợp.
Chỉ vì có thuật ngữ khác bao trùm khái niệm này không có nghĩa nó là từ tốt hơn
asynchrony. Từcommutativityvừa khó đọc, vừa khó nghe, vừa khó viết;asynchronyquen thuộc hơn nhiều.Lập luận dựa trên tính giao hoán cũng có giới hạn. Nếu A và B đều giao hoán với C thì
ABC = CAB, nhưng không thể nói chắc điều đó cũng đồng nghĩa vớiACB. Trong bất đồng bộ thìABC = ACB = CABđều phải tương đương nhau (nếu có thuật ngữ toán học sẵn cho chuyện này thì tôi không biết).Là một lập trình viên mạng, tôi đã viết cực nhiều mã concurrent, parallel và async, nhưng bài này khiến tôi thấy hơi rối, như thể đang cố tìm lời giải trên một tầng trừu tượng đầy lỗ hổng. Nếu tool hay cách triển khai tự nó đã sai, thì vấn đề là nó có thể “vỡ” dễ dàng đến thế. Thật ra debug mã đa luồng cũng khá vui; nhìn người khác quá sợ những con quái vật multi-thread đôi khi lại thấy thú vị.