2011-05-30 58 views
9

使用pthread时,我可以在线程创建时传递数据。如何将数据传递到正在运行的线程

将新数据传递给已经运行的线程的正确方法是什么?

我正在考虑做一个全局变量,并让我的线程读取。

谢谢

+7

再一次,人们试图解决问题,因为他们是初学者问题。尽量记住你自己曾经是初学者。 – 2011-05-30 00:59:59

回答

6

这肯定会奏效。基本上,线程只是共享相同内存空间的轻量级进程。全局变量存在于该内存空间中,可用于每个线程。

诀窍不在于读者和作者一样。如果你有一个简单的全局内存块,比如int,那么分配给那个int可能是安全的。 Bt考虑一些更复杂的事情,比如struct。只是要明确,让我们说我们有

struct S { int a; float b; } s1, s2; 

现在s1,s2struct 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。

道德是,虽然全局内存是一种完全可行的方式来做到这一点,但您需要考虑到您的线程将跨越另一个线程的可能性。有几种解决方案,基本的是使用信号量。信号量有两个操作,混淆地命名为PV

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 ; 
    } 

和你保证,你永远不会有螺纹2个半完成的任务,而线程1正在试图打印。

(P线程有信号灯,顺便说一句。)

+3

@Charlie:对不起,但我完全不同意关于线程的措辞作为“轻量级进程”。在许多操作系统中 - 包括许多unixoid系统(如BSD)和Windows--线程是运行代码的实体,而进程只是线程,内存空间等的容器。现在,我意识到这可能是挑剔的,但我认为这个区别非常重要,非常重要。 – 0xC0000022L 2011-05-30 00:42:13

+2

@SAD这在历史和事实上都是错误的。进程是具有一个或多个线程的程序计数器的存储空间。线程是一个“控制线程”,它包含一个程序计数器和一个堆栈。参见例如关于线程的Wiki文章。 http://en.wikipedia.org/wiki/Thread_(computer_science)人们现在很困惑,但是一本好的操作系统书会讨论历史并将其清除。 – 2011-05-30 00:56:26

+0

“线程只是共享相同内存空间的轻量级进程”的想法实际上是一个可怕的错误观念,源于Linux上的“POSIX线程”的原始不可靠的不符合POSIX标准的实现,称为LinuxThreads。这与标准对“过程”和“线程”的定义/用法完全矛盾。 – 2011-05-30 01:13:38

2

全局变量是不好的开始,甚至与多线程编程变得更糟。相反,线程的创建者应该分配某种传递给pthread_create的上下文对象,该对象包含向线程传递信息以及从线程传递信息所需的任何缓冲区,锁,条件变量,队列等。

2

您将需要自己构建它。最典型的方法需要来自另一个线程的一些合作,因为它会是一个奇怪的接口,用一些数据和代码来执行它“中断”一个正在运行的线程......这也会有一些与像POSIX信号或IRQs,这两种都很容易在处理过程中在脚下拍摄自己,如果你没有仔细考虑过它...(简单的例子:你不能在信号处理程序中调用malloc,因为你可能会在malloc中间被打断,所以你可能会崩溃,而其访问仅部分更新malloc的内部数据结构。)

典型的做法是让你的线程创建程序基本上是一个事件循环。您可以构建一个队列结构并将其作为参数传递给线程创建例程。然后其他线程可以入队,线程的事件循环将使它出队并处理数据。请注意,这比全局变量(或全局队列)更清洁,因为它可以扩展为具有多个这样的队列。

您需要在该队列数据结构上进行一些同步。可以写关于如何实现队列结构同步的全书,但最简单的事情会有锁和信号量。修改队列时,线程会锁定。当等待某些东西被出队时,消费者线程会等待一个由入队者增加的信号量。实现一些机制来关闭消费者线程也是一个好主意。

3

几十年来,我一直使用asveikau建议的消息传递,基于生产者 - 消费者队列的通信机制,而没有任何与多线程相关的问题。有一些优点:

