【C++项目】Tiny Linux WebServer---逻辑单元前置---线程同步

文章目录

  • 前言
  • 逻辑单元
    • 线程同步
      • 前置介绍
      • 互斥量 pthread_mutex_t
      • 条件变量 pthread_cond_t
      • 信号量 sem_t
    • 互斥量、条件变量、信号量之间的差别


前言

上篇【C++项目】Tiny Linux WebServer—webserver架构分析与介绍已经简单介绍了webserver基础架构各个单元部分的简要功能与需要采用的技术。接下来我们将要介绍逻辑单元部分的实现。

【C++项目】Tiny Linux WebServer---逻辑单元前置---线程同步_第1张图片

逻辑单元

逻辑单元主要负责处理I/O输入输出,http请求报文解析等逻辑。而为了提高服务器效率,我们需要采用多线程方式实现逻辑单元的并发实行。而实现多线程并发执行,我们使用线程同步以及线程池技术实现以减少服务器的资源安全以及资源消耗。后面将分别讲解线程同步以及线程池。

线程同步

前置介绍

线程主要优势在于,能够通过全局变量来共享信息,不过这种便捷的共享是有代价的;必须确保多个线程不会同时修改同一变量,或者某一线程不会读取正在由其他线程修改的变量。

线程同步:当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作,而其他线程则处于等待状态

临界区:访问某一共享资源的代码片段,并且这段代码的执行应为原子操作,也就是同时访问同一共享资源的其他线程不应中断该片段的执行

互斥量 pthread_mutex_t

      为避免线程更新共享变量时出现问题,可以使用互斥量(mutex 是 mutual exclusion 的缩写)来确保同时仅有一个线程可以访问某项共享资源。可以使用互斥量来保证对任意共享资源的原子访问

       互斥量有两种状态:已锁定(locked)和未锁定(unlocked)。任何时候,至多只有一个线程可以锁定该互斥量。试图对已经锁定的某一互斥量再次加锁,将可能阻塞线程或者报错失败,具体取决于加锁时使用的方法。

      一旦线程锁定互斥量,随即成为该互斥量的所有者,只有所有者才能给互斥量解锁。一般情况下,对每一共享资源(可能由多个相关变量组成)会使用不同的互斥量,每一线程在访问同一资源时将采用如下协议:
⚫ 针对共享资源锁定互斥量
⚫ 访问共享资源
⚫ 对互斥量解锁
       如果多个线程试图执行这一块代码(一个临界区),事实上只有一个线程能够持有该互斥量(其他线程将遭到阻塞),即同时只有一个线程能够进入这段代码区域,如下图所示:
【C++项目】Tiny Linux WebServer---逻辑单元前置---线程同步_第2张图片

互斥量主要作用是在多线程环境下,对共享资源的互斥访问,从而保护共享资源的安全性。常理解为当线程A希望访问共享资源时,会对共享资源或者临界区加上锁,此时其他线程无法对临界区或者共享资源进行访问。其可以理解为下图:【C++项目】Tiny Linux WebServer---逻辑单元前置---线程同步_第3张图片
Linux中提供一把互斥锁mutex(也称之为互斥量)。每个线程在对资源操作前都尝试先加锁,成功加锁才能操作,操作结束解锁。接下来,介绍相关的mutex系统函数:
【C++项目】Tiny Linux WebServer---逻辑单元前置---线程同步_第4张图片

关键字:restrict 它只可以用于限定和约束指针,并表明指针是访问一个数据对象的唯一且初始的方式.即它告诉编译器,所有修改该指针所指向内存中内容的操作都必须通过该指针来修改,而不能通过其它途径(其它变量或指针)来修改;这样做的好处是,能帮助编译器进行更好的优化代码,生成更有效率的汇编代码。

pthread_mutex_t *restrict mutex = xxx;
pthread_mutex_t * mutex1 = mutex;(报错)
#inculde<pthread.h> //头文件

