Tại sao ngôn ngữ lập trình không tự động quản lý sự cố đồng bộ / không đồng bộ?


27

Tôi chưa tìm thấy nhiều tài nguyên về điều này: Tôi đã tự hỏi liệu có thể / một ý tưởng tốt để có thể viết mã không đồng bộ theo cách đồng bộ.

Ví dụ: đây là một số mã JavaScript lấy số lượng người dùng được lưu trữ trong cơ sở dữ liệu (một hoạt động không đồng bộ):

getNbOfUsers(function (nbOfUsers) { console.log(nbOfUsers) });

Thật tuyệt khi có thể viết một cái gì đó như thế này:

const nbOfUsers = getNbOfUsers();
console.log(getNbOfUsers);

Và do đó, trình biên dịch sẽ tự động chăm sóc chờ phản hồi và sau đó thực thi console.log. Nó sẽ luôn chờ các hoạt động không đồng bộ hoàn thành trước khi kết quả phải được sử dụng ở bất kỳ nơi nào khác. Chúng tôi sẽ sử dụng ít hơn các lời hứa gọi lại, async / await hoặc bất cứ điều gì, và sẽ không bao giờ phải lo lắng liệu kết quả của một hoạt động có sẵn ngay lập tức hay không.

Lỗi vẫn có thể quản lý được (đã nbOfUserslấy số nguyên hay lỗi?) Bằng cách sử dụng thử / bắt hoặc một cái gì đó giống như tùy chọn như trong ngôn ngữ Swift .

Có thể không? Nó có thể là một ý tưởng khủng khiếp / một điều không tưởng ... tôi không biết.


58
Tôi không thực sự hiểu câu hỏi của bạn. Nếu bạn "luôn chờ đợi hoạt động không đồng bộ", thì đó không phải là hoạt động không đồng bộ, đó là hoạt động đồng bộ. Bạn có thể làm rõ? Có thể đưa ra một đặc điểm kỹ thuật của loại hành vi bạn đang tìm kiếm? Ngoài ra, "bạn nghĩ gì về nó" không có chủ đề về Kỹ thuật phần mềm . Bạn cần đặt câu hỏi của bạn trong bối cảnh của một vấn đề cụ thể, có một câu trả lời duy nhất, rõ ràng, chính tắc, khách quan.
Jörg W Mittag

4
@ JörgWMittag Tôi tưởng tượng một giả thuyết C # mà ngầm awaitsa Task<T>để chuyển nó sangT
Caleth

6
Những gì bạn đề xuất là không thể làm được. Nó không phụ thuộc vào trình biên dịch để quyết định xem bạn muốn chờ kết quả hay có thể là cháy và quên. Hoặc chạy trong nền và chờ đợi sau. Tại sao lại giới hạn bản thân như thế?
kỳ quái

5
Vâng, đó là một ý tưởng khủng khiếp. Chỉ cần sử dụng async/ awaitthay vào đó, làm cho các phần không đồng bộ của thực thi rõ ràng.
Bergi

5
Khi bạn nói rằng hai điều xảy ra đồng thời, bạn đang nói rằng mọi việc xảy ra theo thứ tự nào cũng được. Nếu mã của bạn không có cách nào để làm rõ thứ tự sắp xếp lại sẽ không phá vỡ kỳ vọng của mã, thì nó không thể làm cho chúng đồng thời.
Cướp

Câu trả lời:


65

Async / await chính xác là quản lý tự động mà bạn đề xuất, mặc dù có thêm hai từ khóa. Tại sao chúng lại quan trọng? Ngoài khả năng tương thích ngược?

  • Nếu không có các điểm rõ ràng trong đó một coroutine có thể bị đình chỉ và tiếp tục, chúng ta sẽ cần một hệ thống loại để phát hiện nơi nào phải chờ đợi một giá trị chờ đợi. Nhiều ngôn ngữ lập trình không có hệ thống kiểu như vậy.

  • Bằng cách chờ đợi một giá trị rõ ràng, chúng ta cũng có thể vượt qua các giá trị đang chờ xung quanh như các đối tượng hạng nhất: lời hứa. Điều này có thể siêu hữu ích khi viết mã thứ tự cao hơn.

  • Mã Async có hiệu ứng rất sâu cho mô hình thực thi ngôn ngữ, tương tự như sự vắng mặt hoặc hiện diện của các ngoại lệ trong ngôn ngữ. Cụ thể, chức năng async chỉ có thể được chờ đợi bởi các chức năng async. Điều này ảnh hưởng đến tất cả các chức năng gọi! Nhưng điều gì sẽ xảy ra nếu chúng ta thay đổi một chức năng từ không đồng bộ thành không đồng bộ ở cuối chuỗi phụ thuộc này? Đây sẽ là một thay đổi không tương thích ngược, trừ khi tất cả các chức năng không đồng bộ và mọi cuộc gọi chức năng đều được chờ theo mặc định.

    Và đó là điều không mong muốn vì nó có ý nghĩa về hiệu suất rất tệ. Bạn sẽ không thể trả lại giá trị rẻ. Mỗi cuộc gọi chức năng sẽ trở nên đắt hơn rất nhiều.

Async là tuyệt vời, nhưng một số loại async ngầm sẽ không hoạt động trong thực tế.

