多线程:我做错了吗?


23

我正在开发一个播放音乐的应用程序。

在回放期间,通常情况需要在单独的线程上发生,因为它们需要同时发生。例如,一个和弦需要的音符一起被听到,所以每个人都分配了其自己的线程中进行播放(编辑澄清:调用note.play()冻结线程,直到音符播放完毕,这就是为什么我需要三个独立线程以同时听到三个音符。)

这种行为在播放音乐时会创建许多线程。

例如,考虑一段音乐,该音乐具有短旋律和较短的伴随和弦进行。整个旋律可以在单个线程上弹奏,但是该进程需要三个线程才能弹奏,因为其每个和弦都包含三个音符。

因此,用于播放进度的伪代码如下所示:

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

因此,假设进程有4个和弦,并且我们演奏了两次,则我们打开3 notes * 4 chords * 2 times= 24个线程。这只是播放一次。

实际上,它在实践中效果很好。我没有注意到任何明显的延迟或由此导致的错误。

但是我想问一下这是正确的做法,还是我做的事情根本上是错误的。每次用户按下按钮时创建这么多线程是否合理?如果没有,我该怎么做?


14
也许您应该考虑混合音频?我不知道您使用的框架是什么,但这是一个示例:wiki.libsdl.org/SDL_MixAudioFormat 或者,您可以利用以下渠道:libsdl.org/projects/SDL_mixer/docs/SDL_mixer_25.html#SEC25
Rufflewind

5
Is it reasonable to create so many threads...取决于语言的线程模型。用于并行处理的线程通常在操作系统级别处理,因此操作系统可以将它们映射到多个内核。创建和在这些线程之间进行切换非常昂贵。并发线程(交织两个任务,不必同时执行两个任务)可以在语言/ VM级别上实现,并且可以非常“便宜”地进行生产和切换,因此您可以说与10个网络套接字进行通信同时进行,但不一定以这种方式获得更多的CPU吞吐量。
2014年

3
除此之外,其他人肯定是正确的,因为线程是一次处理多个声音的错误方法。
2014年

3
您对声波的工作原理非常熟悉吗?通常,您可以通过将两个声波(以相同比特率指定)一起合成一个新的声波来制作和弦。复杂的波浪可以由简单的波浪建立。您只需要一个波形即可播放歌曲。
KChaloux

由于您说note.play()不是异步的,因此每个note.play()的线程都是一种同时播放多个音符的方法。除非..,您可以将这些音符组合成一个音符,然后在单个线程上播放。如果这不可能,那么采用您的方法,您将必须使用某种机制来确保它们保持同步
pnizzle 2014年

Answers:


46

您所做的一种假设可能无效:除其他事项外,您还要求线程同时执行。可能适用于3,但是在某个时候,系统将需要优先确定首先运行哪些线程,以及等待哪个线程。

您的实现最终将取决于您的API,但是大多数现代的API都可以让您提前告知您要播放的内容,并注意安排时间和排队的时间。如果您要自己编写这样的API,而忽略任何现有的系统API(为什么?!),则事件队列混合您的笔记并从单个线程播放它们看起来比每个笔记模型的线程更好。


36
或者换种说法-系统将不会也无法保证任何线程一旦启动后的顺序,顺序或持续时间。
James Anderson

2
@JamesAnderson,除非一个人要在同步中付出巨大的努力,否则最终结果将几乎是偶然的。
2014年

“ API”是指我正在使用的音频库?
阿维夫·科恩

1
@Prog是的。我很确定note.play()
ptyx

26

不要依赖锁步执行的线程。我所知道的每个操作系统都不能保证线程在时间上彼此一致地执行。这意味着,如果CPU运行10个线程,则它们不一定在任何给定的秒中都收到相等的时间。他们可能会很快失去同步,或者可能会保持完美同步。那就是线程的本质:没有任何保证,因为它们执行的行为是不确定的

对于这个特定的应用程序,我相信你需要有一个单独的线程消耗的音符。在消耗音符之前,其他一些过程需要将乐器,五线谱以及任何东西合并为一首音乐

让我们假设您有例如三个产生笔记的线程。我会将它们同步到一个可以放置音乐的单一数据结构上。另一个线程(消费者)读取组合的音符并播放它们。此处可能需要短暂延迟,以确保线程计时不会导致笔记丢失。

相关阅读:生产者-消费者问题


1
谢谢回答。我不是100%肯定我理解您的回答,但我想确保您了解为什么我需要3个线程同时演奏3个音符:这是因为调用会note.play()冻结线程,直到音符完成播放为止。因此,为了使我能够同时进行play()3个笔记,我需要3个不同的线程来执行此操作。您的解决方案能解决我的情况吗?
阿维夫·科恩

不,问题尚不清楚。线程无法演奏和弦吗?

4

解决此音乐问题的经典方法是恰好使用两个线程。一种,优先级较低的线程将处理UI或声音生成代码,例如

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

(请注意仅异步启动便笺而不等待其完成的概念)

第二,实时线程将始终查看现在应该播放的所有音符,声音,样本等。混合它们并输出最终波形。这部分可能(通常是)从经过修饰的第三方库中获取。