int pthread_mutex_init(pthread_mutex_t * restrict mutex,const pthread_mutexattr_t *restrict attr);
功能:初始化互斥量
参数:
	mutex:需要初始化的互斥量变量
	attr:互斥量相关的属性,常设为NULL
返回值:
	 int:  0 创建成功   其他任何返回值都表示出现了错误。如果出现以下情况,该函数将失败并返回对应的值。
	 ENOMEM 描述:内存不足,无法初始化互斥锁属性对象。

===================================================================================================
int pthread_mutex_destroy(pthread_mutex_t * mutex);
功能:释放互斥量资源
参数:传递互斥量变量
返回值:
     -  互斥锁销毁函数在执行成功后返回 0,否则返回错误码。
 =================================================================================================
 int pthread_mutex_lock(phtread_mutex_t* mutex);
 功能:对临界区进行加锁,该方法为阻塞线程,如果有一个线程阻塞了,那么其他线程就会等待
 参数:互斥量
 返回值:
    - 0 加锁成功 加锁不成功会阻塞等待
 ================================================================================================
int phtread_mutex_trylock(phtread_mutex_t* mutex);
功能:尝试对临界区进行加锁,如果加锁失败,不会阻塞,会直接返回
参数:
返回值:
- pthread_mutex_trylock() 在成功获得了一个mutex的锁后返回0,否则返回一个错误提示码错误.
- pthread_mutex_trylock() 函数在以下情况会失败:
- [EBUSY] The mutex could not be acquired because it was already locked. mutex已经被锁住的时候无法再获取锁

The pthread_mutex_lock(), pthread_mutex_trylock() and pthread_mutex_unlock() functions may fail if:
[EINVAL] mutex指向的mutex未被初始化
[EAGAIN] Mutex的lock count(锁数量)已经超过 递归索的最大值,无法再获得该mutex
需要注意的是,只有确保在pthread_mutex_trylock()调用成功时,即返回值为0时,才能去解锁它。

=================================================================================================
int pthread_mutex_unlock(pthread_mutex_t *mutex);
功能:对临界区进行解锁
参数:互斥量
返回值:pthread_mutex_unlock返回值
pthread_mutex_unlock()在成功完成之后会返回零。其他任何返回值都表示出现了错误。如果出现以下情况,该函数将失败并返回对应的值。

条件变量 pthread_cond_t

      条件变量使我们可以睡眠等待某种条件出现。
      条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。
条件变量类型为 pthread_cond_t。

      互斥量(互斥锁)用于对共享资源的访问限制,而条件变量则是用于多个线程相互通知关于共享资源访问状态变化的通知。可以理解为当一个线程的动作改变了共享资源的访问状态,从而使得另一个线程可以访问,此时就需要使用条件变量,即当一个线程访问共享资源需要另外一个线程对共享资源状态的改变。

      条件变量的作用是用于多线程之间关于共享数据状态变化的通信。当一个动作需要另外一个动作完成时才能进行,即:当一个线程的行为依赖于另外一个线程对共享数据状态的改变时,这时候就可以使用条件变量

      我们将生产者消费者模型来举例,如果消费者线程发现队列没有产品,那么就会阻塞自己。如果没有条件变量,生产者线程就只能做到将产品放入队列的功能,而无法实现将阻塞的消费者线程激活,而阻塞的消费者线程也无法自己激活。理论消费者线程可以通过轮询方式进行确认队列是否为空。但是,该方案会消耗大量的CPU资源。因此最好的方式是 生产者在放入队列的同时,通知消费者队列非空。
      现在考虑这个实现:消费线程在阻塞之前要先解锁,同时还要将自己的标识符放入一个地方,以便生产线程通过这个标识符来激活自己。这样看起来是没问题了,然而不要忘记了,线程之间是并发/并行的。消费线程可能刚完成解锁的操作,就被生产线程获取到了并开始执行,这时,因为消费线程还未挂起自己,来不及将自己的标识符保存在某个位置,所以生产线程不认为有正在等待的线程。这时,切换到消费线程后,消费线程将永远的等待下去,虽然队列中有产品。而生产线程因为队列中有产品可能也一直的等待下去,形成了死锁。
      解决方法是必须让解锁、保存线程标识符、挂起这一系列操作成为原子操作。这中解决方案就是条件变量,所以不难想到使用条件变量的时候必须要“伴随”一个互斥量。
      条件变量是与互斥量相关联的一种用于多线程之间关于共享数据状态改变的通信机制。它将解锁和挂起封装成为原子操作。等待一个条件变量时,会解开与该条件变量相关的锁,因此,使用条件变量等待的前提之一就是保证互斥量加锁。线程醒来之后,该互斥量会被自动加锁,所以,在完成相关操作之后需要解锁。

#inculde<pthread.h> //头文件int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
功能:初始化一个条件变量
参数:
	- cond是一个指向结构pthread_cond_t的指针,
	- cond_attr是一个指向结构pthread_condattr_t的指针 条件变量相关的属性,常设为NULL
返回值:
	返回值:函数成功返回0;任何其他返回值都表示错误。
==================================================================================================int pthread_cond_destroy(pthread_cond_t *cond);
功能:释放一个条件变量
参数:需要释放的条件变量指针 cond
返回值:
	返回值:函数成功返回0;任何其他返回值都表示错误。	
===================================================================================================int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex); 
功能:等待,调用该函数,线程会阻塞       先解锁后等待,不阻塞后会重新加锁、
参数:
	- cond是一个指向结构pthread_cond_t的指针,
	- 需要解锁的互斥锁
返回值:
	返回值:函数成功返回0;任何其他返回值都表示错误。	
====================================================================================================int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict  abstime);
功能:等待多长时间,调用该函数,线程会阻塞,知道时间结束
参数:
	-cond:条件变量指针
	-mutex:互斥锁(与wait相同)
	-abstime:所需要等待的时间  
返回值:
	返回值:函数成功返回0;任何其他返回值都表示错误。	
==========================================================================================================int pthread_cond_signal(pthread_cond_t *cond);
功能:唤醒一个等待的线程 
调用pthread_cond_signal后要立刻释放互斥锁,因为pthread_cond_wait的最后一步是要将指定的互斥量重新锁住,
如果pthread_cond_signal之后没有释放互斥锁,pthread_cond_wait仍然要阻塞。
参数:
	-cond 条件变量指针
返回值:
	返回值:函数成功返回0;任何其他返回值都表示错误。	
=========================================================================================================int pthread_cond_broadcast(pthread_cond_t *cond);
功能:唤醒所有的等待的线程
参数:
	-cond 条件变量指针
返回值:
	返回值:函数成功返回0;任何其他返回值都表示错误。	

信号量 sem_t

      信号量(Semaphore),有时被称为信号灯,是在多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用。在进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量。为了完成这个过程,需要创建一个信号量VI,然后将Acquire Semaphore VI以及Release Semaphore VI分别放置在每个关键代码段的首末端。确认这些信号量VI引用的是初始创建的信号
      以一个停车场的运作为例。简单起见,假设停车场只有三个车位,一开始三个车位都是空的。这时如果同时来了五辆车,看门人允许其中三辆直接进入,然后放下车拦,剩下的车则必须在入口等待,此后来的车也都不得不在入口处等待。这时,有一辆车离开停车场,看门人得知后,打开车拦,放入外面的一辆进去,如果又离开两辆,则又可以放入两辆,如此往复。在这个停车场系统中,车位是公共资源,每辆车好比一个线程,看门人起的就是信号量的作用。
      抽象的来讲,信号量的特性如下:信号量是一个非负整数(车位数),所有通过它的线程/进程(车辆)都会将该整数减一(通过它当然是为了使用资源),当该整数值为零时,所有试图通过它的线程都将处于等待状态。在信号量上我们定义两种操作: Wait(等待) 和 Release(释放)。当一个线程调用Wait操作时,它要么得到资源然后将信号量减一,要么一直等下去(指放入阻塞队列),直到信号量大于等于一时。Release(释放)实际上是在信号量上执行加操作,对应于车辆离开停车场,该操作之所以叫做“释放”是因为释放了由信号量守护的资源。