Các ngôn ngữ chức năng thuần túy như Haskell có một chút thoát vì lệnh thực thi phần lớn không được xác định và không quan sát được. Hoặc phrased khác nhau: bất kỳ thứ tự cụ thể của hoạt động phải được mã hóa rõ ràng. Điều đó có thể khá cồng kềnh đối với các chương trình trong thế giới thực, đặc biệt là các chương trình I / O-heavy mà mã async phù hợp rất tốt.


2
Bạn không nhất thiết cần một hệ thống loại. Tương lai minh bạch trong ví dụ ECMAScript, Smalltalk, Self, Drameak, Io, Ioke, Seph, có thể dễ dàng thực hiện mà không cần hệ thống tyoe hoặc hỗ trợ ngôn ngữ. Trong Smalltalk và hậu duệ của nó, một đối tượng có thể thay đổi trong suốt danh tính của nó, trong ECMAScript, nó có thể thay đổi hình dạng trong suốt. Đó là tất cả những gì bạn cần để làm cho Futures minh bạch, không cần hỗ trợ ngôn ngữ cho sự không đồng bộ.
Jörg W Mittag

6
@ JörgWMittag Tôi hiểu những gì bạn đang nói và làm thế nào điều đó có thể hoạt động, nhưng tương lai minh bạch mà không có hệ thống loại khiến việc đồng thời có tương lai hạng nhất khá khó khăn phải không? Tôi sẽ cần một số cách để chọn xem tôi muốn gửi tin nhắn đến tương lai hay giá trị của tương lai, tốt nhất là một cái gì đó tốt hơn someValue ifItIsAFuture [self| self messageIWantToSend]vì đó là khó khăn để tích hợp với mã chung.
amon

8
@amon "Tôi có thể viết mã async của mình như lời hứa và lời hứa là đơn nguyên." Monads không thực sự cần thiết ở đây. Thunks về cơ bản chỉ là lời hứa. Vì hầu hết tất cả các giá trị trong Haskell đều được đóng hộp, nên hầu như tất cả các giá trị trong Haskell đều đã được hứa hẹn. Đó là lý do tại sao bạn có thể ném parkhá nhiều ở bất cứ đâu trong mã Haskell thuần túy và nhận được paralellism miễn phí.
DarthFennec

2
Async / await nhắc nhở tôi về đơn vị tiếp tục.
les

3
Trong thực tế, cả ngoại lệ và async / await là các trường hợp của hiệu ứng đại số .
Alex Reinking

21

Những gì bạn đang thiếu, là mục đích của các hoạt động không đồng bộ: Chúng cho phép bạn sử dụng thời gian chờ đợi của mình!

Nếu bạn biến một hoạt động không đồng bộ, như yêu cầu một số tài nguyên từ máy chủ, thành một hoạt động đồng bộ bằng cách ngầm và chờ trả lời, thì luồng của bạn không thể làm gì khác với thời gian chờ . Nếu máy chủ mất 10 mili giây để phản hồi, sẽ có khoảng 30 triệu chu kỳ CPU bị lãng phí. Độ trễ của phản hồi trở thành thời gian thực hiện cho yêu cầu.

Lý do duy nhất tại sao các lập trình viên phát minh ra các hoạt động không đồng bộ, là để che giấu độ trễ của các tác vụ chạy dài vốn có đằng sau các tính toán hữu ích khác . Nếu bạn có thể lấp đầy thời gian chờ đợi bằng công việc hữu ích, đó là thời gian CPU được lưu. Nếu bạn không thể, tốt, không có gì bị mất bởi hoạt động không đồng bộ.

Vì vậy, tôi khuyên bạn nên nắm lấy các hoạt động không đồng bộ mà ngôn ngữ của bạn cung cấp cho bạn. Họ ở đó để giúp bạn tiết kiệm thời gian.


tôi đã nghĩ đến một ngôn ngữ chức năng nơi các hoạt động không bị chặn, vì vậy ngay cả khi nó có cú pháp đồng bộ, một tính toán chạy dài sẽ không chặn luồng
Cinn

6
@Cinn Tôi không tìm thấy điều đó trong câu hỏi và ví dụ trong câu hỏi là Javascript, không có tính năng này. Tuy nhiên, nhìn chung, trình biên dịch khá khó để tìm ra các cơ hội có ý nghĩa cho việc song song hóa như bạn mô tả: Việc khai thác một cách có ý nghĩa tính năng như vậy sẽ đòi hỏi người lập trình phải suy nghĩ rõ ràng về những gì họ đặt ngay sau một cuộc gọi có độ trễ dài. Nếu bạn thực hiện thời gian chạy đủ thông minh để tránh yêu cầu này đối với lập trình viên, thì thời gian chạy của bạn có thể sẽ tiết kiệm được hiệu suất tiết kiệm vì nó sẽ cần song song mạnh mẽ giữa các lệnh gọi hàm.
cmaster

2
Tất cả các máy tính chờ ở cùng một tốc độ.
Bob Jarvis - Tái lập Monica

2
@BobJarvis Có. Nhưng họ khác nhau về số lượng công việc họ có thể đã làm trong thời gian chờ đợi ...
cmaster

13

Một số làm.

Chúng không phải là chính (vì) async là một tính năng tương đối mới mà bây giờ chúng tôi mới cảm thấy tốt nếu nó thậm chí là một tính năng tốt hoặc cách trình bày nó với các lập trình viên theo cách thân thiện / có thể sử dụng / biểu cảm / v.v. Các tính năng không đồng bộ hiện có phần lớn được áp dụng cho các ngôn ngữ hiện có, đòi hỏi một cách tiếp cận thiết kế khác nhau một chút.

