【发布时间】:2011-09-04 12:11:33
【问题描述】:
当使用 pthread 时,我可以在线程创建时传递数据。
将新数据传递给已经运行的线程的正确方法是什么?
我正在考虑创建一个全局变量并让我的线程从中读取。
谢谢
【问题讨论】:
-
再一次,人们试图结束问题,因为它们是初学者问题。试着记住你自己曾经是个初学者。
标签: c++ c multithreading pthreads
当使用 pthread 时,我可以在线程创建时传递数据。
将新数据传递给已经运行的线程的正确方法是什么?
我正在考虑创建一个全局变量并让我的线程从中读取。
谢谢
【问题讨论】:
标签: c++ c multithreading pthreads
这肯定会奏效。基本上,线程只是共享相同内存空间的轻量级进程。位于该内存空间中的全局变量可供每个线程使用。
诀窍不在于读者,而在于作者。如果你有一个简单的全局内存块,比如int,那么分配给那个int 可能是安全的。 Bt 考虑一些更复杂的东西,比如struct。只是为了确定,假设我们有
struct S { int a; float b; } s1, s2;
现在s1,s2 是struct S 类型的变量。我们可以初始化它们
s1 = { 42, 3.14f };
我们可以分配它们
s2 = s1;
但是当我们分配它们时,处理器不能保证一步完成对整个结构的分配——我们说它不是原子的。所以现在让我们想象两个线程:
thread 1:
while (true){
printf("{%d,%f}\n", s2.a, s2.b );
sleep(1);
}
thread 2:
while(true){
sleep(1);
s2 = s1;
s1.a += 1;
s1.b += 3.14f ;
}
我们可以看到我们希望 s2 具有值 {42, 3.14}, {43, 6.28}, {44, 9.42} ....
但我们看到的打印结果可能是这样的
{42,3.14}
{43,3.14}
{43,6.28}
或
{43,3.14}
{44,6.28}
等等。问题是线程 1 可能会在分配期间的任何时间获得控制权并“查看”s2。
道理是,虽然全局内存是一种完全可行的方法,但您需要考虑线程相互交叉的可能性。有几种解决方案,基本的解决方案是使用信号量。信号量有两个操作,在荷兰语中被混淆地命名为 P 和 V。
P 只是等到变量为 0 后再继续,将变量加 1; V 从变量中减去 1。唯一特别的是它们以原子方式执行此操作——它们不能被打断。
现在,你编码为
thread 1:
while (true){
P();
printf("{%d,%f}\n", s2.a, s2.b );
V();
sleep(1);
}
thread 2:
while(true){
sleep(1);
P();
s2 = s1;
V();
s1.a += 1;
s1.b += 3.14f ;
}
并且您可以保证,当线程 1 尝试打印时,您永远不会让线程 2 完成任务的一半。
(顺便说一下,Pthreads 有信号量。)
【讨论】:
void* 正是为了传递这种上下文,因此惯用代码将使用它。
全局变量一开始就不好,多线程编程更糟。相反,线程的创建者应该分配某种传递给pthread_create 的上下文对象,其中包含将信息传入和传出线程所需的任何缓冲区、锁、条件变量、队列等。
【讨论】:
您需要自己构建它。最典型的方法需要来自另一个线程的一些合作,因为它会有点奇怪的接口来“中断”正在运行的线程,并在其上执行一些数据和代码......这也会有一些相同的技巧诸如 POSIX 信号或 IRQ 之类的东西,如果您没有仔细考虑过,在处理这两者时很容易将自己开枪打死……(简单示例:您不能在信号处理程序中调用 malloc,因为你可能会在malloc 的中间被打断,所以你在访问malloc 的内部数据结构时可能会崩溃,这些数据结构只是部分更新。)
典型的方法是让你的线程创建例程基本上是一个事件循环。您可以构建一个队列结构并将其作为参数传递给线程创建例程。然后其他线程可以将事物入队,线程的事件循环会将其出队并处理数据。请注意,这比全局变量(或全局队列)更简洁,因为它可以扩展为拥有多个这样的队列。
您将需要对该队列数据结构进行一些同步。可以写整本书来介绍如何实现队列结构的同步,但最简单的就是锁和信号量。修改队列时,线程获取锁。当等待某些东西出队时,消费者线程将等待一个信号量,该信号量由入队者递增。实现一些机制来关闭消费者线程也是一个好主意。
【讨论】:
几十年来,我一直在使用 asveikau 建议的消息传递、基于生产者-消费者队列的通信机制,没有任何与多线程相关的问题。有一些优点:
1) 队列中传递的“threadCommsClass”实例通常可以包含线程完成其工作所需的所有内容 - 用于输入数据的成员、用于输出数据的成员、用于线程调用的方法工作,在某个地方放置任何错误/异常消息和一个“returnToSender(this)”事件来调用,因此通过某种线程安全将所有内容返回给请求者意味着工作线程不需要知道。然后,工作线程在一组完全封装的不需要锁定的数据上异步运行。 'returnToSender(this)' 可能会将对象排队到另一个 P-C 队列中,它可能会将其 PostMessage 到 GUI 线程,它可能会将对象释放回池或只是 dispose() 它。无论它做什么,工作线程都不需要知道它。
2) 请求线程不需要知道哪个线程完成了工作——请求者需要的只是一个推送队列。在极端情况下,队列另一端的工作线程可能会序列化数据并通过网络将其传递给另一台机器,仅在收到网络回复时调用 returnToSender(this) - 请求者不需要知道这一点详细信息 - 仅表明工作已完成。
3) 通常可以安排'threadCommsClass' 实例和队列比请求者线程和工作线程的寿命都长。当请求者或工作人员被终止并在另一个之前处理()时,这极大地缓解了这些问题 - 因为他们不直接共享数据,所以不会有 AV/其他。这也消除了所有那些“我无法停止我的工作线程,因为它卡在阻塞 API 上”的问题——如果它只是孤立的并且没有可能写入被释放的东西,为什么还要停止它呢?
4) 线程池简化为单行 for 循环,该循环创建多个工作线程并将它们传递给相同的输入队列。
5) 锁定仅限于队列。应用程序中的互斥锁、condVar、临界区和其他同步锁越多,控制它们就越困难,出现间歇性死锁的可能性就越大,这是调试的噩梦。对于排队的消息,(理想情况下)只有队列类有锁。队列类必须 100% 与多个生产者/消费者一起工作,但这是一个类,而不是一个充满不协调锁定的应用程序,(耶!)。
6) 可以随时随地在任何线程中引发 threadCommsClass 并将其推送到队列中。请求者代码甚至不需要直接执行此操作,例如。调用记录器类方法,'myLogger.logString("操作成功完成");'可以将字符串复制到 comms 对象中,将其排队到执行日志写入的线程并“立即”返回。然后由记录器类线程在它出列时处理日志数据 - 它可能会将其写入日志文件,它可能会在一分钟后发现由于网络问题而无法访问日志文件。它可能决定日志文件太大,将其归档并启动另一个。它可以将字符串写入磁盘,然后将 threadCommsClass 实例 PostMessage 写入 GUI 线程,以便在终端窗口中显示,无论如何。日志请求线程无关紧要,它会继续执行,就像任何其他已调用日志记录的线程一样,不会对性能产生重大影响。
7) 如果您确实需要终止在队列中等待的线程,而不是等待操作系统在应用程序关闭时终止它,只需将其排队等待一条消息,告诉它终止。
肯定有缺点:
1) 将数据直接推入线程成员,发信号通知它运行并等待它完成更容易理解并且会更快,假设不必每次都创建线程。
2) 真正的异步操作,其中线程将一些工作排入队列,并在稍后的某个时间通过调用必须将结果返回的一些事件处理程序来返回它,对于习惯于单线程代码的开发人员来说更难以处理和通常需要状态机类型设计,其中必须在 threadCommsClass 中发送上下文数据,以便在返回结果时可以采取正确的操作。如果偶尔出现请求者只需要等待的情况,它可以在 threadCommsClass 中发送一个由 returnToSender 方法发出信号的事件,但这显然比简单地等待某个线程句柄完成更复杂。
无论使用什么设计,忘记其他海报所说的简单全局变量。线程通信中有一些全局类型的情况——我经常使用的一个是线程安全的 threadCommsClass 实例池(这只是一个预先填充有对象的队列)。任何希望进行通信的线程都必须从池中获取一个 threadCommsClass 实例,将其加载并排队。通信完成后,最后一个使用它的线程将其释放回池中。这种方法可以防止 new() 失控,并允许我在测试期间轻松监控池级别,而无需任何复杂的内存管理器(我通常使用计时器每秒将池级别转储到状态栏)。泄漏的物体(水平下降)和双重释放的物体(水平上升)很容易检测到并因此得到修复。
多线程可以安全并提供可扩展的高性能应用程序,维护/增强几乎是一种乐趣,(几乎:),但您必须放弃简单的全局变量 - 像对待它们一样对待它们龙舌兰酒 - 现在简单快捷,但你只知道他们明天会让你大吃一惊。
祝你好运!
马丁
【讨论】: