如何在C++中正确地进行线程编程?

4
我需要编写一个相当大的、动态的稀疏矩阵对象类,并且想要实现以下操作:一个线程处理将元素放入矩阵,另一个线程处理从矩阵中读取数据。
这两个线程唯一可能冲突的时候是它们同时想要访问同一行/列。因此,我决定为每行/列使用一个简单的互斥锁即可。
这是我第一次在C/C++中使用线程,我想按照规范来做。我有两个问题:
1. 如何生成这些线程?这更多是一个语言问题。 2. 如何尽可能高效地实现锁定?如果存在冲突,则请求线程将排队等待资源被释放。但是,如何实现唤醒呢?我可以使用循环轮询内存位置,但那不够优雅。理想情况下,我认为基于中断的方法最好。

7
C++ 没有内置的线程支持。你需要选择一个线程库来使用(如果想实现跨平台,可以尝试Boost的多线程库)。 - Tyler McHenry
Tyler说的没错。该库包括可以锁定的实例,当获取锁时会唤醒这些实例。所有内容都在其中。 - Steven Sudit
1
@*: 顺便说一下 -- C++0x 被吹嘘为具有内置线程支持的语言。 - dirkgently
@Tyler,由于现在大多数主流编译器都提供了即将到来的C++标准的元素,因此它们实际上已经面向大众。如果您能明确指出您所指的C++标准,那就太好了。正如dirkgently所提到的:http://www2.research.att.com/~bs/C++0xFAQ.html#std-thread http://www2.research.att.com/~bs/C++0xFAQ.html#std-thread - mloskot
11个回答

18

6

C++本身不提供任何线程支持。在Windows上,您可以使用CreateThread。在UNIX上,您可以使用POSIX线程(pthreads)。

通常情况下不需要实现自己的并发原语。例如,在Windows上,您可以使用CreateMutex创建互斥对象,并使用WaitForSingleObject等待直到它被释放。


这是正确的,但是建议他直接使用特定于系统的功能可能不会产生好的结果。 - Steven Sudit
1
Boost的回答已经有更多的赞同票了。我会让这个事实说明一切 :) - Thomas
@Steven Sudit:pthreads非常便携,因为P代表POSIX中的P,而POSIX又代表可移植操作系统接口。当我看过Boost.Thread时,它似乎遭受了LCD(最低公共分母)综合症(但这显然取决于一个人的需求,如果它能够满足你的需求,那就很好)。 - just somebody
@Steven SuditпјҡиҝҷеҸҜиғҪжҳҜжҲ‘们зҡ„иғҢжҷҜеҮәеҚ–дәҶжҲ‘们пјҢжҲ‘зҡ„зЎ®жҳҜгҖӮжҲ‘дё»иҰҒдёҺ{Free,Open}BSDе’ҢеҹәдәҺGNUе·Ҙе…·й“ҫе’ҢLinuxеҶ…ж ёпјҲдё»иҰҒжҳҜRedhatпјүзҡ„еҗ„з§Қж“ҚдҪңзі»з»ҹеңЁдёҚеҗҢзҡ„иҒҢдҪҚдёҠе·ҘдҪңпјҲеҢ…з»ҙжҠӨгҖҒзү№е®ҡдәҺж“ҚдҪңзі»з»ҹзҡ„е®һз”ЁзЁӢеәҸгҖҒзҪ‘з»ңжөҒйҮҸжӢҰжҲӘзӯүпјүгҖӮжҲ‘жңүеҫҲеӨҡзҗҶз”ұи®Өдёәд»»дҪ•дёӨдёӘжүҖи°“зҡ„вҖңLinuxеҸ‘иЎҢзүҲвҖқйғҪжҳҜдёҚеҗҢзҡ„ж“ҚдҪңзі»з»ҹгҖӮUnixдёҚжҳҜеҚ•дёҖзҡ„ж“ҚдҪңзі»з»ҹпјҢе…¶еҗ«д№үж—©е·Іжү©еӨ§/зЁҖйҮҠпјҢж¶өзӣ–дәҶд»ҠеӨ©дҪҝз”Ёзҡ„еӨ§еӨҡж•°йҖҡз”Ёж“ҚдҪңзі»з»ҹпјҲзҢңжөӢпјүгҖӮPOSIXпјҲеӯҗйӣҶпјүзҡ„иҰҶзӣ–иҢғеӣҙжӣҙе№ҝгҖӮ - just somebody
例如:随着苹果公司在OS X中采用“unix”,pthreads的可移植性是否更广泛,因为它现在适用于Mac电脑中使用的操作系统,还是与以前一样不可移植,因为没有了MacOS,只有另一个“unix”? - just somebody
显示剩余4条评论