Điều đó nói rằng, rõ ràng không phải một ý tưởng tốt để làm ở khắp mọi nơi. Một lỗi phổ biến là thực hiện các cuộc gọi không đồng bộ trong một vòng lặp, tuần tự hóa hiệu quả việc thực hiện chúng. Có các cuộc gọi không đồng bộ được ẩn có thể che khuất loại lỗi đó. Ngoài ra, nếu bạn hỗ trợ cưỡng chế ngầm từ Task<T>(hoặc tương đương với ngôn ngữ của bạn) T, điều đó có thể thêm một chút phức tạp / chi phí cho trình đánh máy và báo cáo lỗi khi không rõ hai người lập trình thực sự muốn.

Nhưng đó không phải là vấn đề không thể vượt qua. Nếu bạn muốn hỗ trợ hành vi đó, bạn gần như chắc chắn có thể, mặc dù sẽ có sự đánh đổi.


1
Tôi nghĩ một ý tưởng có thể là bọc mọi thứ trong các chức năng không đồng bộ, các tác vụ đồng bộ sẽ giải quyết ngay lập tức và chúng tôi có tất cả một loại để xử lý (Chỉnh sửa: @amon giải thích lý do tại sao đó là một ý tưởng tồi ...)
Cinn

8
Bạn có thể cho một vài ví dụ cho " Một số làm " được không?
Bergi

2
Lập trình không đồng bộ không theo bất kỳ cách nào mới, chỉ là ngày nay mọi người phải đối phó với nó thường xuyên hơn.
Khối

1
@Cubic - đó là một tính năng ngôn ngữ theo như tôi biết. Trước đó chỉ là (vụng về) chức năng người dùng.
Telastyn

12

Có những ngôn ngữ làm điều này. Nhưng, thực sự không có nhiều nhu cầu, vì nó có thể dễ dàng thực hiện với các tính năng ngôn ngữ hiện có.

Miễn là bạn có một số cách thể hiện sự không đồng bộ, bạn có thể triển khai Tương lai hoặc Lời hứa hoàn toàn như một tính năng của thư viện, bạn không cần bất kỳ tính năng ngôn ngữ đặc biệt nào. Và miễn là bạn có một số biểu hiện trong suốt , bạn có thể kết hợp hai tính năng này với nhau và bạn có Tương lai trong suốt .

Ví dụ, trong Smalltalk và hậu duệ của nó, một đối tượng có thể thay đổi danh tính, nó có thể "trở thành" một đối tượng khác theo nghĩa đen (và trên thực tế, phương thức thực hiện điều này được gọi là Object>>become:).

Hãy tưởng tượng một tính toán chạy dài trả về a Future<Int>. Điều này Future<Int>có tất cả các phương pháp giống như Int, ngoại trừ với các triển khai khác nhau. Future<Int>'s +phương pháp không thêm một số khác và trả kết quả, nó sẽ trả về một mới Future<Int>mà kết thúc tốt đẹp các tính toán. Vân vân và vân vân. Các phương thức không thể được thực hiện một cách hợp lý bằng cách trả về a Future<Int>, thay vào đó sẽ tự động awaitkết quả và sau đó gọi self become: result., điều này sẽ làm cho đối tượng hiện đang thực thi ( selfnghĩa là Future<Int>) trở thành resultđối tượng, tức là từ bây giờ trên tham chiếu đối tượng được sử dụng Future<Int>là bây giờ Intở khắp mọi nơi, hoàn toàn minh bạch cho khách hàng.

Không có tính năng ngôn ngữ liên quan đến không đồng bộ đặc biệt cần thiết.


Ok, nhưng điều đó có vấn đề nếu cả hai Future<T>Tchia sẻ một số giao diện chung và tôi sử dụng chức năng từ giao diện đó. Có nên becomekết quả và sau đó sử dụng các chức năng, hay không? Tôi đang nghĩ về những thứ như một toán tử đẳng thức hoặc một đại diện gỡ lỗi chuỗi.
amon

Tôi hiểu rằng nó không thêm bất kỳ tính năng nào, điều chúng ta có các cú pháp khác nhau để viết ngay lập tức giải quyết các tính toán và tính toán dài hạn, và sau đó chúng ta sẽ sử dụng kết quả theo cách tương tự cho các mục đích khác. Tôi đã tự hỏi nếu chúng ta có thể có một cú pháp xử lý minh bạch cả hai, làm cho nó dễ đọc hơn và vì vậy lập trình viên không phải xử lý nó. Giống như làm a + b, cả hai số nguyên, không có vấn đề gì nếu a và b có sẵn ngay lập tức hoặc sau đó, chúng tôi chỉ cần viết a + b(có thể làm được Int + Future<Int>)
Cinn

@Cinn: Có, bạn có thể làm điều đó với Tương lai minh bạch và bạn không cần bất kỳ tính năng ngôn ngữ đặc biệt nào để làm điều đó. Bạn có thể triển khai nó bằng cách sử dụng các tính năng đã có sẵn, ví dụ như Smalltalk, Self, Drameak, Us, Korz, Io, Ioke, Seph, ECMAScript và rõ ràng, như tôi vừa đọc, Python.
Jörg W Mittag

