WebRTC源码分析-线程基础之线程管理

前言

WebRTC中的线程管理是通过ThreadManager对象来实施的,该类起着牧羊者的作用,rtc::Thread类对象就是羊群。其通过什么样的技术来实现对rtc::Thread管理的?在不同的系统平台下如何实现?下文将进行阐述。

该类的声明和定义与Thread类一样,位于rtc_base目录下的thread.h与thread.cc文件中。先看其类的声明:

class  ThreadManager {
public:
    static  const  int kForever = -1;
    static  ThreadManager* Instance();
    Thread* CurrentThread();
    void SetCurrentThread(Thread* thread);
    Thread* WrapCurrentThread();
    void UnwrapCurrentThread();
    bool IsMainThread();
private:
    ThreadManager();
    ~ThreadManager();
#if  defined(WEBRTC_POSIX)
    pthread_key_t key_;
#endif
#if  defined(WEBRTC_WIN)
    const  DWORD key_;
#endif
    const  PlatformThreadRef main_thread_ref_;
RTC_DISALLOW_COPY_AND_ASSIGN(ThreadManager);
};

ThreadManager的构造

ThreadManager实现为单例模式,通过静态方法Instance()来获取唯一的实例。其构造与 析构函数均声明为private。

先看静态方法Instance的实现:

ThreadManager* ThreadManager::Instance() {
      static  ThreadManager* const thread_manager = new  ThreadManager();
      return thread_manager;
}

该方法很简单,但是注意这个方法不是线程安全的,那么在WebRTC的多线程环境下是如何保证ThreadManager对象被安全的构造?WebRTC通过一定机制确保了Instance()方法第一次的调用肯定是在单线程的环境下,也即在主线程中被调用,因此是线程安全的。如何实现这点?

  1. WebRTC中启动新线程的标准方法是通过创建Thread对象,然后调用Thread.Start()方法来启用新的线程,而该方法的内部会直接调用一次Insance(),如下截图:
    111.png
  2. WebRTC启动新线程的非标准方法,即用户继承了Thread对象,并且不能通过Thread.Start()方法来启用新线程。此时,WebRTC中是如何保证这点的?如下截图,Thread的WrapCurrent()方法的说明以及其实现说明了此种情况:


    image.png

    继承Thread的类,如果不能通过Thread.Start()来启动线程时,应该在构造中调用WrapCurrent()方法,该方法如下图所示,首先就会调用ThreadManager::Instance()来获取ThreadManager的单例对象。


    image.png

至此,WebRTC通过上述的方式确保了ThreadManager对象被安全的构造。

private构造函数的实现:

#if  defined(WEBRTC_POSIX)
      ThreadManager::ThreadManager() : main_thread_ref_(CurrentThreadRef()) {
#if  defined(WEBRTC_MAC)
             InitCocoaMultiThreading();
 #endif
            pthread_key_create(&key_, nullptr);
     }
#endif

#if  defined(WEBRTC_WIN)
  ThreadManager::ThreadManager()
        : key_(TlsAlloc()), main_thread_ref_(CurrentThreadRef()) {}
#endif

我们可以看到在Windows和类Unix系统中实现进行了区分,WEBRTC_POSIX宏表征该系统是类Unix系统,而WEBRTC_WIN宏表征是Windows系统。虽然实现稍微有些许不同,在MAC下还需要调用InitCocoaMultiThreading()方法来初始化多线程库。但是两个构造函数均初始化了成员key_与main_thread_ref_(我们可以看到WebRTC中的私有成员均以下划线结尾)。其中key是线程管理的关键。

key_的初始化:在Windows平台上,key_被声明为DWORD类型,赋值为TlsAlloc()函数的返回值,TlsAlloc()函数是Windows的系统API,Tls表示的是线程局部存储Thread Local Storage的缩写,其为每个可能的线程分配了一个线程局部变量的槽位,该槽位用来存储WebRTC的Thread线程对象指针。如果不了解相关概念,可以看微软的官方文档,或者TLS--线程局部存储这篇博客来了解。在类Unix系统上,key_被声明pthread_key_t类型,使用方法pthread_key_create(&key_, nullptr);赋值。实质是类Unix系统上的线程局部存储实现,隶属于线程库pthread,因此方法与变量均以pthread开头。总之,在ThreadManager的构造之初,WebRTC就为各个线程所对应的Thread对象制造了一个线程局部变量的槽位,成为多线程管理的关键。

