Đa luồng: tôi đang làm sai?


23

Tôi đang làm việc trên một ứng dụng phát nhạc.

Trong quá trình phát lại, thường thì mọi thứ cần phải xảy ra trên các luồng riêng biệt vì chúng cần xảy ra đồng thời. Ví dụ, các nốt của hợp âm cần được nghe cùng nhau, vì vậy mỗi nốt được gán một luồng riêng để chơi. (Chỉnh sửa để làm rõ: gọi note.play()đóng băng chuỗi cho đến khi chơi nốt và đây là lý do tại sao tôi cần ba các chủ đề riêng biệt để có ba ghi chú nghe cùng một lúc.)

Loại hành vi này tạo ra nhiều chủ đề trong khi phát lại một bản nhạc.

Ví dụ, hãy xem xét một bản nhạc có giai điệu ngắn và tiến trình hợp âm ngắn đi kèm. Toàn bộ giai điệu có thể được chơi trên một chuỗi, nhưng tiến trình cần ba luồng để chơi, vì mỗi hợp âm của nó có ba nốt.

Vì vậy, mã giả để chơi một tiến trình trông như thế này:

void playProgression(Progression prog){
    for(Chord chord : prog)
        for(Note note : chord)
            runOnNewThread( func(){ note.play(); } );
}

Vì vậy, giả sử tiến trình có 4 hợp âm và chúng tôi chơi nó hai lần, hơn là chúng tôi đang mở 3 notes * 4 chords * 2 times= 24 luồng. Và đây chỉ là để chơi một lần.

Trên thực tế, nó hoạt động tốt trong thực tế. Tôi không nhận thấy bất kỳ độ trễ đáng chú ý nào, hoặc lỗi phát sinh từ việc này.

Nhưng tôi muốn hỏi liệu đây có phải là thực hành đúng hay không, hoặc tôi đang làm gì đó sai về cơ bản. Có hợp lý để tạo ra nhiều chủ đề mỗi khi người dùng nhấn nút không? Nếu không, làm thế nào tôi có thể làm điều đó khác nhau?


14
Có lẽ bạn nên xem xét việc trộn âm thanh thay thế? Tôi không biết những gì khuôn khổ bạn đang sử dụng nhưng đây là một ví dụ: wiki.libsdl.org/SDL_MixAudioFormat Hoặc, bạn có thể tận dụng các kênh truyền hình: libsdl.org/projects/SDL_mixer/docs/SDL_mixer_25.html#SEC25
Rufflewind

5
Is it reasonable to create so many threads...phụ thuộc vào mô hình luồng của ngôn ngữ. Các luồng được sử dụng cho song song thường được xử lý ở cấp HĐH để HĐH có thể ánh xạ chúng tới nhiều lõi. Chủ đề như vậy là tốn kém để tạo và chuyển đổi giữa. Các luồng cho đồng thời (xen kẽ hai tác vụ, không nhất thiết phải thực thi đồng thời cả hai) có thể được triển khai ở cấp độ ngôn ngữ / VM và có thể được thực hiện rất "rẻ" để sản xuất và chuyển đổi giữa để bạn có thể nói, nói chuyện với 10 ổ cắm mạng ít nhiều đồng thời, nhưng bạn sẽ không nhất thiết phải có thêm thông lượng CPU theo cách đó.
Doval

3
Điều đó sang một bên, những người khác chắc chắn đúng về các chủ đề là cách sai để xử lý nhiều âm thanh cùng một lúc.
Doval

3
Bạn có quen thuộc với cách sóng âm hoạt động không? Thông thường, bạn tạo hợp âm bằng cách kết hợp các giá trị của hai sóng âm thanh (được chỉ định ở cùng tốc độ bit) với nhau thành một sóng âm thanh mới. Sóng phức tạp có thể được xây dựng từ những cái đơn giản; bạn chỉ yêu cầu một dạng sóng duy nhất để phát một bài hát.
KChaloux

Vì bạn nói rằng note.play () không đồng bộ, nên một luồng cho mỗi note.play () là cách tiếp cận để phát nhiều ghi chú cùng một lúc. KHÔNG GIỚI HẠN .., bạn có thể kết hợp các ghi chú đó thành một ghi chú mà sau đó bạn chơi trên một chuỗi. Nếu điều đó là không thể thì với cách tiếp cận của bạn, bạn sẽ phải sử dụng một số cơ chế để đảm bảo chúng vẫn đồng bộ
pnizzle

Câu trả lời:


46