3
@amon: Ý tưởng của Tương lai minh bạch là bạn không biết đó là tương lai. Từ quan điểm của bạn, không có giao diện chung giữa Future<T>Tbởi vì theo quan điểm của bạn, không cóFuture<T> , chỉ có a T. Bây giờ, tất nhiên có rất nhiều thách thức kỹ thuật xung quanh làm thế nào để làm cho hiệu quả này, hoạt động nào nên được chặn so với không chặn, v.v., nhưng điều đó thực sự độc lập với việc bạn làm nó như một ngôn ngữ hay như một tính năng của thư viện. Tính minh bạch là một yêu cầu được quy định bởi OP trong câu hỏi, tôi sẽ không cho rằng nó khó và có thể không có ý nghĩa.
Jörg W Mittag

3
@ Jörg Điều đó có vẻ như sẽ có vấn đề trong bất cứ điều gì ngoại trừ các ngôn ngữ chức năng vì bạn không có cách nào để biết khi nào mã thực sự được thực thi trong mô hình đó. Điều đó thường hoạt động tốt khi nói Haskell, nhưng tôi không thể thấy nó hoạt động như thế nào trong các ngôn ngữ thủ tục hơn (và thậm chí trong Haskell, nếu bạn quan tâm đến hiệu suất, đôi khi bạn phải thực thi và hiểu mô hình cơ bản). Một ý tưởng thú vị tuy nhiên.
Voo

7

Họ làm (tốt, hầu hết trong số họ). Tính năng bạn đang tìm kiếm được gọi là chủ đề .

Chủ đề có vấn đề riêng của họ tuy nhiên:

  1. Bởi vì mã có thể bị treo bất cứ lúc nào , bạn không bao giờ có thể cho rằng mọi thứ sẽ không thay đổi "một mình". Khi lập trình với các chủ đề, bạn lãng phí rất nhiều thời gian suy nghĩ về cách chương trình của bạn nên đối phó với những thứ thay đổi.

    Hãy tưởng tượng một máy chủ trò chơi đang xử lý cuộc tấn công của người chơi vào người chơi khác. Một cái gì đó như thế này:

    if (playerInMeleeRange(attacker, victim)) {
        const damage = calculateAttackDamage(attacker, victim);
        if (victim.health <= damage) {
    
            // attacker gets whatever the victim was carrying as loot
            const loot = victim.getInventoryItems();
            attacker.addInventoryItems(loot);
            victim.removeInventoryItems(loot);
    
            victim.sendMessage("${attacker} hits you with a ${attacker.currentWeapon} and you die!");
            victim.setDead();
        } else {
            victim.health -= damage;
            victim.sendMessage("${attacker} hits you with a ${attacker.currentWeapon}!");
        }
        attacker.markAsKiller();
    }
    

    Ba tháng sau, một người chơi phát hiện ra rằng bằng cách bị giết và đăng xuất chính xác khi attacker.addInventoryItemsđang chạy, sau đó victim.removeInventoryItemssẽ thất bại, anh ta có thể giữ vật phẩm của mình và kẻ tấn công cũng nhận được một bản sao của vật phẩm của mình. Anh ta làm điều này nhiều lần, tạo ra một triệu tấn vàng từ không khí mỏng và phá vỡ nền kinh tế của trò chơi.

    Ngoài ra, kẻ tấn công có thể đăng xuất trong khi trò chơi đang gửi tin nhắn cho nạn nhân và anh ta sẽ không nhận được thẻ "kẻ giết người" trên đầu, vì vậy nạn nhân tiếp theo của anh ta sẽ không chạy trốn khỏi anh ta.

  2. Vì mã có thể bị treo tại bất kỳ điểm nào , bạn cần sử dụng khóa ở mọi nơi khi thao tác cấu trúc dữ liệu. Tôi đã đưa ra một ví dụ ở trên có hậu quả rõ ràng trong một trò chơi, nhưng nó có thể tinh tế hơn. Xem xét thêm một mục vào đầu danh sách được liên kết:

    newItem.nextItem = list.firstItem;
    list.firstItem = newItem;
    

    Đây không phải là vấn đề nếu bạn nói rằng các luồng chỉ có thể bị treo khi chúng thực hiện I / O chứ không phải bất cứ lúc nào. Nhưng tôi chắc chắn bạn có thể tưởng tượng một tình huống có hoạt động I / O - chẳng hạn như đăng nhập:

    for (player = playerList.firstItem; player != null; player = item.nextPlayer) {
        debugLog("${item.name} is online, they get a gold star");
        // Oops! The player might've logged out while the log message was being written to disk, and now this will throw an exception and the remaining players won't get their gold stars.
        // Or the list might've been rearranged and some players might get two and some players might get none.
        player.addInventoryItem(InventoryItems.GoldStar);
    }
    
  3. Bởi vì mã có thể bị treo tại bất kỳ thời điểm nào , có khả năng có thể có rất nhiều trạng thái để lưu. Hệ thống xử lý vấn đề này bằng cách cung cấp cho mỗi luồng một ngăn xếp hoàn toàn riêng biệt. Nhưng ngăn xếp khá lớn, vì vậy bạn không thể có hơn 2000 luồng trong chương trình 32 bit. Hoặc bạn có thể giảm kích thước ngăn xếp, có nguy cơ làm cho nó quá nhỏ.


3

