【牛客网C++服务器项目学习】Day13-线程同步机制类封装

项目学习地址:【牛客网C++服务器项目学习】

day13

  1. C++模板类的编写

简化编程人员的工作,使得程序员编写的自定义类能够被复用到其他程序代码中去。

使用方法:

1 - 声明和定义
template <typename T> // typename T 该位置可以定义不止一个(map的参数就是两个)
class MyClass
{
	//
    int sum();
};

类模板中的成员函数,放在模板类外去定义的写法
template <typename T>
int MyClass<T>::sum()
{
    //
}

2 - 类的使用
MyClass<int> obj;
MyClass<int> obj();

模板类的声明、定义以及使用就是如此啦。其实我们实际开发中经常会使用到C++内置的几个模板类:vector、map这几个STL容器函数,都是基于模板类编写出来的。

  1. C++异常处理

在之前Linux C的程序编写中,当某个函数执行发生错误后,我们调用perror函数在终端打印输出错误信息。在Linux C++中,对异常错误的处理,则是使用另一套机制:

    • throw
      • 在问题出现时,程序会抛出一个异常。程序在throw语句后立即终止,throw之后的语句不会执行。
      • throw的使用方法:
        • 我们可以使用throw语句在程序的任何位置抛出异常。throw语句的操作数可以是任意的表达式,表达式的结果的类型决定了抛出的异常的类型。
        • C++ 提供了一系列标准的异常,定义在 中,我们可以在程序中使用这些标准的异常。它们是以父子类层次结构组织起来的,如下所示:
        • 【牛客网C++服务器项目学习】Day13-线程同步机制类封装_第1张图片
    • catch
    • try
      • try和catch是与throw一起使用的语句。使用的方式是这样的:;在try语句块中放入需要保护的代码,执行的代码中如果通过throw抛出了异常,会通过catch进行接收。
  1. 回顾一下C++的OOP(静态成员、静态函数、类内声明,类外定义)
    • C++的静态类成员
      • C++中使用static关键字,修饰类内变量或者函数,可以使得类内变量或者函数成为静态变量和静态成员函数,他们具有以下的特点:
        • 类内静态变量和函数被整个类所拥有;
        • 由于被整个类所拥有的,静态的变量或者函数是先于类对象存在的,因此必须在类内声明,在类外进行初始化。
        • 不具有this指针。
        • 因为没有this指针,所以在静态成员函数中无法访问类内的其他普通变量;
        • 不能被virtual修饰成虚函数:因为没有this指针,无法通过虚函数表指针进行访问。虚函数的调用关系:this -> vptr -> ctabel -> virtual function
      • 虚函数的声明和定义方式
class MyClass
{
	//声明
    static int mem;
    static int func1();
};
//定义。无需再加上static关键字
int MyClass::mem = 10;
int MyClass::func1()
{
    //函数体
}
  1. 互斥锁、条件变量和信号量二次回顾

在day08的学习中,掌握了三种方式常用函数的使用方式。牛客网这个项目中,在locker.h中把三种同步方式都封装了,有必要重新总结一下三种同步方式各自的优缺点以及使用场景,这样可以帮助我们更好的理解各自在程序代码中发挥的作用个,甚至让我们可以对代码做出自己的修改

互斥锁:

变量类型:pthread_mutex_t

上锁:

    • pthread_mutex_lock(&m_mutex):对互斥量m_mutex上锁。如果m_mutex为0,代表未上锁,上锁完成后函数返回0;若m_mutex已经被其他进程/线程上锁了,则当前进程/线程被blocking阻塞。
    • pthread_mutex_trylock(&m_mutex):和lock函数基本上一致,唯一的不同在于对已经上过锁的m_mutex调用trylock后,trylock会立即返回 EBUSY, 不会阻塞?

解锁:

    • ptread_mutex_unlock(&m_mutex):对上了锁的m_mutex解锁后,被阻塞的想要对m_mutex上锁的进程/线程会由操作系统进行调度

互斥锁使用起来简单、直观,对临界区的保护是上锁和解锁两种状态。但是,正是因为如此,使用互斥锁对临界区进行保护的话,从进程/线程的角度出发,看到的只有临界区 可进入/不可进入两种状态,进入临界区后,哪些资源可以用,哪些资源不能用,互斥锁是做不到的。

当然,我们可以在进入临界区后,通过一些if语句代码进行判断,但是这样会增加编程的复杂性,这正是互斥锁的局限性,也正是因为互斥锁不能表征临界区资源的具体访问内容,才会有条件变量和信号量的出现。

所以说,互斥锁的使用场景应该是在简单的场景上,进入临界区后,就开始访问资源。要么是1, 要么是0的简单情况。

条件变量:

变量类型:pthread_cond_t

