不要轻易开始穿线! 比赛条件可能是一个让人头疼的问题。特别是如果您没有很多线程经验! (您已被警告:这里有龙!毛茸茸的、不确定的、无法可靠复制的龙!)
你知道什么是死锁吗?活锁怎么样?
那就是说...
正如ckarmann 和其他人已经建议的那样: 使用工作队列模型。每个 CPU 内核一个线程。 将工作分成 N 个块。使块相当大,就像许多行一样。当每个线程空闲时,它会从队列中取出下一个工作块。
在最简单的IDEAL版本中,您有 N 个内核、N 个线程和问题的 N 个子部分,每个线程从一开始就知道它要做什么。
但由于启动/停止线程的开销,这在实践中通常不会发生。您真的希望线程已经生成并等待操作。(例如通过信号量。)
工作队列模型本身非常强大。它使您可以并行化诸如快速排序之类的事情,这通常不会在 N 个线程/内核之间优雅地并行化。
线程多于内核?你只是在浪费开销。每个线程都有开销。即使在#threads=#cores 下,您也永远无法获得完美的 Nx 加速因子。
每行一个线程将非常低效!每个像素一个线程?我什至不想考虑它。(在使用矢量化处理器单元时,这种逐像素方法更有意义,就像他们在旧 Cray 上使用的那样。但不是线程!)
图书馆?你的平台是什么?在 Unix/Linux/g++ 下,我建议使用 pthreads 和信号量。(Pthreads 也可以在带有 microsoft 兼容层的 windows 下使用。但是,uhgg。我不太相信它!Cygwin 在那里可能是一个更好的选择。)
在 Unix/Linux 下,man:
* pthread_create, pthread_detach.
* pthread_mutexattr_init, pthread_mutexattr_settype, pthread_mutex_init,
* pthread_mutexattr_destroy, pthread_mutex_destroy, pthread_mutex_lock,
* pthread_mutex_trylock, pthread_mutex_unlock, pthread_mutex_timedlock.
* sem_init, sem_destroy, sem_post, sem_wait, sem_trywait, sem_timedwait.
有些人喜欢 pthread 的条件变量。但我总是更喜欢 POSIX 1003.1b 信号量。他们处理您想要在另一个线程开始等待更好之前发出信号的情况。或者在另一个线程多次发出信号的地方。
哦,帮自己一个忙:将您的线程/互斥量/信号量 pthread 调用包装到几个 C++ 类中。这将简化很多事情!
我需要锁定我的只读和只写数组吗?
这取决于您的精确硬件和软件。通常只读数组可以在线程之间自由共享。但有些情况并非如此。
写法大同小异。通常,只要只有一个线程正在写入每个特定的内存点,就可以了。但有些情况并非如此!
写作比阅读更麻烦,因为您可能会陷入这些奇怪的围栏情况。内存通常写成字而不是字节。当一个线程写入单词的一部分,而另一个线程写入不同的部分时,取决于哪个线程在什么时候做什么的确切时间(例如不确定性),您可能会得到一些非常不可预测的结果!
我会谨慎行事:为每个线程提供其自己的读写区域副本。完成后,将数据复制回来。当然,都在互斥锁下。
除非您谈论的是千兆字节的数据,否则内存块非常快。那几微秒的性能时间根本不值得调试噩梦。
如果您要使用互斥锁在线程之间共享一个公共数据区域,那么冲突/等待互斥锁的低效率会堆积起来并破坏您的效率!
看,干净的数据边界是好的多线程代码的本质。当你的界限不明确时,那就是你遇到麻烦的时候。
同样,保持边界上的所有内容互斥也很重要!并保持互斥区域短!
尽量避免同时锁定多个互斥锁。如果您确实锁定了多个互斥体,请始终以相同的顺序锁定它们!
尽可能使用 ERROR-CHECKING 或 RECURSIVE 互斥锁。FAST 互斥锁只是自找麻烦,实际(测量)速度增益很少。
如果遇到死锁情况,请在 gdb 中运行它,按 ctrl-c,访问每个线程并回溯。通过这种方式,您可以很快找到问题。(活锁更难!)
最后一个建议:构建它单线程,然后开始优化。在单核系统上,您可能会发现自己从诸如 foo[i++]=bar ==> *(foo++)=bar 之类的东西中获得的速度比从线程中获得的速度更快。
附录: 我所说的保持互斥区域短于上方是什么意思?考虑两个线程:(给定一个 Mutex 类的全局共享互斥对象。)
/*ThreadA:*/ while(1){ mutex.lock(); printf("a\n"); usleep(100000); mutex.unlock(); }
/*ThreadB:*/ while(1){ mutex.lock(); printf("b\n"); usleep(100000); mutex.unlock(); }
会发生什么?
在我的 Linux 版本下,一个线程将连续运行,而另一个将饿死。当 mutex.unlock() 和 mutex.lock() 之间发生上下文交换时,它们很少会改变位置。
附录: 在您的情况下,这不太可能成为问题。但是对于其他问题,人们可能事先不知道完成一个特定的工作块需要多长时间。将问题分解为 100 个部分(而不是 4 个部分)并使用工作队列将其拆分为 4 个核心可以消除此类差异。
如果一个工作块的完成时间是另一个工作块的 5 倍,那么最终一切都会平息。虽然有太多的块,但获取新工作块的开销会造成明显的延迟。这是针对特定问题的平衡行为。