Rất nhiều câu trả lời ở đây gây hiểu lầm, bởi vì trong khi câu hỏi thực sự hỏi về lập trình không đồng bộ và không chặn IO, tôi không nghĩ chúng ta có thể thảo luận một câu hỏi mà không thảo luận về vấn đề khác trong trường hợp cụ thể này.

Mặc dù lập trình không đồng bộ vốn dĩ là tốt, không đồng bộ, nhưng việc lập trình không đồng bộ chủ yếu là để tránh chặn các luồng nhân. Node.js sử dụng tính không đồng bộ thông qua các cuộc gọi lại hoặc Promises để cho phép các hoạt động chặn được gửi từ một vòng lặp sự kiện và Netty trong Java sử dụng tính không đồng bộ thông qua các cuộc gọi lại hoặc CompletableFutures để làm điều gì đó tương tự.

Tuy nhiên, mã không chặn không yêu cầu tính không đồng bộ . Nó phụ thuộc vào mức độ ngôn ngữ lập trình và thời gian chạy của bạn sẵn sàng làm cho bạn.

Go, Erlang và Haskell / GHC có thể xử lý việc này cho bạn. Bạn có thể viết một cái gì đó giống như var response = http.get('example.com/test')và yêu cầu nó giải phóng một chuỗi nhân phía sau hậu trường trong khi chờ phản hồi. Điều này được thực hiện bởi các con khỉ đột, các quá trình Erlang hoặc forkIObuông các chuỗi nhân phía sau hậu trường khi chặn, cho phép nó làm những việc khác trong khi chờ phản hồi.

Đúng là ngôn ngữ không thể thực sự xử lý sự không đồng bộ cho bạn, nhưng một số trừu tượng cho phép bạn đi xa hơn các ngôn ngữ khác, ví dụ như các phần tiếp theo không được xác định hoặc các coroutines không đối xứng. Tuy nhiên, nguyên nhân chính của mã không đồng bộ, chặn các cuộc gọi hệ thống, hoàn toàn có thể được trừu tượng hóa khỏi nhà phát triển.

Node.js và Java hỗ trợ mã không chặn không đồng bộ , trong khi Go và Erlang hỗ trợ mã không chặn đồng bộ . Cả hai đều là những cách tiếp cận hợp lệ với sự đánh đổi khác nhau.

Lập luận khá chủ quan của tôi là những người tranh luận chống lại việc quản lý không chặn thay mặt cho nhà phát triển cũng giống như những người tranh luận về việc thu gom rác trong những năm đầu. Đúng, nó phải chịu một chi phí (trong trường hợp này chủ yếu là nhiều bộ nhớ hơn), nhưng nó làm cho việc phát triển và gỡ lỗi dễ dàng hơn, và làm cho các cơ sở mã hóa mạnh mẽ hơn.

Cá nhân tôi cho rằng mã không chặn không đồng bộ nên được dành riêng cho lập trình hệ thống trong tương lai và các ngăn xếp công nghệ hiện đại hơn sẽ chuyển sang thời gian chạy không chặn đồng bộ để phát triển ứng dụng.


1
Đây là một câu trả lời thực sự thú vị! Nhưng tôi không chắc là tôi hiểu sự khác biệt của bạn giữa mã không chặn đồng bộ và mã không đồng bộ. Đối với tôi, mã không chặn đồng bộ có nghĩa là một cái gì đó giống như hàm C waitpid(..., WNOHANG)không thành công nếu nó phải chặn. Hay là đồng bộ hóa của Cameron ở đây có nghĩa là không có cuộc gọi lại có thể nhìn thấy của lập trình viên / máy trạng thái / vòng lặp sự kiện? Nhưng đối với ví dụ Go của bạn, tôi vẫn phải chờ kết quả từ một con khỉ đột bằng cách đọc từ một kênh, phải không? Làm thế nào điều này ít async hơn async / await trong JS / C # / Python?
amon

1
Tôi sử dụng "không đồng bộ" và "đồng bộ" để thảo luận về mô hình lập trình tiếp xúc với nhà phát triển và "chặn" và "không chặn" để thảo luận về việc chặn luồng nhân trong đó nó không thể làm gì hữu ích, ngay cả khi có các tính toán khác cần làm và có một bộ xử lý logic dự phòng mà nó có thể sử dụng. Chà, một con goroutine chỉ có thể chờ đợi một kết quả mà không chặn luồng cơ bản, nhưng một con goroutine khác có thể giao tiếp với nó qua một kênh nếu nó muốn. Con goroutine không cần sử dụng kênh trực tiếp để chờ đọc ổ cắm không chặn.
Louis Jackman

Hmm ok, tôi hiểu sự khác biệt của bạn bây giờ. Trong khi tôi quan tâm nhiều hơn đến việc quản lý luồng dữ liệu và kiểm soát giữa các coroutines, bạn quan tâm nhiều hơn đến việc không bao giờ chặn luồng nhân chính. Tôi không chắc chắn Go hay Haskell có bất kỳ lợi thế nào so với C ++ hoặc Java về vấn đề này vì chúng cũng có thể khởi động các luồng nền, làm như vậy chỉ cần thêm một chút mã.
amon

@LouisJackman có thể giải thích một chút về tuyên bố cuối cùng của bạn về việc không chặn async cho lập trình hệ thống. Ưu điểm của phương pháp không chặn async là gì?
sunprophit

