Phân luồng bằng Python [đã đóng]


76

Các mô-đun được sử dụng để viết các ứng dụng đa luồng trong Python là gì? Tôi biết các cơ chế đồng thời cơ bản được cung cấp bởi ngôn ngữ và cả Stackless Python , nhưng điểm mạnh và điểm yếu tương ứng của chúng là gì?


Tôi thấy có ít nhất ba lý do khiến câu hỏi này (cố chấp, khuyến nghị và quá rộng) nên được tạm hoãn.
lpapp

Tôi biết câu hỏi này là từ 09 nhưng ai đó có thể trả lời nó với asyncioxin vui lòng? Cảm ơn trước và rất muốn xem mã trông như thế nào.
alvas

@lpapp trong đó câu hỏi tạo ra một số câu trả lời có ý kiến, tôi tin rằng cách diễn đạt của nó là khách quan (quá rộng, có thể). OP đang hỏi những mô-đun nào được sử dụng trong Python để đồng thời và ưu / nhược điểm cho từng mô-đun. Nghe có vẻ hợp lý với tôi.
guyarad

Câu trả lời:


117

Theo thứ tự ngày càng phức tạp:

Sử dụng mô-đun phân luồng

Ưu điểm:

  • Thực sự dễ dàng để chạy bất kỳ chức năng nào (thực tế là bất kỳ chức năng nào có thể gọi được) trong luồng riêng của nó.
  • Chia sẻ dữ liệu nếu không phải là dễ dàng (khóa không bao giờ dễ dàng :), ít nhất là đơn giản.

Nhược điểm:

  • Như đã đề cập bởi Juergen Các luồng Python thực sự không thể truy cập đồng thời trạng thái trong trình thông dịch (có một khóa lớn, Khóa thông dịch viên toàn cầu khét tiếng .) Điều đó có nghĩa là trong thực tế, các luồng hữu ích cho các tác vụ liên kết I / O (mạng, ghi vào đĩa, vân vân), nhưng hoàn toàn không hữu ích khi thực hiện tính toán đồng thời.

Sử dụng mô-đun đa xử lý

Trong trường hợp sử dụng đơn giản, điều này trông giống hệt như sử dụng threadingngoại trừ mỗi tác vụ được chạy trong quy trình riêng của nó chứ không phải luồng riêng của nó. (Hầu như nghĩa đen: Nếu bạn lấy ví dụ của Eli , và thay thế threadingvới multiprocessing, Threadvới Process, và Queue(module) với multiprocessing.Queue, nó sẽ chạy tốt.)

Ưu điểm:

  • Đồng thời thực tế cho tất cả các tác vụ (không có Khóa thông dịch viên toàn cầu).
  • Cân nhiều bộ xử lý, thậm chí có thể chia tỷ lệ cho nhiều máy .

Nhược điểm:

  • Quá trình xử lý chậm hơn luồng.
  • Chia sẻ dữ liệu giữa các quy trình phức tạp hơn so với các luồng.
  • Bộ nhớ không được chia sẻ ngầm. Bạn phải chia sẻ nó một cách rõ ràng hoặc bạn phải chọn các biến và gửi chúng qua lại. Điều này an toàn hơn, nhưng khó hơn. (Nếu nó ngày càng quan trọng, các nhà phát triển Python dường như đang thúc đẩy mọi người theo hướng này.)

Sử dụng mô hình sự kiện, chẳng hạn như Twisted

Ưu điểm:

  • Bạn có quyền kiểm soát cực kỳ tốt đối với mức độ ưu tiên, đối với những gì thực thi khi nào.

Nhược điểm:

  • Ngay cả với một thư viện tốt, lập trình không đồng bộ thường khó hơn lập trình phân luồng, khó cả về mặt hiểu những gì sẽ xảy ra và về gỡ lỗi những gì thực sự đang xảy ra.

Trong mọi trường hợp, tôi cho rằng bạn đã hiểu nhiều vấn đề liên quan đến đa nhiệm, đặc biệt là vấn đề phức tạp về cách chia sẻ dữ liệu giữa các tác vụ. Nếu vì lý do nào đó mà bạn không biết khi nào và làm thế nào để sử dụng khóa và các điều kiện, bạn phải bắt đầu với những điều đó. Mã đa nhiệm chứa đầy sự tinh tế và khó hiểu, và tốt nhất là bạn nên hiểu rõ về các khái niệm trước khi bắt đầu.