条件阻塞:

    • pthread_cond_wait(&cond, &m_mutex):阻塞当前进程/线程,并把互斥锁释放掉。直到条件cond被pthread_cond_signal函数激活,才会重新执行当前进程/线程,并恢复互斥锁到调用该函数之前的状态。
    • pthread_cond_timedwait(&cond, &m_mutex, &abstime)
    • 等待条件有两种方式:无条件等待pthread_cond_wait()和计时等待pthread_cond_timedwait(),其中计时等待方式如果在给定时刻前条件没有满足,则返回ETIMEOUT,结束等待,其中abstime以与time()系统调用相同意义的绝对时间形式出现,0表示格林尼治时间1970年1月1日0时0分0秒。
      无论哪种等待方式,都必须和一个互斥锁配合,以防止多个线程同时请求pthread_cond_wait()(或 pthread_cond_timedwait(),下同)的竞争条件(Race Condition)。mutex互斥锁必须是普通锁(PTHREAD_MUTEX_TIMED_NP)或者适应锁(PTHREAD_MUTEX_ADAPTIVE_NP),且**在调用pthread_cond_wait()前必须由本线程加锁(pthread_mutex_lock()),**而在更新条件等待队列以前,mutex保持锁定状态,并在线程挂起进入等待前解锁。在条件满足从而离开 pthread_cond_wait()之前,mutex将被重新加锁,以与进入pthread_cond_wait()前的加锁动作对应(这个对应的意思是?在cond_wait中调用lock函数?如果其他线程正在使用锁,就阻塞在此?)。

条件激活:

    • pthread_cond_signal()
    • pthread_cond_broadcast()
    • signal函数最多只能激活一个被条件变量阻塞的进程/线程,如果有多个进程/线程被阻塞住了,由调度算法决定哪一个被激活。broadcast则是把所有的等待进程/线程激活。

条件变量一定和配合着互斥锁来使用的,目的就是弥补互斥锁功能的单一性。在条件不满足的时候阻塞进程/线程(阻止进程/线程访问临界区资源),在临界区资源满足访问要求时,激活一个或者全部进程/线程去访问资源。相较于互斥锁,不需要在临界区内使用if代码去判断,在满足访问要求后,直接使用signal函数激活一个进程/线程即可。

条件变量就像是在互斥锁机制的基础上,打了一个补丁。互斥锁能够做的事情,条件变量也能做,互斥锁做不到的,条件变量兴许能够做。

信号量:

信号量其实就是一个计数器,也是一个整数。每一次调用wait操作将会使semaphore值减一,而如果semaphore值已经为0,则wait操作将会阻塞。每一次调用post操作将会使semaphore值加一。

信号量与线程锁、条件变量相比还有以下几点不同:
1)锁必须是同一个线程获取以及释放,否则会死锁。而条件变量和信号量则不必。
2)信号的递增与减少会被系统自动记住,系统内部信号量的底层有一个计数器保存计数数值,不必担心会丢失,而唤醒一个条件变量时,如果没有相应的线程在等待该条件变量,这次唤醒将被丢失。

互斥锁能做的事情,信号量能不能做?能。初始化信号量将其数值设置为1即可。

条件变量能做的事情,信号量能不能做?能。信号量的数值可以表征可用资源的数量,让进程/线程得以知道哪些资源可以访问,哪些资源不能访问。

所以,我的结论是信号量是三者之中最完善的机制。还没想明白为什么需要封装其他同步机制。

希望学到后面我能回答这个问题。

  1. 防止头文件重复引用的#ifndef/#define/#endif

想必很多人都看过“头文件中用到的 #ifndef/#define/#endif 来防止该头文件被重复引用”。但是是否能理解“被重复引用”是什么意思?头文件被重复引用了,会产生什么后果?是不是所有的头文件中都要加入#ifndef/#define/#endif 这些代码?

  • 其实“被重复引用”是指一个头文件在同一个cpp文件中被include了多次,这种错误常常是由于include嵌套造成的。如:存在a.h文件#include "c.h"而此时b.cpp文件导入了#include “a.h” 和#include "c.h"此时就会造成c.h重复包含。

  • 头文件被重复引用引起的后果:

    • **编译效率降低:**有些头文件重复引用,只是增加了编译工作的工作量,不会引起太大的问题,仅仅是编译效率低一些,但是对于大工程而言编译效率就是很重要的了。
    • **编译错误:**有些头文件重复包含,会引起编译错误,比如在头文件中定义了全局变量或写了函数的实现而不是声明(虽然这种方式不被推荐,但确实是C规范允许的),这种会引起重复定义。
  • 是不是所有的头文件中都要加入这些代码?

    • 不是一定要加,但是不管怎样,用#ifndef/#define/#endif或者其他方式避免头文件重复包含,只有好处没有坏处。培养一个好的编程习惯是学习编程的一个重要分支。所以在写头文件时,最好是把内容都写在#ifndef和#endif之间。

使用方法:

#ifndef __XXX_H__    //意思是  "if not define __XXX_H__" 也就是没包含XXX.h
     
#define __XXX_H__   //就定义__XXX_H__

...  //此处放头文件中本来应该写的代码

#endif       //否则不需要定义

此外,随着编译器的更新,出现了一种新的写法:#pragma once。这种写法只能被较高版本的编译器认可,所以为了保险起见,还是老老实实的写#ifndef吧

你可能感兴趣的:(服务器项目学习,c++,服务器,linux,后端,网络)