main_thread_ref_的初始化:该成员为PlatformThreadRef类型的对象,赋值为CurrentThreadRef()方法的返回值,如下源码所示:在Windows系统下,取值为WinAPI GetCurrentThreadId()返回的当前线程描述符,DWORD类型;在FUCHSIA系统下(该系统是Google新开发的操作系统,像Android还是基于Linux内核属于类Unix范畴,遵循POSIX规范,但FUCHSIA是基于新内核zircon开发的),返回zx_thread_self(),zx_handle_t类型;在类Unix系统下,通过pthread库的pthread_self()返回,pthread_t类型。总之,如前文所述,这部分代码肯定是在主线程中所运行,因此,main_thread_ref_存储了主线程TID在不同平台下的不同表示。

PlatformThreadRef CurrentThreadRef() {
#if  defined(WEBRTC_WIN)
        return GetCurrentThreadId();
#elif  defined(WEBRTC_FUCHSIA)
        return zx_thread_self();
#elif  defined(WEBRTC_POSIX)
        return pthread_self();
#endif
}

private析构函数的实现:

 ThreadManager::~ThreadManager() {
  // By above RTC_DEFINE_STATIC_LOCAL.
  RTC_NOTREACHED() <<  "ThreadManager should never be destructed.";
}

根据日志,我们看到ThreadManager单例对象的析构函数是永不会被调用的,直到整个进程结束自动去释放该对象所占用的空间。否则,会触发断言,在标准错误输出上述错误日志后,调用系统的abort()函数。后续会对RTC_NOTREACHED宏进行展开描述,看看其究竟是如何处理的。

获取,设置当前线程关联的Thread对象

#if  defined(WEBRTC_WIN)
      Thread* ThreadManager::CurrentThread() {
             return  static_cast<Thread*>(TlsGetValue(key_));
      }

      void  ThreadManager::SetCurrentThread(Thread* thread) {
             RTC_DCHECK(!CurrentThread() || !thread);
             TlsSetValue(key_, thread);
      }
#endif

#if  defined(WEBRTC_POSIX)
      Thread* ThreadManager::CurrentThread() {
            return  static_cast<Thread*>(pthread_getspecific(key_));
      }

      void ThreadManager::SetCurrentThread(Thread* thread) {
#if RTC_DLOG_IS_ON
             if (CurrentThread() && thread) {
                    RTC_DLOG(LS_ERROR) << "SetCurrentThread: Overwriting an existing value?";
             }
 #endif  // RTC_DLOG_IS_ON
            pthread_setspecific(key_, thread);
    }
#endif

如前文所述,不论是何种平台,在ThreadManager的构造之初就为Thread指针分配了线程局部存储的槽位key_,通过不同平台的get,set方法就可以将当前线程所关联的Thread对象指针从该槽位取出或设置进去。但是,有这么几个点需要注意:

  • Thread是用户层线程的表征,可以通过其来访问,操作该线程在内核中的数据结构。但用户层和内核层的线程表征,二者并非是共存关系。以主线程来说,进程一运行起来其线程内核结构就存在,但是用户层主线程的表征Thread对象是不存在的,因此,在程序入口main()函数开头调用ThreadManager::CurrentThread()方法,得到的必然是空指针。如果想要将主线程纳入管理,必然要先创建一个Thread对象,然后调用ThreadManager::SetCurrentThread(Thread* thread)设置到当前线程的线程局部存储的槽位中。正如example目录下的peerconnection_client示例工程那样做的,其中Win32Thread就是Thread类的子类。
121.png
  • 对于非主线程,如何纳入管理?由前文所说,主线程外,WebRTC的其他线程以Thread.Start()来启动,新的线程中会执行Thread.PreRun()方法。该方法中就调用了ThreadManager::SetCurrentThread(Thread* thread)方法将新的线程纳入ThreadManager的管理,在线程结束后,调用ThreadManager::SetCurrentThread(nullptr)解除管理。
1.png

包装当前线程为Thread对象,当前线程去包装

Thread* ThreadManager::WrapCurrentThread() {
    Thread* result = CurrentThread();
    if (nullptr == result) {
         result = new  Thread(SocketServer::CreateDefault());
         result->WrapCurrentWithThreadManager(this, true);
    }
    return result;
}

如果已经有Thread对象与当前线程关联,那么直接返回该对象。否则构造一个新的Thread对象,并通过该对象的WrapCurrentWithThreadManager()方法将新建的Thread对象纳入ThreadManager的管理之中:

bool  Thread::WrapCurrentWithThreadManager(ThreadManager* thread_manager,
                                                          bool  need_synchronize_access) {
          RTC_DCHECK(!IsRunning());
#if  defined(WEBRTC_WIN)
          if (need_synchronize_access) {
                   // We explicitly ask for no rights other than synchronization.
                   // This gives us the best chance of succeeding.
                   thread_ = OpenThread(SYNCHRONIZE, FALSE, GetCurrentThreadId());
                   if (!thread_) {
                             RTC_LOG_GLE(LS_ERROR) <<  "Unable to get handle to thread.";
                             return  false;
                   }
                   thread_id_ = GetCurrentThreadId();
          }
#elif  defined(WEBRTC_POSIX)
         thread_ = pthread_self();
#endif
         owned_ = false;
         thread_manager->SetCurrentThread(this);
         return  true;
}