#includeint sem_init(sem_t *sem, int pshared, unsigned int value);
功能:信号量初始化 
参数:
	- sem 信号量指针
	- pshared: 是否信号量是用在线程之间还是在进程之间的 0:线程间  非0: 进程
	- value: 信号量的值
返回值:
	成功返回 0
========================================================================================================int sem_destroy(sem_t *sem);
功能:销毁信号量
参数:
	-sem 信号量指针
返回值:
	成功返回 0
========================================================================================================int sem_wait(sem_t *sem);
功能:对信号量加锁 如果大于0,减一并直接返回  如果值为0,阻塞
参数:
	-sem 信号量指针
返回值:
========================================================================================================int sem_trywait(sem_t *sem);
功能:
	试图对sem加锁
参数:
	-sem 信号量指针
返回值:
	成功返回 0
========================================================================================================int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
功能:
	等待abs_timeout时间
参数:

返回值:
	成功返回 0
========================================================================================================int sem_post(sem_t *sem);
功能:解锁一个信号量 对信号量的值加1;如果+1后的信号量值大于0,则会唤醒阻塞的线程或者进程 

参数:
	-sem 信号量指针
返回值:
	成功返回 0
========================================================================================================int sem_getvalue(sem_t *sem, int *sval);
功能:返回信号量的值
参数:
	- sem 信号量指针
	- sval 信号量值存储在该变量中返回
返回值:
	成功读取则返回 0
========================================================================================================

互斥量、条件变量、信号量之间的差别

信号量强调的是线程(或进程)间的同步:“信号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作(大家都在sem_wait的时候,就阻塞在那里)。当信号量为单值信号量是,也可以完成一个资源的互斥访问。
互斥锁(又名互斥量) 强调的是资源的访问互斥:互斥锁是用在多线程多任务互斥的,一个线程占用了某一个资源,那么别的线程就无法访问,直到这个线程unlock,其他的线程才开始可以利用这个资源。比如对全局变量的访问,有时要加锁,操作完了,在解锁。有的时候锁和信号量会同时使用的” 也就是说,信号量不一定是锁定某一个资源,而是流程上的概念,比如:有A,B两个线程,B线程要等A线程完成某一任务以后再进行自己下面的步骤,这个任务并不一定是锁定某一资源,还可以是进行一些计算或者数据处理之类。而线程互斥量则是“锁住某一资源”的概念,在锁定期间内,其他线程无法对被保护的数据进行操作。在有些情况下两者可以互换。
条件变量常与互斥锁同时使用,达到线程同步的目的:条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足。在发送信号时,如果没有线程等待在该条件变量上,那么信号将丢失;而信号量有计数值,每次信号量post操作都会被记录

  1. 互斥锁必须是谁上锁就由谁来解锁,而信号量的wait和post操作不必由同一个线程执行。
  2. 互斥锁要么被锁住,要么被解开,和二值信号量类似
  3. sem_post是各种同步技巧中,唯一一个能在信号处理程序中安全调用的函数
  4. 互斥锁是为上锁而优化的;条件变量是为等待而优化的; 信号量既可用于上锁,也可用于等待,因此会有更多的开销和更高的复杂性
  5. 互斥锁,条件变量都只用于同一个进程的各线程间,而信号量(有名信号量)可用于不同进程间的同步。当信号量用于进程间同步时,要求信号量建立在共享内存区。
  6. 信号量有计数值,每次信号量post操作都会被记录,而条件变量在发送信号时,如果没有线程在等待该条件变量,那么信号将丢失。
    【C++项目】Tiny Linux WebServer---逻辑单元前置---线程同步_第5张图片

你可能感兴趣的:(WebServer,后端)