WebRTC源码分析之锁-CriticalSection


多线程在使用临界区时需要加锁,WebRTC封装了跨平台的锁 CriticalSectionGlobalLock,为了简化锁的使用,提供了 区域锁 CritScopeTryCritScopeGlobalLockScope

CriticalSection使用示例

我们用两个线程对同一个全局变量各加1000000次。不同的编程方式如下:

工程

示例工程:https://pan.baidu.com/s/1rbI2hwXpMA-Pb-i-zCdVWA 提取码:cenz

不加锁
#include <unistd.h>
#include <fcntl.h>
#include <pthread.h>
#include <iostream>

using namespace std;

int value = 0;

void * mythread(void * arg)
{
	int i,tmp;
	for(i=0;i<1000000;i++)
	{
		tmp = value;
		tmp++;
		value = tmp;
	}
}

int main()
{
	pthread_t thid0;
	pthread_t thid1;

    /*创建两个线程*/
	pthread_create(&thid0,NULL,mythread,(void*)0);
	pthread_create(&thid1,NULL,mythread,(void*)1);

    /*阻塞的回收线程*/
	pthread_join(thid0,NULL);
	pthread_join(thid1,NULL);

	cout<<"value = "<<value<<endl;

	return 0;
}

在这里插入图片描述

在不加锁的情况下,得到的结果和期待的结果不同,我们期待的结果是2000000。

加锁-使用pthread库中的互斥锁
#include <unistd.h>
#include <fcntl.h>
#include <pthread.h>
#include <iostream>

using namespace std;

int value = 0;
pthread_mutex_t mutex;    /*定义锁*/

void * mythread(void * arg)
{
	int i,tmp;
	for(i=0;i<1000000;i++)
	{
		pthread_mutex_lock(&mutex);      /*上锁*/
		tmp = value;
		tmp++;
		value = tmp;
		pthread_mutex_unlock(&mutex);    /*释放锁*/
	}
}

int main()
{
	pthread_t thid0;
	pthread_t thid1;

    /*初始化锁*/
	pthread_mutex_init(&mutex,NULL);

	pthread_create(&thid0,NULL,mythread,(void*)0);
	pthread_create(&thid1,NULL,mythread,(void*)1);

	pthread_join(thid0,NULL);
	pthread_join(thid1,NULL);

    /*销毁锁*/
	pthread_mutex_destroy(&mutex);

	cout<<"value = "<<value<<endl;

	return 0;
}

在这里插入图片描述
加了锁以后,得到了正确结果。

加锁-使用CriticalSection
#define WEBRTC_POSIX

#include <unistd.h>
#include <fcntl.h>
#include <pthread.h>
#include <iostream>
#include "critical_section.h"

using namespace std;

int value = 0;
rtc::CriticalSection critsect_;

void * mythread(void * arg)
{
	int i,tmp;
	for(i=0;i<1000000;i++)
	{
		critsect_.Enter();
		tmp = value;
		tmp++;
		value = tmp;
		critsect_.Leave();
	}
}

int main()
{
	pthread_t thid0;
	pthread_t thid1;

	pthread_create(&thid0,NULL,mythread,(void*)0);
	pthread_create(&thid1,NULL,mythread,(void*)1);

	pthread_join(thid0,NULL);
	pthread_join(thid1,NULL);

	cout<<"value = "<<value<<endl;

	return 0;
}

在这里插入图片描述
使用WebRTC封装的锁CriticalSection,但这种方式不太地道。

加锁-使用CritScope
#define WEBRTC_POSIX

#include <unistd.h>
#include <fcntl.h>
#include <pthread.h>
#include <iostream>
#include "critical_section.h"

using namespace std;

int value = 0;
rtc::CriticalSection critsect_;    /*定义锁*/

void * mythread(void * arg)
{
	int i,tmp;
	for(i=0;i<1000000;i++)
	{
		{   /*加括号,形成小的作用域。*/
			rtc::CritScope cs(&critsect_);   /*上锁*/
			tmp = value;
			tmp++;
			value = tmp;
		}   /*释放cs对象,并释放锁。*/
	}
}

int main()
{
	pthread_t thid0;
	pthread_t thid1;

	pthread_create(&thid0,NULL,mythread,(void*)0);
	pthread_create(&thid1,NULL,mythread,(void*)1);

	pthread_join(thid0,NULL);
	pthread_join(thid1,NULL);

	cout<<"value = "<<value<<endl;

	return 0;
}

在这里插入图片描述
这种方式才是使用WebRTC中锁,最正确的方式。定义了CritScope对象,在其作用域内都是上锁的,离开作用域就释放锁。

加锁-使用GlobalLock
#define WEBRTC_POSIX

#include <unistd.h>
#include <fcntl.h>
#include <pthread.h>
#include <iostream>
#include "critical_section.h"

using namespace std;

int value = 0;
rtc::GlobalLock lock;   /*定义锁*/

void * mythread(void * arg)
{
	int i,tmp;
	for(i=0;i<1000000;i++)
	{
		{
			rtc::GlobalLockScope ls(&lock);    /*上锁*/
			tmp = value;
			tmp++;
			value = tmp;
		}   /*释放锁*/
	}
}

int main()
{
	pthread_t thid0;
	pthread_t thid1;

	pthread_create(&thid0,NULL,mythread,(void*)0);
	pthread_create(&thid1,NULL,mythread,(void*)1);

	pthread_join(thid0,NULL);
	pthread_join(thid1,NULL);

	cout<<"value = "<<value<<endl;

	return 0;
}

在这里插入图片描述
GlobalLock实现的锁紧用于保护全局变量,不用作其他用途。从实现原理上,CriticalSection

GlobalLock底层实现上是不一样的。

CriticalSection源码分析

在实现区域锁CritScopeGlobalLockScope的时候,使用到了资源获取即初始化(RAII)。关于RAII,在《WebRTC源码分析之智能指针scoped_refptr》中有介绍。

CriticalSection类

CriticalSection用于定义一把跨平台的锁。

类的声明
class RTC_LOCKABLE CriticalSection
{
 public:
  CriticalSection();
  ~CriticalSection();

  /*阻塞的进入临界区*/
  void Enter() const RTC_EXCLUSIVE_LOCK_FUNCTION();

  /*非阻塞的进入临界区,无法获取锁,不阻塞。*/
  bool TryEnter() const RTC_EXCLUSIVE_TRYLOCK_FUNCTION(true);

  /*离开临界区*/
  void Leave() const RTC_UNLOCK_FUNCTION();

 private:
  bool CurrentThreadIsOwner() const;

  mutable pthread_mutex_t mutex_;     /*定义互斥锁*/   

  mutable PlatformThreadRef thread_;  
  mutable int recursion_count_;       /*记录递归次数*/
};

RTC_LOCKABLERTC_EXCLUSIVE_LOCK_FUNCTION()RTC_UNLOCK_FUNCTION()RTC_EXCLUSIVE_TRYLOCK_FUNCTION(true) 这几个宏展开后都和__attribute__有关,这些宏都是给编译器用的,现在就不详细介绍了,之后会写一篇文章单独介绍这些内容。

构造器和析构器
CriticalSection::CriticalSection() 
{
  /*定义互斥锁的属性*/
  pthread_mutexattr_t mutex_attribute;
  pthread_mutexattr_init(&mutex_attribute);

  /*将互斥锁设置为递归的互斥锁*/
  pthread_mutexattr_settype(&mutex_attribute, PTHREAD_MUTEX_RECURSIVE);

  /*初始化互斥锁*/
  pthread_mutex_init(&mutex_, &mutex_attribute);

  /*销毁互斥锁的属性*/
  pthread_mutexattr_destroy(&mutex_attribute);

  CS_DEBUG_CODE(thread_ = 0);
  CS_DEBUG_CODE(recursion_count_ = 0);
  RTC_UNUSED(thread_);
  RTC_UNUSED(recursion_count_);
}

CriticalSection::~CriticalSection() 
{
  /*销毁互斥锁*/
  pthread_mutex_destroy(&mutex_);
}

CriticalSection是一把递归锁,linux提供的锁默认属性是非递归锁,所以需要通过锁的属性将锁设置为递归锁。

递归锁就是同一个线程可以多次对同一把锁上锁。同一个线程对同一把非递归锁和递归锁的使用方式:

非递归锁上锁 —— 解锁 —— 上锁 —— 解锁 —— 上锁 —— 解锁

递归锁上锁 —— 上锁 —— 上锁 —— 解锁 —— 解锁 —— 解锁

CurrentThreadIsOwner函数
bool CriticalSection::CurrentThreadIsOwner() const 
{
#if CS_DEBUG_CHECKS
  /*用于比较thread_保存的线程是否和现在的线程相等*/
  return IsThreadRefEqual(thread_, CurrentThreadRef());
#else
  return true;
#endif  
}

CurrentThreadRef()函数返回当前线程的id,IsThreadRefEqual()函数用于比较线程id是否相等。其中thread_CriticalSection类的数据成员。

Enter函数
void CriticalSection::Enter() const RTC_EXCLUSIVE_LOCK_FUNCTION() 
{
  /*上锁*/
  pthread_mutex_lock(&mutex_);

#if CS_DEBUG_CHECKS
  if (!recursion_count_)  /*若是线程第一次使用锁,则记录线程的id。*/
  {
    RTC_DCHECK(!thread_);
    thread_ = CurrentThreadRef();
  } 
  else 
  {
    /*若发生了递归,判断是否是同一线程在递归调用互斥锁。*/
    RTC_DCHECK(CurrentThreadIsOwner());
  }

  /*记录递归的次数*/
  ++recursion_count_;
#endif
}

Enter()函数会调用linux系统接口pthread_mutex_lock上锁。pthread_mutex_lock函数在获取不到锁时,会阻塞线程,直到获取到锁。

当递归的上锁时,需要判断是否是同一线程,只有同一个线程才能递归的给同一把锁多次上锁。同时需要记录锁被递归的次数,上锁次数和解锁次数要保持一致。

TryEnter函数
bool CriticalSection::TryEnter() const RTC_EXCLUSIVE_TRYLOCK_FUNCTION(true) 
{
  /*非阻塞上锁*/
  if (pthread_mutex_trylock(&mutex_) != 0)
    return false;

#if CS_DEBUG_CHECKS
  if (!recursion_count_) 
  {
    RTC_DCHECK(!thread_);
    thread_ = CurrentThreadRef();
  } 
  else 
  {
    RTC_DCHECK(CurrentThreadIsOwner());
  }

  ++recursion_count_;
#endif
  return true;
}

TryEnter()函数也是用于上锁的,但Enter()函数不同,当无法获取锁时,不会阻塞线程,而是直接返回false

Leave函数
void CriticalSection::Leave() const RTC_UNLOCK_FUNCTION() 
{
  /*保证上锁和解锁是同一个线程*/
  RTC_DCHECK(CurrentThreadIsOwner());

#if CS_DEBUG_CHECKS
  /*递归次数减一*/
  --recursion_count_;

  RTC_DCHECK(recursion_count_ >= 0);

  if (!recursion_count_)
    thread_ = 0;    /*锁全部解锁以后,将线程置为零。*/
#endif

  /*解锁*/
  pthread_mutex_unlock(&mutex_);
}

Leave()函数用于释放锁。

CritScope类
类的声明
class RTC_SCOPED_LOCKABLE CritScope 
{
 public:
  explicit CritScope(const CriticalSection* cs) RTC_EXCLUSIVE_LOCK_FUNCTION(cs);
  ~CritScope() RTC_UNLOCK_FUNCTION();

 private:
  const CriticalSection* const cs_;     /*托管的锁*/
  RTC_DISALLOW_COPY_AND_ASSIGN(CritScope);
};

CritScope类定义的对象就是一把区域锁CritScope对象需要CriticalSection对象(锁对象)配合使用,CritScope对象仅仅是对锁对象的托管。

区域锁利用的是RAII。定义CritScope对象时,会在其构造器中上锁,在CritScope对象离开其作用域时,其析构器会释放锁。

RTC_DISALLOW_COPY_AND_ASSIGN(CritScope);用于禁用拷贝构造和赋值运算符重载,其展开过程及结果如下:

#define RTC_DISALLOW_COPY_AND_ASSIGN(TypeName) \
  TypeName(const TypeName&) = delete;          \
  RTC_DISALLOW_ASSIGN(TypeName)

#define RTC_DISALLOW_ASSIGN(TypeName) \
  TypeName& operator=(const TypeName&) = delete

展开后的结果:

CritScope(const CritScope&) = delete;
CritScope& operator=(const CritScope&) = delete;
构造器和析构器
CritScope::CritScope(const CriticalSection* cs) 
    : cs_(cs) 
{
  cs_->Enter();
}

CritScope::~CritScope() 
{
  cs_->Leave();
}

CritScope类仅有有参构造器,所以在构造CritScope对象时必须提供CriticalSection对象(锁对象)。

在构造器中上锁,析构器中释放锁,实现了锁对象的托管。

TryCritScope类
class TryCritScope 
{
 public:
  explicit TryCritScope(const CriticalSection* cs);
  ~TryCritScope();

  bool locked() const __attribute__((__warn_unused_result__));

 private:
  const CriticalSection* const cs_;  /*托管的锁*/
  const bool locked_;                /*是否上锁*/
  mutable bool lock_was_called_;     /*是否调用过locked()函数*/
  RTC_DISALLOW_COPY_AND_ASSIGN(TryCritScope);
};

TryCritScope::TryCritScope(const CriticalSection* cs)
    : cs_(cs), locked_(cs->TryEnter()) 
{
  CS_DEBUG_CODE(lock_was_called_ = false);
  RTC_UNUSED(lock_was_called_);
}

TryCritScope::~TryCritScope() 
{
  /*若没有调用过locked()函数,则断言失败。*/
  CS_DEBUG_CODE(RTC_DCHECK(lock_was_called_)); 
  if (locked_)   /*如果之前上锁成功,才释放锁。*/
    cs_->Leave();
}

/*判断是否上锁*/
bool TryCritScope::locked() const 
{
  CS_DEBUG_CODE(lock_was_called_ = true);
  return locked_;
}

TryCritScopeCritScope类似,CritScope类对象在上锁时,无法获取锁时线程被阻塞。TryCritScope对象无法获取锁时,线程不会被阻塞,获取锁失败会直接返回false

生成TryCritScope对象后,可能上锁失败,需要通过TryCritScope类对象调用locked()函数判断上锁是否成功,并且这一步是必须要做的。如果不调用locked()函数,TryCritScope类对象在析构时会断言失败。

GlobalLockPod类
class RTC_LOCKABLE GlobalLockPod 
{
 public:
  void Lock() RTC_EXCLUSIVE_LOCK_FUNCTION();

  void Unlock() RTC_UNLOCK_FUNCTION();

  volatile int lock_acquired;
};

void GlobalLockPod::Lock() 
{
  const struct timespec ts_null = {0};   /*0秒*/

  while (AtomicOps::CompareAndSwap(&lock_acquired, 0, 1)) 
  {
    nanosleep(&ts_null, nullptr);   /*和sleep(0)类似*/
  }
}

void GlobalLockPod::Unlock() 
{
  int old_value = AtomicOps::CompareAndSwap(&lock_acquired, 1, 0);
  
  /*若未上锁就解锁,old_value值为0,断言失败。*/
  RTC_DCHECK_EQ(1, old_value) << "Unlock called without calling Lock first";
}

GlobalLockPod类仅用于保护全局变量,底层实现上也和CriticalSection类不同。

CompareAndSwap函数对lock_acquired的操作是原子操作。

先介绍两个知识点,再说一下GlobalLockPod是如何工作的。

int CompareAndSwap(volatile int* i, int old_value, int new_value) 用于原子的操作i变量,当*i的值和old_value值相等时,将new_value的值赋给*i,这个函数的返回值*i的初始值。
这个函数在逻辑上等价于以下代码:

int CompareAndSwap(volatile int* i, int old_value, int new_value)
{
    int tmp = *i;
    
    if(tmp == old_value)
        *i = new_value;
    
    return tmp;
}

使用示例如下:

#include <iostream>
#include "atomic_ops.h"

using namespace std;