Một giả định bạn đang thực hiện có thể không hợp lệ: bạn yêu cầu (trong số những thứ khác) mà các luồng của bạn thực thi đồng thời. Có thể làm việc cho 3, nhưng tại một số điểm, hệ thống sẽ cần ưu tiên các luồng nào sẽ chạy trước và chuỗi nào chờ.

Việc triển khai của bạn cuối cùng sẽ phụ thuộc vào API của bạn, nhưng hầu hết các API hiện đại sẽ cho phép bạn biết trước những gì bạn muốn chơi và quan tâm đến thời gian và tự xếp hàng. Nếu bạn tự viết mã API như vậy, bỏ qua bất kỳ API hệ thống hiện có nào (tại sao bạn?!), Một hàng đợi sự kiện trộn các ghi chú của bạn và phát chúng từ một luồng duy nhất trông giống như một cách tiếp cận tốt hơn so với mô hình trên mỗi luồng ghi chú.


36
Hoặc để nói khác đi - hệ thống sẽ không và không thể đảm bảo thứ tự, trình tự hoặc thời lượng của bất kỳ luồng nào sau khi bắt đầu.
James Anderson

2
@JamesAnderson ngoại trừ nếu một người sẽ thực hiện những nỗ lực lớn trong việc đồng bộ hóa, điều này sẽ kết thúc ở việc gần như hoàn toàn cuối cùng một lần nữa.
Đánh dấu

Theo 'API', ý bạn là thư viện âm thanh tôi đang sử dụng?
Manila Cohn

1
@Prog Có. Tôi khá chắc chắn rằng nó có một cái gì đó thuận tiện hơn mà note.play ()
ptyx

26

Đừng dựa vào các chủ đề thực hiện trong bước chân. Mọi hệ điều hành tôi biết đều không đảm bảo rằng các luồng thực thi đúng lúc với nhau. Điều này có nghĩa là nếu CPU chạy 10 luồng, chúng không nhất thiết phải nhận thời gian bằng nhau trong bất kỳ giây nào. Họ có thể nhanh chóng thoát khỏi sự đồng bộ, hoặc họ có thể hoàn toàn đồng bộ. Đó là bản chất của các luồng: không có gì được đảm bảo, vì hành vi thực hiện của chúng là không xác định .

Đối với ứng dụng cụ thể này, tôi tin rằng bạn cần phải có một chủ đề duy nhất tiêu thụ các ghi chú âm nhạc. Trước khi nó có thể tiêu thụ các ghi chú, một số quy trình khác cần hợp nhất các nhạc cụ, nhân viên, bất cứ thứ gì vào một bản nhạc duy nhất .

Hãy để chúng tôi giả sử rằng bạn có ví dụ như ba chủ đề sản xuất ghi chú. Tôi sẽ đồng bộ hóa chúng trên một cấu trúc dữ liệu duy nhất nơi tất cả chúng có thể đặt nhạc của chúng. Một chủ đề khác (người tiêu dùng) đọc các ghi chú kết hợp và chơi chúng. Có thể cần phải có một độ trễ ngắn ở đây để đảm bảo rằng thời gian của luồng không làm mất các ghi chú.

Đọc liên quan: vấn đề nhà sản xuất-người tiêu dùng .


1
Cảm ơn đã trả lời. Tôi không chắc chắn 100% tôi hiểu bạn trả lời, nhưng tôi muốn chắc chắn rằng bạn hiểu tại sao tôi cần 3 chủ đề để phát 3 ghi chú cùng một lúc: đó là vì gọi note.play()đóng băng chủ đề cho đến khi ghi chú xong. Vì vậy, để tôi có thể có play()3 ghi chú cùng một lúc, tôi cần 3 chủ đề khác nhau để làm điều này. Có giải pháp của bạn giải quyết tình hình của tôi?
Manila Cohn

Không, và điều đó không rõ ràng từ câu hỏi. Không có cách nào cho một chủ đề để chơi một hợp âm?

4

Một cách tiếp cận cổ điển cho vấn đề âm nhạc này sẽ là sử dụng chính xác hai luồng. Một, một luồng ưu tiên thấp hơn sẽ xử lý UI hoặc mã tạo âm thanh như

void playProgression(Progression prog){
    for(Chord chord : prog)
        for(Note note : chord)
            otherthread.startPlaying(note);
}

(lưu ý khái niệm chỉ bắt đầu ghi chú không đồng bộ và tiếp tục mà không đợi nó kết thúc)

và thứ hai, chuỗi thời gian thực sẽ liên tục nhìn vào tất cả các ghi chú, âm thanh, mẫu, v.v. nên chơi ngay bây giờ; trộn chúng và tạo ra dạng sóng cuối cùng. Phần này có thể (và thường là) được lấy từ thư viện bên thứ ba được đánh bóng.