该线程通常对资源的“资源匮乏”非常敏感-如果任何实际的声音输出处理被阻塞的时间长于声卡缓冲的输出,那么它将导致可听见的伪影,例如中断或爆裂声。如果您有24个直接输出音频的线程,那么您中有一个线程将在某些时候口吃的机会更大。这通常被认为是不可接受的,因为人类对音频毛刺非常敏感(远远超过视觉伪像),甚至会发现很小的口吃。


1
OP表示,API只能同时播放一个音符
鸣叫鸭

4
@MooingDuck如果API可以混合使用,则应该混合使用;如果操作员说API无法混合,那么解决方案是混合代码,并让其他线程通过api执行my_mixed_notes_from_whole_chord_progression.play()。
Peteris 2014年

1

好吧,是的,您做错了什么。

第一件事是创建线程很昂贵。它比仅调用函数有更多的开销。

因此,如果您需要多个线程来执行此工作,则应该回收这些线程。

在我看来,您有一个主线程负责其他线程的调度。因此,只要有新音符要演奏,主线程就会引导音乐并开始新的线程。因此,与其让线程死掉并重新启动它们,不如让其他线程在睡眠循环中保持活动状态,在该循环中,它们每隔x毫秒(或纳秒)检查一次是否有新音符要播放,否则就进入睡眠状态。然后,主线程不会启动新线程,而是告诉现有线程池弹奏音符。只有在池中没有足够的线程时,它才能创建新线程。

下一个是同步。实际上,几乎没有任何现代多线程系统真正保证所有线程都在同一时间执行。如果您在计算机上运行的线程和进程多于内核(大多数情况下),那么这些线程不会获得100%的CPU时间。他们必须共享CPU。这意味着每个线程都会占用少量CPU时间,然后在共享之后,下一个线程会在较短时间内获取CPU。系统不保证您的线程获得与其他线程相同的CPU时间。这意味着,一个线程可能正在等待另一个线程完成,因此被延迟。

如果不能在一个线程上播放多个音符,则应该查看一下,以便该线程准备所有音符,然后仅给出“开始”命令。

如果您需要对线程进行处理,请至少重用线程。这样一来,您就不需要具有与整个乐谱中所有音符一样多的线程,而只需要与同时演奏的最大音符数一样多的线程。


“ [是否]可以在一个线程上播放多个音符,以便该线程准备所有音符,然后仅给出“开始”命令。-这也是我的第一个想法。我有时想,如果有关于多线程的评论(如轰炸programmers.stackexchange.com/questions/43321/...)的程序员设计过程中误入歧途不会导致很多。我对任何大型的,前期的过程最终会导致需要大量线程的过程表示怀疑。我建议您专心寻找单线程解决方案。
user1172763

1

务实的答案是,如果它适用于您的应用程序并满足您当前的要求,那么您就不会做错:) 但是,如果您的意思是“这是一个可扩展,高效,可重用的解决方案”,那么答案是否定的。该设计对线程的行为做出了许多假设,这些假设在不同情况下可能正确,也可能不正确(更长的旋律,更多同时出现的音符,不同的硬件等)。

作为替代方案,请考虑使用定时循环和线程。定时循环在其自己的线程中运行,并不断检查是否需要演奏音符。它通过将系统时间与旋律开始的时间进行比较来实现此目的。通常可以从旋律的节奏和音符序列中很容易地计算出每个音符的时机。当需要播放新音符时,请从线程池中借用一个线程并play()在该音符上调用该函数。定时循环可以以多种方式工作,但最简单的是连续循环,睡眠时间短(可能在和弦之间),以最大程度地减少CPU使用率。

这种设计的一个优点是线程数不会超过同时发音符的最大数目+ 1(定时循环)。定时循环还可以保护您避免由于线程延迟而导致的定时延迟。第三,旋律的节奏不是固定的,可以通过改变定时的计算来改变。

顺便说note.play()一句,我同意其他评论者的意见,即该函数是一个非常差劲的API。任何合理的声音API都可以让您以更加灵活的方式来混合和安排笔记。就是说,有时候我们必须忍受我们拥有的东西:)


0

在我看来,这是一个简单的实现,假设您必须使用。其他答案涵盖了为什么这不是一个很好的API,因此我不再赘述,我只是假设这是您必须忍受的。您的方法将使用大量线程,但是在现代PC上,只要线程数不超过几十个,就不必担心。

如果可行,您应该做的一件事(例如,从文件中播放而不是由用户弹奏键盘)是增加一些延迟。因此,您启动一​​个线程,它会一直休眠直到系统时钟的特定时间,然后在适当的时间开始播放音符(注意,有时睡眠可能会提前中断,因此请检查时钟并在需要时增加睡眠时间)。尽管不能保证OS会在您的睡眠结束时准确地继续执行线程(除非您使用实时操作系统),但比起只启动线程并在不检查时间的情况下开始播放,它可能会更加准确。 。

然后,下一步是使用线程池,该步骤使事情稍微复杂但又不过分,并且可以减少上述延迟。也就是说,当一个线程完成一个音符时,它不会退出,而是等待新的音符播放。当您请求开始演奏音符时,您首先尝试从池中获取一个空闲线程,并仅在需要时添加一个新线程。当然,这将需要一些简单的线程间通信,而不是当前的“一劳永逸”的方法。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.