@sunprophit Không chặn không đồng bộ chỉ là một chuyển đổi trình biên dịch (thường là async / await), trong khi đó, không chặn đồng bộ yêu cầu hỗ trợ thời gian chạy như một số kết hợp thao tác ngăn xếp phức tạp, chèn các điểm lợi tức vào các lệnh gọi hàm giảm số lượng (yêu cầu VM như BEAM), v.v. Giống như thu gom rác, nó giao dịch với độ phức tạp thời gian chạy ít hơn để dễ sử dụng và mạnh mẽ. Các ngôn ngữ hệ thống như C, C ++ và Rust tránh các tính năng thời gian chạy lớn hơn như thế này do các miền được nhắm mục tiêu của chúng, vì vậy việc không chặn không đồng bộ có ý nghĩa hơn ở đó.
Louis Jackman

2

Nếu tôi đọc đúng bạn, bạn đang yêu cầu một mô hình lập trình đồng bộ, nhưng thực hiện hiệu suất cao. Nếu điều đó là chính xác thì điều đó đã có sẵn cho chúng ta dưới dạng các luồng hoặc quy trình xanh, ví dụ Erlang hoặc Haskell. Vì vậy, có, đó là một ý tưởng tuyệt vời, nhưng việc trang bị thêm cho các ngôn ngữ hiện tại không thể luôn suôn sẻ như bạn muốn.


2

Tôi đánh giá cao câu hỏi và tìm thấy phần lớn các câu trả lời chỉ đơn thuần là bảo vệ nguyên trạng. Trong phổ của các ngôn ngữ cấp thấp đến cấp cao, chúng tôi đã bị mắc kẹt trong một thời gian. Cấp độ cao hơn tiếp theo rõ ràng sẽ là một ngôn ngữ ít tập trung vào cú pháp (nhu cầu về các từ khóa rõ ràng như await và async) và nhiều hơn về ý định. (Rõ ràng tín dụng cho Charles Simonyi, nhưng nghĩ về năm 2019 và tương lai.)

Nếu tôi nói với một lập trình viên, hãy viết một số mã chỉ đơn giản là tìm nạp một giá trị từ cơ sở dữ liệu, bạn có thể giả sử một cách an toàn ý tôi là "và BTW, đừng treo UI" và "không đưa ra các cân nhắc khác che giấu các lỗi khó tìm ". Các lập trình viên của tương lai, với một thế hệ ngôn ngữ và công cụ tiếp theo, chắc chắn sẽ có thể viết mã chỉ cần lấy một giá trị trong một dòng mã và đi từ đó.

Ngôn ngữ cấp cao nhất sẽ là nói tiếng Anh và dựa vào năng lực của người thực hiện nhiệm vụ để biết bạn thực sự muốn làm gì. (Nghĩ rằng máy tính trong Star Trek, hoặc hỏi điều gì đó về Alexa.) Chúng ta ở xa đó, nhưng nhích lại gần hơn, và tôi dự đoán rằng ngôn ngữ / trình biên dịch có thể tạo ra mã mạnh mẽ hơn, có chủ ý mà không cần đi xa cần AI.

Một mặt, có những ngôn ngữ hình ảnh mới hơn, như Scratch, thực hiện điều này và không bị sa lầy với tất cả các kỹ thuật cú pháp. Chắc chắn, có rất nhiều công việc hậu trường đang diễn ra để lập trình viên không phải lo lắng về điều đó. Điều đó nói rằng, tôi không viết phần mềm lớp doanh nghiệp trong Scratch, vì vậy, giống như bạn, tôi có cùng kỳ vọng rằng đã đến lúc các ngôn ngữ lập trình trưởng thành tự động quản lý vấn đề đồng bộ / không đồng bộ.


1

Vấn đề bạn mô tả là hai lần.

  • Chương trình bạn đang viết nên hoạt động không đồng bộ một cách tổng thể khi nhìn từ bên ngoài .
  • không nên được hiển thị tại trang web cuộc gọi cho dù một cuộc gọi chức năng có khả năng từ bỏ quyền kiểm soát hay không.

Có một vài cách để đạt được điều này, nhưng về cơ bản, họ đã đun sôi để

  1. có nhiều luồng (ở một mức độ trừu tượng)
  2. có nhiều loại chức năng ở cấp độ ngôn ngữ, tất cả đều được gọi như thế này foo(4, 7, bar, quux).

Đối với (1), tôi kết hợp với nhau để chạy và chạy nhiều tiến trình, sinh ra nhiều luồng nhân và triển khai luồng xanh lập lịch trình xử lý các luồng mức thời gian chạy ngôn ngữ lên các luồng nhân. Từ quan điểm của vấn đề, họ là như nhau. Trong thế giới này, không có chức năng nào từ bỏ hoặc mất quyền kiểm soát từ quan điểm của chủ đề . Bản thân luồng đôi khi không có quyền kiểm soát và đôi khi không chạy nhưng bạn không từ bỏ quyền kiểm soát chủ đề của chính mình trong thế giới này. Một hệ thống phù hợp với mô hình này có thể có hoặc không có khả năng sinh ra các luồng mới hoặc tham gia vào các luồng hiện có. Một hệ thống phù hợp với mô hình này có thể có hoặc không có khả năng sao chép một luồng như Unix fork.

(2) là thú vị. Để thực hiện công lý, chúng ta cần nói về các hình thức giới thiệu và loại bỏ.