int main()
{
	volatile int i = 10;
	volatile int j = 22;

	int reti = rtc::AtomicOps::CompareAndSwap(&i,10,100);
	int retj = rtc::AtomicOps::CompareAndSwap(&j,20,200);
 
	cout<<"i   = "<<i<<endl;
	cout<<"j   = "<<j<<endl;

	cout<<"reti = "<<reti<<endl;
	cout<<"retj = "<<retj<<endl;

	return 0;
}

在这里插入图片描述

nanosleep(&ts_null, nullptr) 将线程挂起0秒,看似线程挂起0秒毫无意义。nanosleep(&ts_null, nullptr)sleep(0)等价,sleep(0)不是将线程挂起0秒,线程会让出CPU,重新回到就绪队列,再次竞争CPU。
在这里插入图片描述
假设每个线程在CPU中执行的时间片为5ms,若每个线程都充分利用这5ms,如上图。若线程B执行1ms后调用了sleep(0)后,让出了CPU,线程A和线程B重新竞争CPU,每次线程B仅使用CPU1ms。

假设线程A和线程B使用GlobalLockPod类保护的全局变量。在线程上锁之前,GlobalLockPod类的数据成员lock_acquired被设置为0。若线程A先获取CPU的执行权,调用Lock()函数上锁,lock_acquired的值为0,执行到while (AtomicOps::CompareAndSwap(&lock_acquired, 0, 1))时,lock_acquired被设置为了1,AtomicOps::CompareAndSwap(&lock_acquired, 0, 1)返回0,不执行while循环,Lock()执行完毕。线程A获取到了锁。在线程A释放锁之前,线程B获取CPU的执行权,也调用Lock()函数上锁。此时lock_acquired的值为1,执行到while (AtomicOps::CompareAndSwap(&lock_acquired, 0, 1))时,lock_acquired的值不变,AtomicOps::CompareAndSwap(&lock_acquired, 0, 1)返回1,进入while循环。线程B调用nanosleep(&ts_null, nullptr)让出CPU的执行权,重新进入就绪队列,再次竞争CPU的执行权。及时调用nanosleep(&ts_null, nullptr)的好处是避免了忙等,假设线程B有5ms的CPU执行权限,若不调用nanosleep(&ts_null, nullptr)函数,则这5ms的时间将一直执行while循环。若调用nanosleep(&ts_null, nullptr)函数,while执行一次线程B就让出CPU的使用权,这样可以更高效的利用CPU。

在线程A没有释放锁之前,线程B虽然会一直执行while循环,但每次只执行一次while循环就让出CPU,所以效率也是很高的。线程A调用Unlock()函数解锁,lock_acquired被置为0,此时线程B就可以获取到锁跳出while循环。

GlobalLock类
class GlobalLock : public GlobalLockPod 
{
 public:
  GlobalLock();
};

GlobalLock::GlobalLock() 
{
  lock_acquired = 0;
}

GlobalLock在构造器中将lock_acquired置为0,在第一次使用锁之前,需要将lock_acquired置为0。

GlobalLockScope类
class RTC_SCOPED_LOCKABLE GlobalLockScope 
{
 public:
  explicit GlobalLockScope(GlobalLockPod* lock)
      RTC_EXCLUSIVE_LOCK_FUNCTION(lock);
    
  ~GlobalLockScope() RTC_UNLOCK_FUNCTION();

 private:
  GlobalLockPod* const lock_;
  RTC_DISALLOW_COPY_AND_ASSIGN(GlobalLockScope);
};

GlobalLockScope::GlobalLockScope(GlobalLockPod* lock) 
    : lock_(lock) 
{
  lock_->Lock();
}

GlobalLockScope::~GlobalLockScope() 
{
  lock_->Unlock();
}

GlobalLockScope类和CritScope类功能类似都是区域锁CritScope使用的是CriticalSection锁,GlobalLockScope使用的是GlobalLockPod锁。

小结

本文分析了WebRTC中的锁,CriticalSectionGlobalLockPod是使用不同方式实现的两把锁,在这两把锁的基础上提供了多个区域锁CritScopeTryCritScopeGlobalLockScope

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值