Đầu tiên, điều quan trọng là phải biết sự khác biệt giữa các luồng và hàng đợi và GCD thực sự làm gì. Khi chúng tôi sử dụng hàng đợi công văn (thông qua GCD), chúng tôi thực sự xếp hàng, không phân luồng. Khung công tác được thiết kế đặc biệt để giúp chúng tôi tránh khỏi việc phân luồng, vì Apple thừa nhận rằng "việc thực hiện một giải pháp phân luồng chính xác [có thể] trở nên cực kỳ khó khăn, nếu không [đôi khi] không thể đạt được." Do đó, để thực hiện đồng thời các nhiệm vụ (các nhiệm vụ mà chúng ta không muốn đóng băng UI), tất cả những gì chúng ta cần làm là tạo một hàng đợi các nhiệm vụ đó và giao cho GCD. Và GCD xử lý tất cả các luồng liên quan. Do đó, tất cả những gì chúng tôi thực sự đang làm là xếp hàng.
Điều thứ hai cần biết ngay là nhiệm vụ là gì. Một tác vụ là tất cả các mã trong khối hàng đợi đó (không phải trong hàng đợi, bởi vì chúng ta có thể thêm mọi thứ vào hàng đợi mọi lúc, nhưng trong bao đóng nơi chúng ta đã thêm nó vào hàng đợi). Một nhiệm vụ đôi khi được gọi là một khối và một khối đôi khi được gọi là một nhiệm vụ (nhưng chúng thường được gọi là các nhiệm vụ, đặc biệt là trong cộng đồng Swift). Và cho dù nhiều hay ít mã, tất cả các mã trong dấu ngoặc nhọn đều được coi là một tác vụ duy nhất:
serialQueue.async {
// this is one task
// it can be any number of lines with any number of methods
}
serialQueue.async {
// this is another task added to the same queue
// this queue now has two tasks
}
Và rõ ràng đề cập rằng đồng thời đơn giản có nghĩa là cùng một lúc với những thứ khác và nối tiếp có nghĩa là cái khác (không bao giờ cùng một lúc). Để nối tiếp một cái gì đó, hoặc đặt một cái gì đó nối tiếp, chỉ có nghĩa là thực hiện nó từ đầu đến cuối theo thứ tự từ trái sang phải, từ trên xuống dưới, không bị gián đoạn.
Có hai loại hàng đợi, nối tiếp và đồng thời, nhưng tất cả các hàng đợi là đồng thời liên quan đến nhau . Thực tế là bạn muốn chạy bất kỳ mã nào "trong nền" có nghĩa là bạn muốn chạy nó đồng thời với một luồng khác (thường là luồng chính). Do đó, tất cả các hàng đợi gửi, nối tiếp hoặc đồng thời, thực hiện các nhiệm vụ của chúng đồng thời liên quan đến các hàng đợi khác . Bất kỳ tuần tự hóa nào được thực hiện bởi hàng đợi (theo hàng đợi nối tiếp), chỉ phải thực hiện với các tác vụ trong hàng đợi gửi [nối tiếp] đó (như trong ví dụ ở trên có hai nhiệm vụ trong cùng một hàng đợi nối tiếp; các tác vụ đó sẽ được thực hiện sau cái khác, không bao giờ đồng thời).
NHIỆM VỤ SERIAL (thường được gọi là hàng đợi gửi riêng) đảm bảo thực hiện từng nhiệm vụ một từ lúc bắt đầu đến khi kết thúc theo thứ tự mà chúng đã được thêm vào hàng đợi cụ thể đó. Đây là bảo đảm duy nhất của việc xê-ri hóa ở bất cứ đâu trong cuộc thảo luận về hàng đợi - đó là các nhiệm vụ cụ thể trong một hàng đợi nối tiếp cụ thể được thực hiện nối tiếp. Tuy nhiên, hàng đợi nối tiếp có thể chạy đồng thời với các hàng đợi nối tiếp khác nếu chúng là các hàng đợi riêng biệt bởi vì, một lần nữa, tất cả các hàng đợi đồng thời liên quan đến nhau. Tất cả các tác vụ chạy trên các luồng riêng biệt nhưng không phải mọi tác vụ đều được đảm bảo để chạy trên cùng một luồng (không quan trọng, nhưng thú vị để biết). Và khung iOS không đi kèm với bất kỳ hàng đợi nối tiếp sẵn sàng sử dụng nào, bạn phải tạo chúng. Hàng đợi riêng tư (không toàn cầu) được nối tiếp theo mặc định, do đó, để tạo một hàng đợi nối tiếp:
let serialQueue = DispatchQueue(label: "serial")
Bạn có thể làm cho nó đồng thời thông qua thuộc tính thuộc tính của nó:
let concurrentQueue = DispatchQueue(label: "concurrent", attributes: [.concurrent])
Nhưng tại thời điểm này, nếu bạn không thêm bất kỳ thuộc tính nào khác vào hàng đợi riêng tư, Apple khuyên bạn chỉ nên sử dụng một trong các hàng đợi toàn cầu sẵn sàng hoạt động của họ (tất cả đều đồng thời). Ở dưới cùng của câu trả lời này, bạn sẽ thấy một cách khác để tạo hàng đợi nối tiếp (sử dụng thuộc tính đích), đó là cách Apple khuyên bạn nên làm điều đó (để quản lý tài nguyên hiệu quả hơn). Nhưng bây giờ, ghi nhãn nó là đủ.
QUEUES CONCURRENT (thường được gọi là hàng đợi công văn toàn cầu) có thể thực hiện các nhiệm vụ cùng một lúc; tuy nhiên, các nhiệm vụ được đảm bảo bắt đầu theo thứ tự chúng đã được thêm vào hàng đợi cụ thể đó, nhưng không giống như các hàng đợi nối tiếp, hàng đợi không đợi nhiệm vụ đầu tiên kết thúc trước khi bắt đầu nhiệm vụ thứ hai. Các tác vụ (như với hàng đợi nối tiếp) chạy trên các luồng riêng biệt và (như với hàng đợi nối tiếp) không phải mọi tác vụ đều được đảm bảo để chạy trên cùng một luồng (không quan trọng, nhưng thú vị để biết). Và khung iOS đi kèm với bốn hàng đợi đồng thời sẵn sàng sử dụng. Bạn có thể tạo một hàng đợi đồng thời bằng ví dụ trên hoặc bằng cách sử dụng một trong các hàng đợi toàn cầu của Apple (thường được khuyến nghị):
let concurrentQueue = DispatchQueue.global(qos: .default)
RETAIN-CYCLE RESISTANT: Các hàng đợi điều phối là các đối tượng được tính tham chiếu nhưng bạn không cần giữ lại và giải phóng hàng đợi toàn cầu vì chúng là toàn cục, do đó giữ lại và phát hành bị bỏ qua. Bạn có thể truy cập hàng đợi toàn cầu trực tiếp mà không cần phải gán chúng cho một tài sản.
Có hai cách để gửi hàng đợi: đồng bộ và không đồng bộ.
TRANH CHẤP SYNC có nghĩa là luồng trong đó hàng đợi được gửi đi (luồng gọi) tạm dừng sau khi gửi hàng đợi và đợi tác vụ trong khối hàng đợi đó kết thúc thực hiện trước khi tiếp tục. Để gửi đồng bộ:
DispatchQueue.global(qos: .default).sync {
// task goes in here
}
TRANH CHẤP ASYNC có nghĩa là luồng gọi tiếp tục chạy sau khi gửi hàng đợi và không đợi tác vụ trong khối hàng đợi đó hoàn tất thực hiện. Để gửi không đồng bộ:
DispatchQueue.global(qos: .default).async {
// task goes in here
}
Bây giờ người ta có thể nghĩ rằng để thực hiện một nhiệm vụ nối tiếp, nên sử dụng hàng đợi nối tiếp và điều đó không chính xác. Để thực thi nhiều tác vụ nối tiếp, nên sử dụng hàng đợi nối tiếp, nhưng tất cả các tác vụ (tự cách ly) được thực hiện nối tiếp. Xem xét ví dụ này:
whichQueueShouldIUse.syncOrAsync {
for i in 1...10 {
print(i)
}
for i in 1...10 {
print(i + 100)
}
for i in 1...10 {
print(i + 1000)
}
}
Bất kể bạn định cấu hình như thế nào (nối tiếp hoặc đồng thời) hoặc gửi (đồng bộ hóa hoặc không đồng bộ) hàng đợi này, tác vụ này sẽ luôn được thực hiện nối tiếp. Vòng lặp thứ ba sẽ không bao giờ chạy trước vòng lặp thứ hai và vòng lặp thứ hai sẽ không bao giờ chạy trước vòng lặp thứ nhất. Điều này đúng trong bất kỳ hàng đợi sử dụng bất kỳ công văn. Đó là khi bạn giới thiệu nhiều nhiệm vụ và / hoặc hàng đợi trong đó thực sự nối tiếp và đồng thời thực sự phát huy tác dụng.
Hãy xem xét hai hàng đợi này, một nối tiếp và một đồng thời:
let serialQueue = DispatchQueue(label: "serial")
let concurrentQueue = DispatchQueue.global(qos: .default)
Giả sử chúng tôi gửi hai hàng đợi đồng thời trong async:
concurrentQueue.async {
for i in 1...5 {
print(i)
}
}
concurrentQueue.async {
for i in 1...5 {
print(i + 100)
}
}
1
101
2
102
103
3
104
4
105
5
Đầu ra của chúng bị xáo trộn (như mong đợi) nhưng lưu ý rằng mỗi hàng đợi thực hiện nhiệm vụ riêng nối tiếp. Đây là ví dụ cơ bản nhất về đồng thời - hai tác vụ chạy cùng lúc trong nền trong cùng một hàng đợi. Bây giờ hãy tạo một chuỗi đầu tiên:
serialQueue.async {
for i in 1...5 {
print(i)
}
}
concurrentQueue.async {
for i in 1...5 {
print(i + 100)
}
}
101
1
2
102
3
103
4
104
5
105
Không phải hàng đợi đầu tiên sẽ được thực hiện nối tiếp? Đó là (và thứ hai cũng vậy). Bất cứ điều gì khác xảy ra trong nền không liên quan đến hàng đợi. Chúng tôi đã bảo hàng đợi nối tiếp thực hiện nối tiếp và nó đã làm ... nhưng chúng tôi chỉ giao cho nó một nhiệm vụ. Bây giờ hãy cho nó hai nhiệm vụ:
serialQueue.async {
for i in 1...5 {
print(i)
}
}
serialQueue.async {
for i in 1...5 {
print(i + 100)
}
}
1
2
3
4
5
101
102
103
104
105
Và đây là ví dụ cơ bản nhất (và duy nhất có thể) của tuần tự hóa - hai tác vụ chạy nối tiếp (lần lượt từng bước) trong nền (đến luồng chính) trong cùng một hàng đợi. Nhưng nếu chúng ta tạo cho chúng hai hàng đợi nối tiếp riêng biệt (vì trong ví dụ trên chúng là cùng một hàng đợi), đầu ra của chúng lại bị xáo trộn:
serialQueue.async {
for i in 1...5 {
print(i)
}
}
serialQueue2.async {
for i in 1...5 {
print(i + 100)
}
}
1
101
2
102
3
103
4
104
5
105
Và đây là ý của tôi khi tôi nói tất cả các hàng đợi đồng thời liên quan đến nhau. Đây là hai hàng đợi nối tiếp thực hiện các nhiệm vụ của chúng cùng một lúc (vì chúng là các hàng đợi riêng biệt). Một hàng đợi không biết hoặc quan tâm đến các hàng đợi khác. Bây giờ, hãy quay lại hai hàng đợi nối tiếp (của cùng một hàng đợi) và thêm hàng đợi thứ ba, một hàng đợi đồng thời:
serialQueue.async {
for i in 1...5 {
print(i)
}
}
serialQueue.async {
for i in 1...5 {
print(i + 100)
}
}
concurrentQueue.async {
for i in 1...5 {
print(i + 1000)
}
}
1
2
3
4
5
101
102
103
104
105
1001
1002
1003
1004
1005
Điều đó thật bất ngờ, tại sao hàng đợi đồng thời lại đợi hàng đợi nối tiếp kết thúc trước khi nó được thực thi? Đó không phải là sự tương tranh. Sân chơi của bạn có thể hiển thị một đầu ra khác nhau nhưng tôi đã cho thấy điều này. Và nó đã cho thấy điều này bởi vì mức độ ưu tiên của hàng đợi đồng thời của tôi không đủ cao để GCD thực hiện nhiệm vụ sớm hơn. Vì vậy, nếu tôi giữ mọi thứ giống nhau nhưng thay đổi QoS của hàng đợi toàn cầu (chất lượng dịch vụ của nó, đơn giản là mức độ ưu tiên của hàng đợi) let concurrentQueue = DispatchQueue.global(qos: .userInteractive)
, thì đầu ra sẽ như mong đợi:
1
1001
1002
1003
2
1004
1005
3
4
5
101
102
103
104
105
Hai hàng đợi nối tiếp thực hiện các nhiệm vụ của chúng theo nối tiếp (như mong đợi) và hàng đợi đồng thời thực hiện nhiệm vụ nhanh hơn vì nó được cấp mức ưu tiên cao (QoS cao hoặc chất lượng dịch vụ).
Hai hàng đợi đồng thời, như trong ví dụ in đầu tiên của chúng tôi, hiển thị một bản in lộn xộn (như mong đợi). Để khiến chúng được in gọn gàng nối tiếp, chúng ta sẽ phải làm cho cả hai cùng một hàng đợi nối tiếp (cùng một ví dụ của hàng đợi đó, không chỉ là cùng một nhãn) . Sau đó, mỗi nhiệm vụ được thực hiện nối tiếp với nhau. Tuy nhiên, một cách khác để khiến chúng được in nối tiếp là giữ cho cả hai đồng thời nhưng thay đổi phương thức điều phối của chúng:
concurrentQueue.sync {
for i in 1...5 {
print(i)
}
}
concurrentQueue.async {
for i in 1...5 {
print(i + 100)
}
}
1
2
3
4
5
101
102
103
104
105
Hãy nhớ rằng, việc gửi đồng bộ chỉ có nghĩa là luồng cuộc gọi chờ cho đến khi tác vụ trong hàng đợi được hoàn thành trước khi tiếp tục. Rõ ràng, sự cảnh báo ở đây là luồng cuộc gọi bị đóng băng cho đến khi tác vụ đầu tiên hoàn thành, có thể hoặc không thể là cách bạn muốn UI thực hiện.
Và chính vì lý do này mà chúng ta không thể làm như sau:
DispatchQueue.main.sync { ... }
Đây là sự kết hợp duy nhất có thể có của các hàng đợi và các phương thức điều phối mà chúng ta không thể thực hiện việc gửi đồng bộ hóa trên hàng đợi chính. Và đó là bởi vì chúng tôi đang yêu cầu hàng đợi chính đóng băng cho đến khi chúng tôi thực hiện nhiệm vụ trong các dấu ngoặc nhọn ... mà chúng tôi đã gửi đến hàng đợi chính, thứ mà chúng tôi vừa đóng băng. Điều này được gọi là bế tắc. Để xem nó trong hành động trong một sân chơi:
DispatchQueue.main.sync { // stop the main queue and wait for the following to finish
print("hello world") // this will never execute on the main queue because we just stopped it
}
// deadlock
Một điều cuối cùng cần đề cập là tài nguyên. Khi chúng tôi đưa ra một hàng đợi một nhiệm vụ, GCD tìm thấy một hàng đợi có sẵn từ nhóm được quản lý nội bộ của nó. Theo như cách viết của câu trả lời này, có 64 hàng đợi có sẵn trên mỗi qos. Điều đó có vẻ như rất nhiều nhưng chúng có thể nhanh chóng được tiêu thụ, đặc biệt là các thư viện của bên thứ ba, đặc biệt là các khung cơ sở dữ liệu. Vì lý do này, Apple có các khuyến nghị về quản lý hàng đợi (được đề cập trong các liên kết bên dưới); một là:
Thay vì tạo các hàng đợi đồng thời riêng tư, hãy gửi các tác vụ đến một trong các hàng đợi gửi đồng thời toàn cầu. Đối với các tác vụ nối tiếp, hãy đặt mục tiêu của hàng đợi nối tiếp của bạn thành một trong các hàng đợi đồng thời toàn cầu.
Bằng cách đó, bạn có thể duy trì hành vi nối tiếp của hàng đợi trong khi giảm thiểu số lượng hàng đợi riêng biệt tạo chủ đề.
Để làm điều này, thay vì tạo chúng như chúng ta đã làm trước đây (mà bạn vẫn có thể), Apple khuyên bạn nên tạo hàng đợi nối tiếp như thế này:
let serialQueue = DispatchQueue(label: "serialQueue", qos: .default, attributes: [], autoreleaseFrequency: .inherit, target: .global(qos: .default))
Để đọc thêm, tôi khuyên bạn nên như sau:
https://developer.apple.com/lvern/archive/documentation/General/Con chấpual
https://developer.apple.com/documentation/dispatch/dispatchqueue