在Windows系统与类Unix系统下的差别一点在于Thread.thread_的赋值方式。Windows系统上,使用OpenThread()来打开当前已存在的线程,获取其句柄,此处注明只获取该线程的同步操作权限,也即在该线程进行Wait等操作,这样能提高该方法的成功率;而类Unix系统上,使用pthread库的pthread_self()方法来获取当前线程pthread_t对象。另外将Thread.owned_标志位置位false,表示该线程对象是通过wrap而来,而非调用Thread.Start的标准方式而来。最后使用 ThreadManager. SetCurrentThread方法将新创建的Thread对象纳入管理。

void  ThreadManager::UnwrapCurrentThread() {
         Thread* t = CurrentThread();
         if (t && !(t->IsOwned())) {
                 t->UnwrapCurrent();
                delete t;
         }
}

对于线程的unwrap操作,会根据该线程是不是wrap而来,即owned_是否为false,来决定是否进行正真的unwrap操作。如果是的话,就调用Thread.UnwrapCurrent方法进行实际操作,并最后删除Thread对象。

void  Thread::UnwrapCurrent() {
        // Clears the platform-specific thread-specific storage.
        ThreadManager::Instance()->SetCurrentThread(nullptr);
#if  defined(WEBRTC_WIN)
       if (thread_ != nullptr) {
               if (!CloseHandle(thread_)) {
                        RTC_LOG_GLE(LS_ERROR)
                                <<  "When unwrapping thread, failed to close handle.";
               }
               thread_ = nullptr;
               thread_id_ = 0;
       }
#elif  defined(WEBRTC_POSIX)
       thread_ = 0;
#endif
}

unwrap操作首先需要将当前线程对象所占的槽位置空,即调用ThreadManager::Instance()->SetCurrentThread(nullptr); 来完成。其次是销毁线程的句柄,在Windows下需要先调用CloseHandle(thread_)销毁句柄,然后句柄置空,类Unix系统下直接将成员thread_置空即可。

判断当前线程是否为主线程

bool  ThreadManager::IsMainThread() {
         return IsThreadRefEqual(CurrentThreadRef(), main_thread_ref_);
}

bool IsThreadRefEqual(const  PlatformThreadRef& a, const  PlatformThreadRef& b) {
#if  defined(WEBRTC_WIN) || defined(WEBRTC_FUCHSIA)
        return  a == b;
#elif  defined(WEBRTC_POSIX)
        return pthread_equal(a, b);
#endif
}

没有太多要说明的,注意类Unix系统下使用pthread库中的pthread_equal()方法进行判断。

总结

  • WebRTC中ThreadManager类扮演者牧羊者的角色,通过线程局部存储(TLS,Thread Local Storage)的技术提供了对Thread管理,而这种管理其实就是为每个线程包装一个与其相关联的rtc::Thread类对象,并将该对象的地址存储在线程本身的局部存储的某个槽中。
  • 不同平台的TLS实现API是不一样的,在Windows上通过Windows API TlsAlloc()来分配槽位(即key值),对应于类Unix系统使用pthread库中的pthread_key_create()来分配;为获取或者设置槽位中的值(即Thread对象地址),在Windows上使用TlsGetValue()和TlsSetValue(),对应于类Unix系统使用pthread_getspecific()与pthread_setspecific()
  • ThreadManager类对象是个单例,但是一个非线程安全的实现,如何保证ThreadManager对象的在WebRTC的多线程环境下安全的初始化?
    以上就是本文所阐述的基本内容。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,829评论 1 331
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,603评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,846评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,600评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,780评论 3 272
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,695评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,136评论 2 293
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,862评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,453评论 0 229
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,942评论 2 233
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,347评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,790评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,293评论 3 221
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,839评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,448评论 0 181
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,564评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,623评论 2 249

推荐阅读更多精彩内容

  • 转自:Youtherhttps://www.cnblogs.com/youtherhome/archive/201...
    njukay阅读 1,585评论 0 52
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,028评论 1 32
  • 概述 线程和进程本质上来说都属于一个内核调度单元,也就是说都可以作为一条单独的执行路径。但是多进程程序通常有一些限...
    loopppp阅读 443评论 0 0
  • 移步系列Android跨进程通信IPC系列Bionic库是Android的基础库之一,也是连接Android系统和...
    凯玲之恋阅读 893评论 0 1
  • 多线程系列文章源码头文件内容: #include #include #include 作为程序员,就是要减少重复劳...
    batbattle阅读 456评论 0 2