4
Tôi cho rằng thứ tự phức tạp của bạn gần như hoàn toàn ngược. Lập trình đa luồng thực sự khó thực hiện một cách chính xác (hầu như không ai làm). Lập trình sự kiện thì khác, nhưng thật sự dễ dàng để hiểu những gì đang xảy ra và viết các bài kiểm tra chứng minh rằng nó làm đúng những gì cần thiết. (Tôi nói rằng đã đạt được độ phủ 100% trên một thư viện mạng đồng thời ồ ạt vào cuối tuần này).
Dustin

3
Hừ! Tôi nghĩ rằng lập trình sự kiện cho thấy sự phức tạp. Nó buộc bạn phải giải quyết trực tiếp hơn. Bạn có thể tranh luận rằng sự phức tạp vốn có trong đồng thời bất kể bạn tiếp cận nó như thế nào, và tôi đồng ý với bạn. Nhưng sau khi thực hiện một số chương trình dựa trên sự kiện và theo luồng khá lớn, tôi nghĩ tôi vẫn giữ nguyên những gì tôi đã nói: chương trình dựa trên sự kiện nằm trong tầm kiểm soát của tôi nhiều hơn, nhưng thực sự thì việc viết mã nó phức tạp hơn.
quark

1
Những gì Quy trình chậm hơn luồng. chính xác nghĩa là gì? Quy trình không được chậm hơn luồng, vì quy trình không chạy bất kỳ mã nào. Các chủ đề trong các quy trình này chạy mã. Tôi chỉ có thể đoán rằng điều đó có nghĩa là quá trình bắt đầu chậm hơn, điều đó là chính xác. Vì vậy, nếu chương trình của bạn cần thường xuyên bắt đầu luồng / quy trình mới, việc sử dụng multiprocessingsẽ chậm hơn. Tuy nhiên, trong trường hợp này, việc sử dụng đa luồng của bạn cũng có thể được cải thiện đáng kể.
guyarad

103

Bạn đã có rất nhiều câu trả lời, từ "chủ đề giả" cho đến các khuôn khổ bên ngoài, nhưng tôi không thấy ai đề cập đến Queue.Queue- "nước sốt bí mật" của phân luồng CPython.