3
首先,您不需要为每列和每行都使用互斥锁。如果您为一行获取了互斥锁,则锁定了该行中的所有单元格,因此无论访问哪个列都没有关系。或者,如果您每列获取一个锁,则锁定了该列中的所有单元格,无论哪一行。因此,您可以为整个表、每个单元格、每行或每列设置一个互斥锁。但是每行和每列各一个互斥锁是没有意义的。
大多数同步原语将阻止您的线程,并且当资源变得可用时,线程将简单地恢复,您不需要担心信号和唤醒。这部分正是同步对象(例如互斥锁或关键段)所为您完成的。
如何构建和使用同步原语的具体细节取决于特定的平台。正如其他人发布的那样,有一些跨平台库可供使用,但至少您必须指定您的目标平台,以便我们知道可用的库。

当你说“每列和每行不需要一个互斥锁”时,你确定这适用于稀疏矩阵吗?我理解你对普通矩阵的观点。 - Mike DeSimone
我觉得在这里可能很容易遇到死锁的问题。比如,一个线程获取了行并尝试获取列,而另一个线程则获取了列并尝试获取行。也许应该维护一种锁获取顺序。 - Steven Sudit
1
@Steven:每行和每列的锁定可能会导致死锁,如果顺序没有维护。我的观点是,没有必要每行和每列都进行锁定。如果需要写入单元格A2,则会锁定行“A”和列“2”。如果需要锁定单元格A3,则会锁定行A..并停止,因为整个行已经被锁定了。那么每列锁的意义是什么呢? - Remus Rusanu
1
@Mike:我指的是逻辑术语中的“锁定”,即我锁定了名为“A”的行,实际意义取决于实现方式(与行相关联的互斥量或与列表头相关联等),因此我认为稀疏/密集并没有区别。但是,如果访问协议是“锁定行,然后锁定列,然后访问单元格”,则无论实现方式如何,该协议都不需要两个锁。 - Remus Rusanu
@Remus:@Alex 正试图通过将虚拟数组[N*M]映射到数组[N+M](的某些内容)并允许别名错误(某种类型的)来创建互斥锁。乍一看,这似乎不是不可能,但行+列寻址使用AND操作而不是OR。 - Potatoswatter
显示剩余5条评论

1

我可以相对简单地回答第一部分 - 这取决于您的平台。如果您正在使用Win32 API,请查看http://msdn.microsoft.com/en-us/library/ms682453%28VS.85%29.aspx "CreateThread"函数并阅读示例。我在Windows上多线程阅读的书是这本:http://www.amazon.co.uk/Windows-PRO-Developer-Jeffrey-Wintellect-Christophe/dp/0735624240,它不仅涵盖了使用CreateThread和其他选项BeginThread进行线程处理,还包括锁定和信号量等。

如果您正在使用Linux,则需要通过pthread函数使用POSIX线程,请参见http://www.yolinux.com/TUTORIALS/LinuxTutorialPosixThreads.html作为示例。

一个关于pthreads的代码示例看起来像这样 - 注意,我留下了从同一函数创建多个线程的功能,即callocing pthread_t变量数组。你可能不需要这个。

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <malloc.h>

void *thread_function(void *arg) 
{
        /* DO SOME STUFF */

        /* Exit Thread */
    pthread_exit();
}

int main(int argc, char** argv)
{
        /* variables */
        int retval = 0;
        /* array of thread handles */
    pthread_t* thread_handle = (pthread_t*) calloc(1, sizeof(pthread_t));;

        /* create function - fork() for threads */
    retval = pthread_create(&thread_handle[0], NULL, thread_function, NULL);

        /* DO SOME STUFF */

        /* join - wait for thread to finish */ 
        pthread_join(thread_handle[0], NULL); 

    return EXIT_SUCCESS;
}

使用gcc 文件名 -o 可执行文件名 -lpthread进行编译


1

你确定你需要一个矩阵吗?根据你所描述的情况,似乎是队列。队列的锁定相对简单:当任何人读取或写入时,它们会通过pthread_mutex获取排他锁。(除非你真的知道你有性能问题,否则不要使用读写锁等)

当然,不需要任何中断。


这是一个稀疏矩阵(http://en.wikipedia.org/wiki/Sparse_matrix)。出于性能原因,它像2-D链表一样工作。 - Mike DeSimone

1

boost threads.开始。

至于您的设计,似乎您已经决定允许任何线程对整个矩阵进行随机访问。

如果可能的话,最好想出如何将矩阵的部分责任划分给特定的线程。每次访问单元格都需要锁定会成为瓶颈。


好的,这是一个链接列表实现,所以不可能。 - alexgolec
问题的性质决定了你选择什么样的解决方案...除非这是作业或类似的事情? - Daniel Earwicker

1

Thomas讲解了线程库。如果你有操作系统,唯一需要处理中断处理程序的原因是没有操作系统提供的线程控制。

关于锁定的事情,我要警告你小心不要死锁。您需要按行和列进行锁定,因此每行和每列都需要自己的互斥锁。(实际上,读者/写者锁对性能来说更好,但您必须更加小心死锁和竞态条件。)

确保您始终以一致的顺序获取和释放锁定;您不希望一个线程锁定第N行,然后阻止锁定第K列,然后已经锁定第K列的线程决定锁定第N行并被阻止,从而给您两个阻塞的线程和什么也没有发生(请播放John Woo手枪对峙场景)。


我也有同样的想法,只是你比我先说了出来。 - Steven Sudit

1
快速想法,选择std::threads的可用实现之一,然后考虑std::asyncstd::future以及相关工具。

1
关于您的第二个问题,如何进行锁定:将线程置于睡眠状态并唤醒它将由操作系统完成,这不是您需要担心的。但是,您的方案存在问题。
您只想阻止对单元格的访问,当且仅当其行和列都被锁定时。也就是说,如果行或列未被锁定,则允许访问。这通常不是锁的工作方式。此外,如果行已被锁定,但您仍然允许访问(因为列未被锁定),则仍然需要“更多”锁定。这意味着您需要更多的互斥量。
我能想到的最佳实现方法是使用一个原子计数器来控制行,以及一个条件变量计数器来控制列。在访问时:
  • 增加行的原子计数器。
  • 如果原子计数器的先前值为零,则该行已解锁。访问是可以的。
    • 增加列的条件变量。理想情况下,这不应该阻塞。
  • 如果行的计数器不为零,则它被锁定。可能在列上阻塞。
    • 等待直到列的条件变量为零。在释放互斥锁之前将其设置为一。
  • 当您完成访问时:
  • 减少条件变量(不要忘记锁定它),并发出信号以唤醒其他访问(如果它变为零)。
  • 原子地减少行计数器。

这需要一些花式操作,但总体上锁定资源的数量非常低。还有其他人能做得更好吗(或者找到我的方案的反例)?

另请注意,将矩阵分成行和列有些随意。如果在此方案下发生了太多争用,您应该将行细分为两半(例如)。


这里存在一个问题:如果三个线程尝试访问同一列,并且有两个被阻塞,第二个将始终将写锁直接传递给第三个线程,尽管处于关键部分。线程2无法释放互斥锁,线程3无法释放写锁。也不能将互斥锁获取移动到写锁之前。我们希望使用读写锁,其中写操作没有优先级。 - Potatoswatter
尝试trylock互斥锁。如果失败,释放写锁,(阻塞)获取并释放互斥锁(以确保其他线程获得读锁),回到获取写锁。这会影响性能:v(。感觉像是需要为每列都添加一个信号的适当实现。 - Potatoswatter
已解决。有趣的问题... - Potatoswatter

0

如果为每行/列设置一个互斥锁,当行写线程和列读线程访问行/列交叉点处的元素时,会出现竞争条件。如果矩阵很大,你将需要许多互斥锁。我不确定每个操作系统都能提供成千上万的互斥锁。

听起来一个线程安全的生产者-消费者队列会是你问题的更好解决方案。看看Intel的Thread Building Blocks(TBB)库。我发现,当你使用数据流范例对程序进行建模时,多线程变得更加容易。

如果你想要减少大型稀疏矩阵的内存占用,请查看Boost.uBlas。它有一个sparce_matrix模板类,使用Map按索引关联存储元素。

出于效率考虑,您不希望通过复制整个矩阵来传递给生产者-消费者队列。相反,您需要传递您的稀疏矩阵的代理。如果TBB还没有某种代理工具,您可以简单地使用boost::shared_ptr。您可能还希望拥有一个预分配的矩阵池,在数据流“电路”中进行回收利用。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接