Tôi sẽ chỉ ra lý do tại sao awaitkhông thể thêm vào một ngôn ngữ như Javascript theo cách tương thích ngược. Ý tưởng cơ bản là bằng cách đưa ra những lời hứa với người dùng và có sự phân biệt giữa bối cảnh đồng bộ và không đồng bộ, Javascript đã rò rỉ một chi tiết triển khai ngăn chặn xử lý thống nhất các chức năng đồng bộ và không đồng bộ. Ngoài ra còn có một thực tế là bạn không thể awaithứa hẹn bên ngoài cơ thể chức năng không đồng bộ. Các lựa chọn thiết kế này không tương thích với "làm cho tính không đồng bộ trở nên vô hình đối với người gọi".

Bạn có thể giới thiệu một chức năng đồng bộ bằng cách sử dụng lambda và loại bỏ nó bằng một lệnh gọi hàm.

Giới thiệu chức năng đồng bộ:

((x) => {return x + x;})

Loại bỏ chức năng đồng bộ:

f(4)

((x) => {return x + x;})(4)

Bạn có thể đối chiếu điều này với việc giới thiệu và loại bỏ chức năng không đồng bộ.

Giới thiệu chức năng không đồng bộ

(async (x) => {return x + x;})

Loại bỏ chức năng không đồng bộ (lưu ý: chỉ hợp lệ trong một asyncchức năng)

await (async (x) => {return x + x;})(4)

Vấn đề cơ bản ở đây là một hàm không đồng bộ cũng là một hàm đồng bộ tạo ra một đối tượng hứa .

Đây là một ví dụ về cách gọi một hàm không đồng bộ một cách đồng bộ trong thay thế node.js.

> (async (x) => {return x + x;})(4)
Promise { 8 }

Theo giả thuyết, bạn có thể có một ngôn ngữ, thậm chí là một ngôn ngữ được gõ động, trong đó sự khác biệt giữa các cuộc gọi chức năng không đồng bộ và đồng bộ không thể nhìn thấy tại trang web cuộc gọi và có thể không hiển thị tại trang web định nghĩa.

Có thể sử dụng một ngôn ngữ như thế và hạ thấp nó xuống Javascript, bạn chỉ cần thực hiện một cách hiệu quả tất cả các chức năng không đồng bộ.


1

Với goroutines ngôn ngữ Go và thời gian chạy ngôn ngữ Go, bạn có thể viết tất cả mã như thể nó là synchrone. Nếu một hoạt động chặn trong một con goroutine, việc thực thi sẽ tiếp tục trong các con goroutine khác. Và với các kênh, bạn có thể giao tiếp dễ dàng giữa các con khỉ đột. Điều này thường dễ dàng hơn các cuộc gọi lại như trong Javascript hoặc async / await trong các ngôn ngữ khác. Xem https://tour.golang.org/concurrency/1 để biết một số ví dụ và giải thích.

Hơn nữa, tôi không có kinh nghiệm cá nhân với nó, nhưng tôi nghe nói Erlang có các phương tiện tương tự.

Vì vậy, vâng, có các ngôn ngữ lập trình như Go và Erlang, giải quyết vấn đề đồng bộ / không đồng bộ, nhưng tiếc là chúng chưa phổ biến lắm. Khi các ngôn ngữ đó ngày càng phổ biến, có lẽ các cơ sở mà họ cung cấp cũng sẽ được triển khai bằng các ngôn ngữ khác.


Tôi gần như không bao giờ sử dụng ngôn ngữ Go nhưng có vẻ như bạn tuyên bố rõ ràng go ..., vì vậy nó trông giống như await ...không?
Cinn

1
@Cinn Thật ra, không. Bạn có thể thực hiện bất kỳ cuộc gọi nào như một con goroutine lên sợi / sợi xanh của chính nó với go. Và bất kỳ cuộc gọi nào có thể chặn đều được thực hiện không đồng bộ bởi thời gian chạy, nó chỉ chuyển sang một con goroutine khác trong thời gian đó (đa tác vụ hợp tác). Bạn chờ đợi bằng cách chờ đợi một tin nhắn.
Ded repeatator

2
Mặc dù Goroutines là một loại đồng thời, tôi sẽ không đặt chúng vào cùng một nhóm như async / await: không phải là coroutines hợp tác mà tự động (và được ưu tiên!) Nhưng điều này cũng không làm cho việc chờ đợi tự động: Tương đương với awaitviệc đọc từ một kênh <- ch.
amon

@amon Theo như tôi biết, các con khỉ đột được lên lịch hợp tác trên các luồng gốc (thông thường chỉ đủ để tối đa hóa sự song song phần cứng thực sự) bởi thời gian chạy và chúng được HĐH lên lịch trước.
Ded repeatator

OP đã yêu cầu "để có thể viết mã không đồng bộ theo cách đồng bộ". Như bạn đã đề cập, với goroutines và thời gian chạy, bạn hoàn toàn có thể làm điều đó. Bạn không phải lo lắng về các chi tiết của luồng, chỉ cần viết chặn đọc và ghi, như thể mã là đồng bộ, và các con khỉ đột khác của bạn, nếu có, sẽ tiếp tục chạy. Bạn thậm chí không phải "chờ đợi" hoặc đọc từ một kênh để nhận được lợi ích này. Do đó, tôi nghĩ rằng Go đang lập trình ngôn ngữ đáp ứng mong muốn của OP một cách chặt chẽ nhất.

1