Để mở rộng: miễn là bạn không cần chồng chéo xử lý nặng CPU Python thuần túy (trong trường hợp đó bạn cần multiprocessing- nhưng nó cũng đi kèm với việc Queuetriển khai riêng , vì vậy bạn có thể áp dụng một số lưu ý cần thiết với lời khuyên chung của tôi. 'm give ;-), tích hợp sẵn của Python threadingsẽ làm được ... nhưng nó sẽ làm điều đó tốt hơn nhiều nếu bạn sử dụng nó một cách cố vấn , ví dụ: như sau.

"Quên" bộ nhớ chia sẻ, được cho là điểm cộng chính của phân luồng và đa xử lý - nó không hoạt động tốt, không mở rộng quy mô tốt, không bao giờ có, sẽ không bao giờ. Chỉ sử dụng bộ nhớ dùng chung cho các cấu trúc dữ liệu được thiết lập một lần trước khi bạn sinh ra các luồng con và không bao giờ thay đổi sau đó - đối với mọi thứ khác, hãy tạo một luồng duy nhất chịu trách nhiệm cho tài nguyên đó và giao tiếp với luồng đó qua Queue.

Dành một chuỗi chuyên biệt cho mọi tài nguyên mà bạn thường nghĩ là bảo vệ bằng các khóa: cấu trúc dữ liệu có thể thay đổi hoặc nhóm liên kết của chúng, kết nối với quy trình bên ngoài (DB, máy chủ XMLRPC, v.v.), tệp bên ngoài, v.v. . Nhận một nhóm luồng nhỏ dành cho các nhiệm vụ mục đích chung không có hoặc cần một nguồn tài nguyên chuyên dụng như vậy - không sinh ra các luồng khi cần và khi cần, nếu không chi phí chuyển đổi luồng sẽ làm bạn choáng ngợp.

Giao tiếp giữa hai luồng luôn thông qua Queue.Queue- một dạng truyền thông điệp, nền tảng lành mạnh duy nhất cho quá trình đa xử lý (bên cạnh bộ nhớ giao dịch, đầy hứa hẹn nhưng tôi biết không có triển khai nào xứng đáng với sản xuất ngoại trừ In Haskell).

Mỗi luồng chuyên dụng quản lý một tài nguyên (hoặc một tập hợp tài nguyên cố kết nhỏ) sẽ lắng nghe các yêu cầu trên một cá thể Queue.Queue cụ thể. Các luồng trong một nhóm sẽ đợi trên một Queue.Queue được chia sẻ duy nhất (Hàng đợi là luồng an toàn vững chắc và sẽ không làm bạn thất vọng trong việc này).

Các chủ đề chỉ cần xếp hàng một yêu cầu trên một số hàng đợi (được chia sẻ hoặc dành riêng) làm như vậy mà không cần đợi kết quả và tiếp tục. Các luồng cuối cùng DO cần một kết quả hoặc xác nhận cho một yêu cầu xếp hàng một cặp (yêu cầu, hàng nhận) với một thể hiện của Hàng đợi. Hàng đợi mà họ vừa thực hiện và cuối cùng, khi phản hồi hoặc xác nhận là không thể thiếu để tiếp tục, họ nhận được (chờ ) từ hàng nhận của họ. Hãy chắc chắn rằng bạn đã sẵn sàng để nhận được phản hồi lỗi cũng như phản hồi hoặc xác nhận thực tế (Twisted's deferredrất tốt trong việc tổ chức loại phản hồi có cấu trúc này, BTW!).

Bạn cũng có thể sử dụng Hàng đợi để "đậu" các bản sao của tài nguyên có thể được sử dụng bởi bất kỳ một luồng nào nhưng không bao giờ được chia sẻ giữa nhiều luồng cùng một lúc (kết nối DB với một số thành phần DBAPI, con trỏ với những người khác, v.v.) - điều này cho phép bạn thư giãn yêu cầu luồng dành riêng có lợi cho việc gộp chung nhiều hơn (luồng gộp từ hàng đợi chia sẻ một yêu cầu cần tài nguyên có thể xếp hàng sẽ lấy tài nguyên đó từ hàng đợi apppropriate, đợi nếu cần, v.v.).

Twisted thực sự là một cách tốt để tổ chức minuet này (hoặc hình vuông nhảy tùy trường hợp), không chỉ nhờ hoãn lại mà vì kiến ​​trúc cơ sở vững chắc, có khả năng mở rộng cao của nó: bạn có thể sắp xếp mọi thứ để sử dụng luồng hoặc quy trình con chỉ khi thực sự được đảm bảo, trong khi thực hiện hầu hết mọi thứ thường được coi là xứng đáng trong một chuỗi sự kiện.

Nhưng, tôi nhận ra rằng Twisted không dành cho tất cả mọi người - phương pháp "dành riêng hoặc gộp tài nguyên, sử dụng Queue up wazoo, không bao giờ làm bất cứ điều gì cần Khóa hoặc cấm Guido, bất kỳ thủ tục đồng bộ hóa nào thậm chí còn nâng cao hơn, chẳng hạn như semaphore hoặc điều kiện" có thể vẫn được sử dụng ngay cả khi bạn không thể xoay sở với các phương pháp luận hướng sự kiện không đồng bộ và sẽ vẫn mang lại độ tin cậy và hiệu suất cao hơn bất kỳ phương pháp phân luồng áp dụng rộng rãi nào khác mà tôi từng gặp.


6
Nếu bạn có thể thích một câu trả lời, tôi sẽ làm như vậy cho câu trả lời này. Đó là một trong những thứ kích thích tư duy nhất mà tôi tìm thấy trên Stack Overflow.
quark

1
@quark, cảm ơn vì những lời tốt đẹp và, rất vui vì bạn thích nó!
Alex Martelli,

4
ước gì có thể tăng gấp đôi upvote này
Corey Goldberg

2
Đây là một câu trả lời chung mạnh mẽ về phân luồng: cách tiếp cận được mô tả phản ánh được lựa chọn từ các ngôn ngữ đa xử lý khác bao gồm scala (LinkedBlockingQueue là cấu trúc hỗ trợ đằng sau Actors) và smalltalk. Một tác phẩm đáng đọc.
StephenBoesch

Bài đăng tuyệt vời Alex! (không thực sự trả lời câu hỏi, nhưng đáng để đọc). Nơi mà hầu hết nội dung có vẻ bất tử (tức là cũng có liên quan đến ngày nay, mặc dù bạn đã viết cách đây 7 năm), tôi đã tự hỏi quan điểm của bạn đã thay đổi như thế nào kể từ đó. Cảm thấy tự do để liên kết đến các nguồn lực khác tất nhiên ...
guyarad

22

Nó phụ thuộc vào những gì bạn đang cố gắng thực hiện, nhưng tôi chỉ sử dụng threadingmô-đun trong thư viện tiêu chuẩn vì nó giúp bạn thực sự dễ dàng sử dụng bất kỳ chức năng nào và chỉ chạy nó trong một chuỗi riêng biệt.

from threading import Thread

def f():
    ...

def g(arg1, arg2, arg3=None):
    ....

Thread(target=f).start()
Thread(target=g, args=[5, 6], kwargs={"arg3": 12}).start()

Và như thế. Tôi thường thiết lập nhà sản xuất / người tiêu dùng bằng cách sử dụng hàng đợi được đồng bộ hóa do Queuemô-đun cung cấp

from Queue import Queue
from threading import Thread

q = Queue()
def consumer():
    while True:
        print sum(q.get())

def producer(data_source):
    for line in data_source:
        q.put( map(int, line.split()) )

Thread(target=producer, args=[SOME_INPUT_FILE_OR_SOMETHING]).start()
for i in range(10):
    Thread(target=consumer).start()

1
Tôi xin lỗi, nếu tôi muốn chuyển một có thể gọi trả về một giá trị làm mục tiêu cho một Luồng, làm cách nào tôi có thể tìm nạp kết quả của nó trong luồng chính? Có thể hay tôi nên sử dụng trình bao bọc để hàm đưa kết quả của nó vào một đối tượng có thể sửa đổi? Tôi không muốn liên kết kết quả với chính đối tượng luồng. Phương pháp hay nhất là gì? Cảm ơn bạn.
newtover

3
@newtover: Bạn vẫn đang mô tả tình huống phân luồng cơ bản của nhà sản xuất / người tiêu dùng như trong ví dụ của tôi, vì vậy trong trường hợp này, giải pháp Pythonic vẫn là sử dụng đối tượng Hàng đợi đồng bộ. Yêu cầu mỗi luồng đặt kết quả của nó trong hàng đợi các giá trị đầu ra và để luồng chính truy xuất chúng từ hàng đợi khi rảnh rỗi. Tài liệu cho lớp Hàng đợi có thể được tìm thấy tại docs.python.org/library/queue.html và chúng thậm chí có ví dụ về việc thực hiện chính xác những gì bạn mô tả tại docs.python.org/library/queue.html#Queue.Queue.join
Eli Courtwright

1
cảm ơn bạn cho liên kết và câu trả lời. Một câu hỏi nữa: có thứ gì giống với từ điển có cùng chức năng không, hay tốt hơn là bạn nên tìm nạp tất cả các mục từ hàng đợi và tự điền vào từ điển? Tôi có thể sử dụng từ điển tích hợp sẵn cho mục đích, tự tham gia các chuỗi không?
newtover

2
@newtover: Thông số Python không đảm bảo rằng dict được đồng bộ hóa, nhưng việc triển khai CPython thì có. Vì vậy, trừ khi bạn đang sử dụng Jython hoặc PyPy hoặc IronPython hoặc thứ gì đó, bạn có thể sử dụng một câu lệnh thông thường, tùy thuộc vào những gì bạn đang làm. Vì vậy, nếu bạn chỉ có các chuỗi khác nhau thiết lập các khóa / giá trị dict thì điều đó sẽ ổn. Nhưng nếu bạn đang iterating qua một dict hoặc đọc / sửa đổi / tái thiết lập các giá trị dict thì có thể bạn sẽ cần phải làm đồng bộ hóa của riêng mình, như thế này: docs.python.org/library/...
Eli Courtwright

1
Nhiệm vụ của tôi ngụ ý là song song tính toán các giá trị ánh xạ mà không có bất kỳ xử lý nào khác của ánh xạ ở giữa. Tôi đã sử dụng dict tích hợp sẵn. Bạn xác nhận rằng việc triển khai CPython của dict đã được đồng bộ hóa, vì vậy tôi sẽ ở lại với giải pháp. Cảm ơn bạn một lần nữa.
newtover

13

Kamaelia là một framework python để xây dựng các ứng dụng với nhiều quy trình giao tiếp.

(nguồn: kamaelia.org ) Kamaelia - Đồng tiền tương đối hữu ích và thú vị

Trong Kamaelia, bạn xây dựng hệ thống từ các thành phần đơn giản có thể nói chuyện với nhau . Điều này tăng tốc độ phát triển, hỗ trợ bảo trì hàng loạt và cũng có nghĩa là bạn xây dựng phần mềm đồng thời tự nhiên . Nó dự định có thể truy cập được bởi bất kỳ nhà phát triển nào , bao gồm cả người mới. Nó cũng làm cho nó vui vẻ :)

Những loại hệ thống? Máy chủ mạng, máy khách, ứng dụng máy tính để bàn, trò chơi dựa trên pygame, hệ thống và đường ống chuyển mã, hệ thống truyền hình kỹ thuật số, trình xóa thư rác, công cụ giảng dạy và nhiều hơn nữa :)

