前言
消息循环的建立
Post消息
Send消息
Invoke跨线程同步执行方法
总结
如之前的总述文章所述,rtc::Thread类封装了WebRTC中线程的一般功能,比如设置线程名称,启动线程执行用户代码,线程的join,sleep,run,stop等方法;同时也提供了线程内部的消息循环,以及线程之间以同步、异步方式投递消息,同步方式在目标线程执行方法并返回结果等线程之间交互的方式;另外,每个线程均持有SocketServer类成员对象,该类实现了IO多路复用功能。
本文将针对rtc::Thread类所提供消息循环,消息投递的功能进行介绍。由于Thread类是通过继承MessageQueue才具有此类功能,因此,在介绍Thread相关API实现之前应先介绍MessageQueue相关的知识:消息队里管理(MessageQueueManager),消息队列(MessageQueue),消息(Message,DelayedMessage),消息数据(MessageData,TypedMessageData,ScopedMessageData,DisposeData)的相关知识。
Thread类在rtc_base/thread.h中声明,定义在rtc_base/thread.c中(只保留了消息循环以及消息投递相关的API):
class RTC_LOCKABLE Thread : public MessageQueue {
public:
virtual void Run();
virtual void Send(const Location& posted_from,
MessageHandler* phandler,
uint32_t id = 0,
MessageData* pdata = nullptr);
template
ReturnT Invoke(const Location& posted_from, FunctorT&& functor) {
FunctorMessageHandler handler(
std::forward(functor));
InvokeInternal(posted_from, &handler);
return handler.MoveResult();
}
bool IsProcessingMessagesForTesting() override;
void Clear(MessageHandler* phandler,
uint32_t id = MQID_ANY,
MessageList* removed = nullptr) override;
void ReceiveSends() override;
bool ProcessMessages(int cms);
protected:
friend class ScopedDisallowBlockingCalls;
private:
void ReceiveSendsFromThread(const Thread* source);
bool PopSendMessageFromThread(const Thread* source, _SendMessage* msg);
void InvokeInternal(const Location& posted_from, MessageHandler* handler);
std::list<_SendMessage> sendlist_;
bool blocking_calls_allowed_ = true;
friend class ThreadManager;
RTC_DISALLOW_COPY_AND_ASSIGN(Thread);
};
由上一篇文章WebRTC源码分析-线程基础之线程基本功能的线程启动分析知道,用户在没有传入自己的Runnable对象时,新的线程上会执行Thread.Run()方法,该方法源码如下,内部会调用ProcessMessages(kForever)去运行消息循环。而用户如果实现了自己的Runnable对象时,也想要运行消息循环,那咋办嘛? 下面的注释就告知你了,在Runnable.Run()中适时的调用ProcessMessages()方法就行。
// By default, Thread::Run() calls ProcessMessages(kForever). To do other
// work, override Run(). To receive and dispatch messages, call
// ProcessMessages occasionally.
void Thread::Run() {
ProcessMessages(kForever);
}
下面看看这个ProcessMessages()怎么建立消息循环的。分两种情形:
1)默认地,ProcessMessages(kForever),告知无限期进行处理。此时的情形就是函数内部的while循环不停的调用Get()去获取消息,然后处理消息Dispatch()。循环能够退出的条件就是Get方法返回false。由
WebRTC源码分析-线程基础之MessageQueue 分析Get()方法分析可知,无限期处理的情况下,只有循环停止工作或者IO处理出错才会导致Get()返回false。
2)如果ProcessMessages(int cmsLoop),有限期进行处理。那么退出循环的方式有两个,一个是使用时间已经到了,返回true;另外一个是Get()方法返回false,有限期处理的情况下,Get()返回false的条件有三:循环停止工作;IO处理出错;已经耗完所有处理时间也还未找到一个MSG。
bool Thread::ProcessMessages(int cmsLoop) {
// Using ProcessMessages with a custom clock for testing and a time greater
// than 0 doesn't work, since it's not guaranteed to advance the custom
// clock's time, and may get stuck in an infinite loop.
RTC_DCHECK(GetClockForTesting() == nullptr || cmsLoop == 0 ||
cmsLoop == kForever);
// 计算终止处理消息的时间
int64_t msEnd = (kForever == cmsLoop) ? 0 : TimeAfter(cmsLoop);
// 下次可以进行消息获取的时间长度
int cmsNext = cmsLoop;
while (true) {
#if defined(WEBRTC_MAC)
ScopedAutoReleasePool pool;
#endif
// 获取消息
Message msg;
if (!Get(&msg, cmsNext))
return !IsQuitting();
// 处理消息
Dispatch(&msg);
// 若不是无限期,计算下次可以进行消息获取的时间。
if (cmsLoop != kForever) {
cmsNext = static_cast(TimeUntil(msEnd));
// 若使用时间已经到了,那么退出循环
if (cmsNext < 0)
return true;
}
}
}
向一个线程Post消息,只是简单地向消息循环的队列中插入一条待处理的消息,然后Post方法就会返回,不会引发当前线程的阻塞。Thread方法并没有重写MQ的Post方法,因此,关于Post方法的细节分析见 WebRTC源码分析-线程基础之MessageQueue
向一个线程Send消息,会阻塞当前线程的运行,直到该消息被目标线程消费完后才会解除阻塞,从Send方法返回。算法流程如源码及其注释如下(分9个步骤):
void Thread::Send(const Location& posted_from,
MessageHandler* phandler,
uint32_t id,
MessageData* pdata) {
// 目标线程的消息循环是否还在处理消息? // 步骤1
if (IsQuitting())
return;
// 创建需要处理的消息 // 步骤2
Message msg;
msg.posted_from = posted_from;
msg.phandler = phandler;
msg.message_id = id;
msg.pdata = pdata;
// 若目标线程就是自己,那么直接在此处处理完消息就ok // 步骤3
if (IsCurrent()) {
phandler->OnMessage(&msg);
return;
}
// 断言当前线程是否具有阻塞权限,无阻塞权限, // 步骤4
// 那么向别的线程Send消息就是个非法操作
AssertBlockingIsAllowedOnCurrentThread();
// 确保当前线程有一个Thread对象与之绑定 // 步骤5
AutoThread thread;
Thread* current_thread = Thread::Current();
RTC_DCHECK(current_thread != nullptr); // AutoThread ensures this
// 创建一个SendMessage对象,放置到目标线程对象的sendlist_ // 步骤6
// ready表征该消息是否已经处理完。
bool ready = false;
{
CritScope cs(&crit_);
_SendMessage smsg;
smsg.thread = current_thread;
smsg.msg = msg;
smsg.ready = &ready;
sendlist_.push_back(smsg);
}
// 将目标线程从IO处理中唤醒,赶紧处理消息啦~ // 步骤7
// 目标线程将在其消息循环中,调用ReceiveSends()处理Send消息~~
WakeUpSocketServer();
// 同步等待消息被处理 // 步骤8
bool waited = false;
crit_.Enter();
while (!ready) {
crit_.Leave();
// 对方也可能向我Send了消息,可不能都互相阻塞住了
// 处理对方可能Send给我的消息。
current_thread->ReceiveSendsFromThread(this);
// 处理完对方的Send消息后,阻塞等待对方处理完我Send的消息,然后来唤醒我吧
// 但这儿会有个意外,这就是waited存在的意义了
current_thread->socketserver()->Wait(kForever, false);
waited = true;
crit_.Enter();
}
crit_.Leave();
// 如果出现过waited,那么再唤醒一次当前线程去处理Post消息。 // 步骤9
if (waited) {
current_thread->socketserver()->WakeUp();
}
}
要理解上述算法,需要搞清楚Send方法的代码是在当前线程执行的,而调用的是目标线程对象Thread的Send方法,即Send方法中的this,是目标线程线程对象Thread。捋清楚这点非常重要!!!这儿我一步步分析上述算法过程:
AutoThread::AutoThread()
: Thread(SocketServer::CreateDefault(), /*do_init=*/false) {
DoInit();
if (!ThreadManager::Instance()->CurrentThread()) {
ThreadManager::Instance()->SetCurrentThread(this);
}
}
AutoThread::~AutoThread() {
Stop();
DoDestroy();
if (ThreadManager::Instance()->CurrentThread() == this) {
ThreadManager::Instance()->SetCurrentThread(nullptr);
}
}
struct _SendMessage {
_SendMessage() {}
Thread* thread; // 当前线程对象
Message msg; // 消息
bool* ready; // 消息是否处理完毕的标志
};
bool waited = false;
crit_.Enter();
while (!ready) {
crit_.Leave();
current_thread->ReceiveSendsFromThread(this);
current_thread->socketserver()->Wait(kForever, false);
waited = true;
crit_.Enter();
}
crit_.Leave();
1)ready这个参数会被当前线程访问,也会被目标线程访问,必然需要加锁,并且要达到两个线程的互斥,还必须要使用同一个锁。而这个锁就是目标线程的线程对象Thread的CriticalSection crit_成员。因此,while循环开头读取ready时进行了加锁解锁操作。
2)按理说,接下来当前线程阻塞等待目标线程完成操作之后通知我解除阻塞就行了。但是,考虑到一点,如果,两个线程同时互相Send消息,那岂不是二者都卡在等待对方通知这个地方,死锁了?为了避免这个情况,在阻塞等待前,先处理所有别人Send给我的消息吧。调用current_thread->ReceiveSendsFromThread(this)进行处理,注意current_thread是当前线程对象,因而是处理的当前线程所接收到的Send消息,而this是目标线程对象。 需要注意一点PopSendMessageFromThread()方法只会把source线程发送的消息从队列中取出,此时source为this,即目标线程对象。所以,此处ReceiveSendsFromThread()只会处理目标线程Send给当前线程的消息,此时,ready置为true,并调用目标线程的WakeUp()方法,唤醒了目标线程,使得目标线程不会阻塞在Send方法中,从而使得目标线程有机会去运行其消息循环,从而消费当前线程Send给它的消息。很绕很绕,我已经尽力了。
void Thread::ReceiveSendsFromThread(const Thread* source) {
_SendMessage smsg;
crit_.Enter();
while (PopSendMessageFromThread(source, &smsg)) {
crit_.Leave();
Dispatch(&smsg.msg);
crit_.Enter();
*smsg.ready = true;
smsg.thread->socketserver()->WakeUp();
}
crit_.Leave();
}
bool Thread::PopSendMessageFromThread(const Thread* source, _SendMessage* msg) {
for (std::list<_SendMessage>::iterator it = sendlist_.begin();
it != sendlist_.end(); ++it) {
if (it->thread == source || source == nullptr) {
*msg = *it;
sendlist_.erase(it);
return true;
}
}
return false;
}
3)处理完目标线程可能Send给我的消息之后,我终于可以安心地阻塞在IO上等待了,current_thread->socketserver()->Wait(kForever, false)。这时,我们看看目标线程如何操作。之前在步骤7中,已经阐述过,目标线程被唤醒后在消息循环中优先阻塞地调用ReceiveSends()方法来处理Send消息,而且ReceiveSends()是被Thread重写过的方法。代码如下:咦,这不是上面调用的ReceiveSendsFromThread()方法嘛?不过传入的指针为空,此时PopSendMessageFromThread()会将目标线程上收到的所有Send消息都拿出来消费完,设置标志位,然后调用消息发送者线程的WakeUp()来唤醒消息发送者。到此处,终于把这个Send逻辑搞清楚了。完了?还没完呢,唤醒该线程的一定就是目标线程处理完Send消息之后嘛?还有这个waited干嘛用的啊?
void Thread::ReceiveSends() {
ReceiveSendsFromThread(nullptr);
}
4)当前线程沉睡在current_thread->socketserver()->Wait(kForever, false)上时,唤醒该线程的一定就是目标线程处理完Send消息之后嘛?那可不一定,可能是别的线程Wake了它,也可能是目标线程Post了一条消息给当前线程(此时会唤醒当前线程,详见Post消息)。那么意味着当前线程Send消息可能还未处理完,自己就被醒了,那么又得进行While中的ready变量的访问了,又得加锁,解锁,把这个过程再走一遍。
5)这个waited变量干嘛用?就代码而言,我们发现waited变量进入了一次while循环就会变成true,表示我等待过至少一次。没有等待过的原因就是目标线程干活很麻利,在第一次判断ready值的时候就已经把活干完了,Send消息已被消费。具体记录这个等待过一次是做什么用呢?看步骤9
// Our Wait loop above may have consumed some WakeUp events for this
// MessageQueue, that weren't relevant to this Send. Losing these WakeUps can
// cause problems for some SocketServers.
//
// Concrete example:
// Win32SocketServer on thread A calls Send on thread B. While processing the
// message, thread B Posts a message to A. We consume the wakeup for that
// Post while waiting for the Send to complete, which means that when we exit
// this loop, we need to issue another WakeUp, or else the Posted message
// won't be processed in a timely manner.
if (waited) {
current_thread->socketserver()->WakeUp();
}
Invoke方法提供了一个方便的方式:阻塞当前线程,在另外一个线程上同步执行方法,并且返回执行结果。
本质上就是将需要执行的方法封装到消息处理器FunctorMessageHandler中,然后向目标线程Send这个携带特殊消息处理器FunctorMessageHandler的消息,该消息被消费后,阻塞结束,FunctorMessageHandler对象携带了方法执行的结果,当前线程可以从中获取到执行结果。其实,这里的重点有二:
template
ReturnT Invoke(const Location& posted_from, FunctorT&& functor) {
FunctorMessageHandler handler(
std::forward(functor));
InvokeInternal(posted_from, &handler);
return handler.MoveResult();
}
void Thread::InvokeInternal(const Location& posted_from,
MessageHandler* handler) {
TRACE_EVENT2("webrtc", "Thread::Invoke", "src_file_and_line",
posted_from.file_and_line(), "src_func",
posted_from.function_name());
Send(posted_from, handler);
}
本文比较详细的介绍了Thread的消息循环,线程间Post消息,Send消息,跨线程执行方法等功能。由于Thread是通过继承MessageQueue才具有这些功能,因此,需要结合另外3篇文章来一起看。
本文对Post消息(非阻塞)只是一笔带过,因为在Thread并没有改写Post方法,而直接是MessageQueue的Post。
本文对Send消息(阻塞)重点拆解了一番,自己倒是理解了,但不知道是否描述的够清晰,是否有错误。博客的作用嘛,就是为了能够理清楚自己的思路,顺便也为他人做些贡献。如果有不对的地方,看到此处的同路人可以指出
本文还对跨线程执行方法提供了一种便捷的方式,本质就是封装一个特殊的消息处理器FunctorMessageHandler到消息中,并使用Send方法使得目标线程消费消息时执行该方法。