Có một khía cạnh rất quan trọng chưa được nêu ra: reentrancy. Nếu bạn có bất kỳ mã nào khác (ví dụ: vòng lặp sự kiện) chạy trong cuộc gọi async (và nếu bạn không thì tại sao bạn thậm chí cần async?), Thì mã có thể ảnh hưởng đến trạng thái chương trình. Bạn không thể ẩn các cuộc gọi không đồng bộ khỏi người gọi vì người gọi có thể phụ thuộc vào các phần của trạng thái chương trình để không bị ảnh hưởng trong suốt thời gian gọi chức năng của anh ta. Thí dụ:

function foo( obj ) {
    obj.x = 2;
    bar();
    log( "obj.x equals 2: " + obj.x );
}

Nếu bar()là một hàm không đồng bộ thì có thể obj.xthay đổi trong quá trình thực thi. Điều này sẽ khá bất ngờ nếu không có bất kỳ gợi ý nào rằng thanh không đồng bộ và hiệu ứng đó là có thể. Cách thay thế duy nhất là nghi ngờ mọi chức năng / phương thức có thể là không đồng bộ và tìm nạp lại và kiểm tra lại bất kỳ trạng thái không cục bộ nào sau mỗi lần gọi hàm. Điều này dễ xảy ra lỗi tinh vi và thậm chí có thể không khả thi nếu một số trạng thái không cục bộ được tìm nạp thông qua các chức năng. Do đó, lập trình viên cần nhận thức được chức năng nào có khả năng thay đổi trạng thái chương trình theo những cách không mong muốn:

async function foo( obj ) {
    obj.x = 2;
    await bar();
    log( "obj.x equals 2: " + obj.x );
}

Bây giờ có thể thấy rõ rằng đó bar()là một hàm không đồng bộ và cách xử lý chính xác là kiểm tra lại giá trị dự kiến obj.xsau đó và xử lý mọi thay đổi có thể xảy ra.

Như đã được lưu ý bởi các câu trả lời khác, các ngôn ngữ chức năng thuần túy như Haskell có thể thoát khỏi hiệu ứng đó hoàn toàn bằng cách tránh sự cần thiết của bất kỳ trạng thái chung / toàn cầu nào. Tôi không có nhiều kinh nghiệm với các ngôn ngữ chức năng nên có lẽ tôi thiên vị với nó, nhưng tôi không nghĩ rằng thiếu trạng thái toàn cầu là một lợi thế khi viết các ứng dụng lớn hơn.


0

Trong trường hợp Javascript mà bạn đã sử dụng trong câu hỏi của mình, có một điểm quan trọng cần lưu ý: Javascript là một luồng và thứ tự thực hiện được đảm bảo miễn là không có cuộc gọi không đồng bộ.

Vì vậy, nếu bạn có một chuỗi như của bạn:

const nbOfUsers = getNbOfUsers();

Bạn được đảm bảo rằng không có gì khác sẽ được thực hiện trong thời gian này. Không cần ổ khóa hoặc bất cứ điều gì tương tự.

Tuy nhiên, nếu getNbOfUserskhông đồng bộ, thì:

const nbOfUsers = await getNbOfUsers();

có nghĩa là trong khi getNbOfUserschạy, sản lượng thực thi và mã khác có thể chạy ở giữa. Điều này có thể lần lượt yêu cầu một số khóa để xảy ra, tùy thuộc vào những gì bạn đang làm.

Vì vậy, nên biết khi nào cuộc gọi không đồng bộ và khi không thực hiện, vì trong một số trường hợp, bạn sẽ cần thực hiện các biện pháp phòng ngừa bổ sung mà bạn không cần nếu cuộc gọi được đồng bộ.


Bạn nói đúng, mã thứ hai của tôi trong câu hỏi không hợp lệ như thể getNbOfUsers()trả về một Lời hứa. Nhưng đó chính xác là vấn đề của câu hỏi của tôi, tại sao chúng ta cần phải viết nó một cách rõ ràng là không đồng bộ, trình biên dịch có thể phát hiện ra nó và xử lý nó tự động theo một cách khác.
Cinn

@Cinn đó không phải là quan điểm của tôi. Quan điểm của tôi là luồng thực thi có thể đến các phần khác trong mã của bạn trong quá trình thực hiện cuộc gọi không đồng bộ, trong khi đó không thể thực hiện được cuộc gọi đồng bộ. Nó sẽ giống như có nhiều luồng chạy nhưng không nhận thức được nó. Điều này có thể kết thúc trong các vấn đề lớn (thường khó phát hiện và tái sản xuất).
jcaron

-4

Điều này có sẵn trong C ++ std::asynckể từ C ++ 11.

Hàm đồng bộ hóa không đồng bộ chạy hàm f không đồng bộ (có khả năng trong một luồng riêng biệt có thể là một phần của nhóm luồng) và trả về một std :: tương lai cuối cùng sẽ giữ kết quả của lệnh gọi hàm đó.

Và với C ++ 20 coroutines có thể được sử dụng:


5
Điều này dường như không trả lời câu hỏi. Theo liên kết của bạn: "Coroutines TS cung cấp cho chúng tôi những gì? Ba từ khóa ngôn ngữ mới: co_await, co_yield và co_return" ... Nhưng câu hỏi đặt ra là tại sao chúng ta cần một từ khóa await(hoặc co_awaittrong trường hợp này) ở vị trí đầu tiên?
Arturo Torres Sánchez
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.