Đây là một đoạn video từ Pycon 2009. Nó bắt đầu bằng cách so sánh Kamaelia với TwistedParallel Python và sau đó trình diễn Kamaelia.

Kết hợp dễ dàng với Kamaelia - Phần 1 (59:08)
Đồng tiền dễ dàng với Kamaelia - Phần 2 (18:15)


1
Tôi không biết chính xác tại sao ai đó lại đánh dấu câu trả lời này ... vui lòng đảo ngược phiếu bầu ... trừ khi bạn có thể đưa ra lý do chính đáng để đánh dấu nó ...
Jon

15
Tôi đoán một số người ghét mèo
Sam Hasler

6

Về Kamaelia, câu trả lời trên không thực sự bao hàm lợi ích ở đây. Cách tiếp cận của Kamaelia cung cấp một giao diện thống nhất, không hoàn hảo về mặt thực dụng, để xử lý các luồng, trình tạo & quy trình trong một hệ thống duy nhất để đồng thời.

Về cơ bản, nó cung cấp một phép ẩn dụ về một thứ đang hoạt động có hộp thư đến và hộp thư đi. Bạn gửi tin nhắn đến hộp thư đi và khi được kết nối với nhau, tin nhắn sẽ chuyển từ hộp thư đi đến hộp thư đến. Phép ẩn dụ / API này vẫn giống nhau cho dù bạn đang sử dụng trình tạo, luồng hoặc quy trình hoặc nói với các hệ thống khác.