Chủ đề đó thường rất nhạy cảm với việc "bỏ đói" tài nguyên - nếu bất kỳ quá trình xử lý âm thanh thực tế nào bị chặn lâu hơn đầu ra bộ đệm âm thanh của bạn, thì nó sẽ gây ra các tạo tác âm thanh như bị gián đoạn hoặc bật. Nếu bạn có 24 luồng phát trực tiếp âm thanh, thì bạn có khả năng cao hơn một trong số chúng sẽ bị vấp ở một số điểm. Điều này thường được coi là không thể chấp nhận được, vì con người khá nhạy cảm với các trục trặc âm thanh (nhiều hơn so với các tạo tác trực quan) và nhận thấy ngay cả nói lắp nhỏ.


1
OP cho biết API chỉ có thể phát một ghi chú cùng một lúc
Mooing Duck

4
@MooingDuck nếu API có thể trộn, thì nó nên trộn; nếu OP nói rằng API không thể trộn thì giải pháp là trộn mã của bạn và để luồng khác thực hiện my_mixed_notes_from_whole_chord_proTHERion.play () thông qua api.
Peteris

1

Vâng, vâng, bạn đang làm điều gì đó sai.

Điều đầu tiên là tạo Chủ đề rất tốn kém. Nó có nhiều chi phí hơn là chỉ gọi một chức năng.

Vì vậy, những gì bạn nên làm, nếu bạn cần nhiều luồng cho công việc này là tái chế các luồng.

Theo tôi, bạn có một luồng chính thực hiện lập lịch cho các luồng khác. Vì vậy, chủ đề chính dẫn qua âm nhạc và bắt đầu các chủ đề mới bất cứ khi nào có một ghi chú mới được phát. Vì vậy, thay vì để các luồng chết và khởi động lại chúng, tốt hơn là giữ các luồng khác tồn tại trong một vòng lặp ngủ, nơi chúng kiểm tra mỗi x mili giây (hoặc nano giây) nếu có một ghi chú mới được phát và nếu không thì ngủ. Chủ đề chính sau đó không bắt đầu các chủ đề mới mà chỉ cho nhóm chủ đề hiện có để phát các ghi chú. Chỉ khi không có đủ chủ đề trong nhóm, nó mới có thể tạo chủ đề mới.

Việc tiếp theo là đồng bộ hóa. Hầu như không có hệ thống đa luồng hiện đại nào thực sự đảm bảo rằng tất cả các luồng được thực thi cùng một lúc. Nếu bạn có nhiều luồng và tiến trình chạy trên máy hơn lõi (phần lớn là như vậy), thì các luồng không nhận được 100% thời gian của CPU. Họ phải chia sẻ CPU. Điều này có nghĩa là mọi luồng đều nhận được một lượng nhỏ thời gian CPU và sau đó sau khi chia sẻ, luồng tiếp theo sẽ lấy CPU trong một thời gian ngắn. Hệ thống không đảm bảo rằng luồng của bạn có cùng thời gian CPU với các luồng khác. Điều này có nghĩa, một luồng có thể đang chờ một luồng khác kết thúc và do đó bị trì hoãn.

Bạn nên có một cái nhìn nếu không thể phát nhiều ghi chú trên một luồng, để luồng đó chuẩn bị tất cả các ghi chú và sau đó chỉ đưa ra lệnh "bắt đầu".

Nếu bạn cần làm điều đó với các chủ đề, ít nhất là sử dụng lại các chủ đề. Sau đó, bạn không cần phải có nhiều chủ đề như có nhiều ghi chú trong toàn bộ, nhưng chỉ cần nhiều chủ đề như số lượng ghi chú tối đa được phát cùng một lúc.


"[Có thể] phát nhiều ghi chú trên một luồng, để luồng đó chuẩn bị tất cả các ghi chú và sau đó chỉ đưa ra lệnh" bắt đầu "." - Đây là suy nghĩ đầu tiên của tôi là tốt. Đôi khi tôi tự hỏi liệu có bị bắn phá với những lời bình luận về đa luồng (ví dụ: lập trình viên.stackexchange.com /questions / 43321 / trộm ) không khiến nhiều lập trình viên lạc lối trong quá trình thiết kế. Tôi nghi ngờ về bất kỳ quy trình lớn, trực tiếp nào kết thúc với việc đặt ra nhu cầu về một loạt các chủ đề. Tôi khuyên bạn nên tìm kiếm một giải pháp đơn luồng.
1172763

1

Câu trả lời thực tế là nếu nó hoạt động cho ứng dụng của bạn và đáp ứng các yêu cầu hiện tại của bạn thì bạn không làm sai :) Tuy nhiên nếu bạn muốn nói "đây có phải là một giải pháp có thể mở rộng, hiệu quả, có thể tái sử dụng" thì câu trả lời là không. Thiết kế đưa ra nhiều giả định về cách các luồng xử lý có thể đúng hoặc không đúng trong các tình huống khác nhau (giai điệu dài hơn, ghi chú đồng thời hơn, phần cứng khác nhau, v.v.).

Là một thay thế xem xét sử dụng một vòng lặp thời gian và một sợi hồ bơi . Vòng lặp thời gian chạy trong luồng riêng của nó và liên tục kiểm tra nếu một ghi chú cần được phát. Nó thực hiện điều này bằng cách so sánh thời gian hệ thống với thời gian mà giai điệu được bắt đầu. Thời gian của mỗi nốt thường có thể được tính rất dễ dàng từ nhịp độ của giai điệu và chuỗi các nốt. Khi một ghi chú mới cần được phát, hãy mượn một luồng từ nhóm luồng và gọi play()hàm trên ghi chú. Vòng lặp thời gian có thể hoạt động theo nhiều cách khác nhau nhưng đơn giản nhất là vòng lặp liên tục với thời gian ngủ ngắn (có thể là giữa các hợp âm) để giảm thiểu việc sử dụng CPU.

Một lợi thế của thiết kế này là số lượng luồng sẽ không vượt quá số lượng ghi chú đồng thời tối đa + 1 (vòng lặp thời gian). Ngoài ra, vòng lặp thời gian bảo vệ bạn chống lại sự trượt trong thời gian có thể gây ra bởi độ trễ của luồng. Thứ ba, nhịp độ của giai điệu không cố định và có thể được thay đổi bằng cách thay đổi cách tính thời gian.

Bên cạnh đó, tôi đồng ý với các nhà bình luận khác rằng chức năng note.play()này là một API rất kém để làm việc. Bất kỳ API âm thanh hợp lý nào cũng sẽ cho phép bạn trộn và lên lịch ghi chú theo cách linh hoạt hơn nhiều. Điều đó nói rằng, đôi khi chúng ta phải sống với những gì chúng ta có :)


0

Có vẻ tốt đối với tôi như là một triển khai đơn giản, giả sử đó là API bạn phải sử dụng . Các câu trả lời khác giải thích tại sao đây không phải là một API rất tốt, vì vậy tôi không nói về điều đó nhiều hơn, tôi chỉ cho rằng đó là những gì bạn phải sống cùng. Cách tiếp cận của bạn sẽ sử dụng một số lượng lớn các luồng, nhưng trên một PC hiện đại không có gì đáng lo ngại, miễn là số lượng luồng là hàng chục.

Một điều bạn nên làm, nếu khả thi (như, phát từ một tệp thay vì từ người dùng nhấn bàn phím), là thêm một số độ trễ. Vì vậy, bạn bắt đầu một chuỗi, nó ngủ cho đến thời gian cụ thể của đồng hồ hệ thống và bắt đầu phát một ghi chú vào đúng thời điểm (lưu ý, đôi khi giấc ngủ có thể bị gián đoạn sớm, vì vậy hãy kiểm tra đồng hồ và ngủ nhiều hơn nếu cần). Mặc dù không có gì đảm bảo rằng HĐH sẽ tiếp tục luồng chính xác khi giấc ngủ của bạn kết thúc (trừ khi bạn sử dụng hệ điều hành thời gian thực), rất có thể chính xác hơn nhiều so với việc bạn chỉ bắt đầu luồng và bắt đầu chơi mà không kiểm tra thời gian .

Sau đó, bước tiếp theo, làm phức tạp mọi thứ một chút nhưng không quá nhiều, và sẽ cho phép bạn giảm độ trễ được đề cập ở trên, sử dụng nhóm luồng. Đó là, khi một chủ đề hoàn thành một ghi chú, nó không thoát ra, mà thay vào đó, chờ một ghi chú mới để chơi. Và khi bạn yêu cầu bắt đầu phát một ghi chú, trước tiên bạn hãy thử lấy một chuỗi miễn phí từ nhóm và chỉ thêm một luồng mới nếu cần. Điều này sẽ yêu cầu một số giao tiếp liên chủ đề đơn giản, tất nhiên, thay vì cách tiếp cận lửa và quên hiện tại của bạn.

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.