1)队列上传递的'threadCommsClass'实例通常可以包含线程完成其工作所需的所有内容 - 输入数据的成员/ s,输出数据的成员/ s,线程调用来完成工作,在某处放置任何错误/异常消息和一个'returnToSender(this)'事件来调用,以便通过一些线程安全的方式将所有内容返回给请求者,这是工作者线程不需要知道的。工作线程然后异步地运行一组完全封装的数据,不需要锁定。 'returnToSender(this)'可能会将对象排队到另一个P-C队列中,它可能会将它PostMessage到一个GUI线程,它可能会将对象释放回池中或者仅仅处理它。无论它做什么,工作者线程都不需要知道它。

2)请求线程不需要知道任何线程做了什么工作 - 所有的请求者需要的是一个推送的队列。在极端情况下,队列另一端的工作线程可能会序列化数据并通过网络将其传递给另一台计算机,只有在收到网络回复时才调用returnToSender(this) - 请求者不需要知道这一点细节 - 只有工作已经完成。

3)通常可以为“threadCommsClass”实例和队列安排活得比两个请求者线程和工作线程。当请求者或工作者被终止并处理()在另一个之前时,这极大地减轻了这些问题 - 因为它们不直接共享数据,不会有AV /不管。这也吹走了所有那些'我无法阻止我的工作线程,因为它阻塞在一个阻塞API'的问题 - 为什么要阻止它,如果它可以成为孤儿,并留下去死,没有可能写入被释放的东西?

4)线程池减小到一个行的for循环,创建几个工作线程,并将它们传递相同的输入队列。

5)锁定被限制为队列。应用程序中越多的互斥锁,condVars,critical-sections和其他同步锁,越难控制它,间歇性死锁的机会越大,这是调试的噩梦。使用排队消息(理想情况下),只有队列类具有锁。队列类必须100%与多生产者/消费者,但这是一个类,而不是一个应用程序充满了不协调的锁定,(yech!)。

6)一种threadCommsClass可以随时提出,任何地方,在任何线程中并压入到队列中。请求者代码甚至不需要直接执行它,例如。对记录器类方法的调用'myLogger.logString(“Operation completed successfully”);'可以将字符串复制到comms对象中,将其排队到执行日志写入的线程并立即返回。然后由logger class线程来处理日志数据,当日志数据出队时它可以将其写入日志文件,它可能会在一分钟后发现日志文件由于网络问题而无法访问。它可能会决定日志文件太大,将其归档并启动另一个日志文件。它可以将字符串写入磁盘,然后将threadMessageClass实例PostMessage到GUI线程以显示在终端窗口中,无论如何。对日志请求线程无关紧要,它只是继续进行,就像任何其他调用日志记录的线程一样,对性能没有显着影响。

7)如果你需要杀死一个线程等待队列中,而不是waiing为OS杀死它的应用程序关闭的,只是对其进行排队的消息告诉它teminate。

有一定的缺点:

1)推搡数据直接进入螺旋件,预示其运行并等待它完成更容易理解并会更快,假设线程没有被每次创建。

2)真正的异步操作,其中线程排队等待某些工作,稍后通过调用某个必须传递结果的事件处理程序返回它,对于用于单线程代码的开发人员来说更难以处理并且通常需要状态机类型设计,其中必须在threadCommsClass中发送上下文数据,以便在结果返回时采取正确的操作。如果偶尔发生请求者需要等待的情况,它可以在threadCommsClass中发送一个由returnToSender方法发送信号的事件,但这显然比简单地等待某个线程处理完成更复杂。

无论设计时,忘记了简单的全局变量作为其他海报说。在线程通信中有一些全局类型 - 我经常使用的是线程安全的threadCommsClass实例池(这只是一个预填充对象的队列)。任何希望通信的线程都必须从池中获取一个threadCommsClass实例,并将其加载并排队。当通信完成后,最后使用它的线程将其释放回池中。这种方法可以防止失控的new(),并允许我在测试过程中轻松监视池级别,而不需要任何复杂的内存管理器(我通常使用定时器每秒钟将池级别转换为状态栏)。泄漏的物体(水平下降)和双重释放的物体(水平上升)很容易检测到,因此得到修复。

多线程可以是安全和提供几乎保持/增强快感可扩展,高性能的应用程序,(几乎:),但你必须裁员简单的全局 - 像对待龙舌兰 - 快速,现在很容易高,但你只知道他们明天就会头昏脑胀。

祝你好运!

马丁

相关问题