Phần "không hoàn hảo" là do cú pháp chưa được thêm vào cho hộp thư đến và hộp thư đi (mặc dù điều này đang được thảo luận) - tập trung vào tính an toàn / khả năng sử dụng trong hệ thống.

Lấy ví dụ về người tiêu dùng sản xuất bằng cách sử dụng ren trần ở trên, điều này trở thành điều này ở Kamaelia:

Pipeline(Producer(), Consumer() )

Trong ví dụ này, không quan trọng nếu đây là các thành phần có ren hay không, sự khác biệt duy nhất giữa chúng từ góc độ sử dụng là kính cơ bản cho thành phần. Các thành phần của trình tạo giao tiếp bằng cách sử dụng danh sách, các thành phần phân luồng bằng Queue.Queues và xử lý dựa trên os.pipes.

Tuy nhiên, lý do đằng sau cách tiếp cận này là khiến việc gỡ lỗi khó trở nên khó hơn. Trong phân luồng - hoặc bất kỳ đồng thời bộ nhớ dùng chung nào mà bạn có, vấn đề số một mà bạn gặp phải là cập nhật dữ liệu được chia sẻ vô tình bị hỏng. Bằng cách sử dụng thông báo chuyển bạn loại bỏ một lớp lỗi.

Nếu bạn sử dụng luồng và khóa trần ở mọi nơi, bạn thường đang làm việc với giả định rằng khi bạn viết mã, bạn sẽ không mắc bất kỳ lỗi nào. Trong khi tất cả chúng ta đều khao khát điều đó, thì điều đó rất hiếm khi xảy ra. Bằng cách gói gọn hành vi khóa ở một nơi, bạn đơn giản hóa những nơi có thể xảy ra sai sót. (Trình xử lý ngữ cảnh trợ giúp, nhưng không trợ giúp với các cập nhật ngẫu nhiên bên ngoài trình xử lý ngữ cảnh)

Rõ ràng là không phải mọi đoạn mã đều có thể được viết dưới dạng truyền thông điệp và kiểu chia sẻ, đó là lý do tại sao Kamaelia cũng có một bộ nhớ giao dịch phần mềm đơn giản (STM), đây là một ý tưởng thực sự gọn gàng với một cái tên khó chịu - nó giống điều khiển phiên bản hơn cho các biến - tức là kiểm tra một số biến, cập nhật chúng và cam kết trở lại. Nếu bạn có một cuộc đụng độ, bạn rửa sạch và lặp lại.

Các liên kết có liên quan:

Dù sao, tôi hy vọng đó là một câu trả lời hữu ích. FWIW, lý do cốt lõi đằng sau thiết lập của Kamaelia là làm cho đồng thời an toàn hơn và dễ sử dụng hơn trong các hệ thống python mà không cần vẫy đuôi của con chó. (tức là một thùng lớn các thành phần

Tôi có thể hiểu tại sao câu trả lời khác của Kamaelia lại bị sửa đổi, vì ngay cả đối với tôi, nó giống như một quảng cáo hơn là một câu trả lời. Là tác giả của Kamaelia, thật vui khi thấy sự nhiệt tình mặc dù tôi hy vọng điều này chứa nội dung phù hợp hơn một chút :-)

Và đó là cách nói của tôi, xin lưu ý rằng câu trả lời này theo định nghĩa là thiên vị, nhưng đối với tôi, mục đích của Kamaelia là cố gắng và kết hợp thực tiễn tốt nhất của IMO là gì. Tôi khuyên bạn nên thử một vài hệ thống và xem cái nào phù hợp với bạn. (ngoài ra nếu điều này không phù hợp với việc tràn ngăn xếp, xin lỗi - tôi mới tham gia diễn đàn này :-)


3

Tôi sẽ sử dụng Microthreads (Tasklets) của Stackless Python, nếu tôi phải sử dụng các luồng.

Toàn bộ trò chơi trực tuyến (nhiều người chơi) được xây dựng dựa trên Stackless và nguyên tắc đa luồng của nó - vì bản gốc chỉ là để làm chậm thuộc tính nhiều người chơi của trò chơi.

Chủ đề trong CPython không được khuyến khích rộng rãi. Một lý do là GIL - một khóa thông dịch toàn cục - tuần tự hóa luồng cho nhiều phần của quá trình thực thi. Kinh nghiệm của tôi là thực sự rất khó để tạo các ứng dụng nhanh theo cách này. Ví dụ của tôi viết mã trong đó tất cả chậm hơn với luồng - với một lõi (nhưng nhiều người chờ đợi đầu vào nên có thể thực hiện một số tăng hiệu suất).

Với CPython, thay vì sử dụng các quy trình riêng biệt nếu có thể.


3

Nếu bạn thực sự muốn làm bẩn bàn tay của mình, bạn có thể thử sử dụng máy phát điện để giả mạo coroutines . Nó có thể không phải là hiệu quả nhất về mặt công việc liên quan, nhưng các quy trình cung cấp cho bạn khả năng kiểm soát rất tốt đối với đa nhiệm hợp tác hơn là đa nhiệm phủ đầu mà bạn sẽ tìm thấy ở những nơi khác.

Một lợi thế bạn sẽ thấy là nói chung, bạn sẽ không cần khóa hoặc tắt tiếng khi sử dụng đa nhiệm đồng tác, nhưng lợi thế quan trọng hơn đối với tôi là tốc độ chuyển đổi gần như bằng không giữa các "luồng". Tất nhiên, Stackless Python được cho là rất tốt cho điều đó; và sau đó có Erlang, nếu nó không phải là Python.

Có lẽ nhược điểm lớn nhất trong đa nhiệm hợp tác là thiếu cách giải quyết chung để chặn I / O. Và trong các coroutines giả mạo, bạn cũng sẽ gặp phải vấn đề là bạn không thể chuyển "thread" từ bất kỳ thứ gì ngoại trừ cấp cao nhất của ngăn xếp trong một chuỗi.

Sau khi bạn đã tạo một ứng dụng thậm chí hơi phức tạp với các quy trình giả mạo, bạn sẽ thực sự bắt đầu đánh giá cao công việc lên lịch quy trình ở cấp hệ điều hành.

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.