展现 Linux C++服务器端编程的精华

展现 Linux C++服务器端编程的精华

  • 服务器开发准备阶段
    • TCP/IP
    • 伯克利 socket API
    • 安装 Linux Ubuntu (虚拟机)
      • 下载虚拟机
      • 下载 ubuntu 系统
      • 安装 ubuntu 系统
      • XShell
      • Xftp (远程文件传输)
    • 服务器开发所用到的 Shell 命令
      • 通用命令
      • 查看系统上的命令
      • 其他杂项命令
    • 简易 vim
      • vim 初学者命令
      • vim 配置文件与环境变量
  • 服务器开发初始阶段
    • 深入学习 C++
      • 准备: 配置环境
      • 关键字:KEY-WORDS
      • OOP
      • 智能指针
      • STL
      • 多线程编程
        • 什么是多线程编程 ?
        • 多线程的工作:
        • 多线程资源竞争问题
        • 资源竞争问题解决
        • 代码顺序紊乱导致错误的解决办法
          • 互斥锁
          • 死锁
        • 线程间通信
    • 偏僻问题
      • 1. 如何判定一个类中是否有成员?
      • 2. sizeof 对于 类/结构体 的尺寸估测
      • 3. 当传递多动态内存参数时,拒绝在参数传递时就地开辟内存
      • 4. 模拟 扩容(预先足够分配内存)的类对象的动态内存管理
      • 5. 移位超限与进制转换

服务器开发准备阶段

TCP/IP

TCP/IP部分 参考笔者拙文:一小时通关计算机网络( 冲冲冲!!!)

伯克利 socket API

server:
bind
listen
write
close

client:

connect
read
close

安装 Linux Ubuntu (虚拟机)

下载虚拟机

https://www.vmware.com/jp/products/workstation-player/workstation-player-evaluation.html

下载 ubuntu 系统

https://ubuntu.com/download/server
http://mirrors.melbourne.co.uk/ubuntu-releases/

安装 ubuntu 系统

展现 Linux C++服务器端编程的精华_第1张图片
展现 Linux C++服务器端编程的精华_第2张图片

展现 Linux C++服务器端编程的精华_第3张图片

展现 Linux C++服务器端编程的精华_第4张图片

展现 Linux C++服务器端编程的精华_第5张图片

展现 Linux C++服务器端编程的精华_第6张图片

展现 Linux C++服务器端编程的精华_第7张图片

XShell

下载

https://www.netsarang.com/zh/xshell/

使用 Xshell 连接远程服务器

展现 Linux C++服务器端编程的精华_第8张图片

注意:XShell 连接不上服务器的解决办法

  1. 服务器未安装 ssh 工具 或者 ssh 服务未开启

解决办法

  • 检测 ssh server是否启动:ps -e | grep ssh
  • 先更新软件源: sudo apt update
  • 下载ssh 工具: sudo apt-get install -y openssh-server
  1. 网段不同

解决办法,在虚拟网络设置 虚拟网络8 的网关地址与 虚拟机 ip 同一网段
展现 Linux C++服务器端编程的精华_第9张图片
3. 重新默认配置

展现 Linux C++服务器端编程的精华_第10张图片

Xftp (远程文件传输)

XFTP: XFTP 是一个远程文件传输的工具,主要用于将本地文件传输到 终端上

展现 Linux C++服务器端编程的精华_第11张图片

服务器开发所用到的 Shell 命令

通用命令

开机准备

  • who : 查看现在登陆的用户信息
  • sudo (最高权限) apt-get update / upgrade / dist-upgrade : 检测系统更新/ 软件包更新 / 大版本更新
  • sudo add-apt-respository ppa:ubuntu-toolchain-r/test : 更新Ubuntu环境下的最新源
  • 换网络点(提高下载速度)步骤
  1. wget -c (网点 : http://mirrors.163.com/.help/sources.list.trusty) : 下载网点配置
  2. cp sources.list.trusty . : 拷贝配置源文件到当前目录(/etc/apt)
  3. cd /etc/apt , cp sources.list sources.list.bak 备份 原配置文件
  4. cp sources.list.trusty sources.list :替换原配置文件

查看

  • man (Manual) pwd :查看 pwd 指令的文档
  • pwd : 查看当前目录连接
  • ls :查看当前目录下的文件
    ls / :查看根目录文件
    可选择: (-la 详细信息查看,a 查看其 连接计数)
    *: 代表了可查找的信息所有可替换,? : 代表了信息的单个替换
  • stat aaa.txt :查看文件属性
  • file abc.txt :查看文件类型
  • cat :输出文件内容
  • tail (-f, -n + 行数) :查看尾部一定行数的内容

切换目录
- cd :切换目录
- cd / :切换根目录(绝对目录)
- cd (不加 /) 相对目录
- cd … :返回上一目录

创建

  • touch abc.txt :没有则创建文件,有则修改文件时间
  • mkdir abc :创建目录
  • ln :创建链接命令

拷贝

  • cp aaa.txt aaa.bak :拷贝
  • cp hzj.txt hzj.txt :替换
    -i (询问) -l (硬连接) -s (软连接,相当于别名,跨硬盘,删除指向文件,软连接失效)

删除

  • rm hzj.txt :删除文件(不能删除目录)
  • rmdir hzj : 删除目录(目录有文件不能被删除)
  • rm -r doc : 递归删除目录(包括目录中的文件)

修改

  • mv hzj.txt hzj.txt.1 :移动文件(修改文件名字)

查看系统上的命令

  • ps : 输出当前用户的进程
    (-ef)(al) :显式全部进程详细的信息
  • top :实时显示进程信息,系统信息
  • htop : 带图形化的 top
    下载: sudo apt-get install htop (远程服务器下载)
  • ps aux| grep top :列出 top 进程的 aux 序号等信息
  • kill -s(-q :强制结束) INT aut序号 : 给该程序发送结束信号
  • df -h :查看当前盘的使用情况

其他杂项命令

  • grep (global re(正则表达式) print) :查看服务器日志

  • grep apple(re表达式) hzj.txt : 在文件中查找 apple 所在的行,并输出其行内容
    (-c : 输出行 -v :输出不出现给顶表达式的行内容 -n : 输出出现的行号与内容)
    作为管道右侧: ps | grep bash (将 | 管道左侧的输出作为右侧语句的输入)

  • zip :压缩

  • unzip :解包

  • tar

  • wget (-c :下载中断时,下次继续在此继续下载) link(链接)

  • echo $PATH : 打印(其环境变量)

  • printenv :输出所有环境变量

  • chomd +x (-x) hzj.txt :改变文件权限

  • ctrl +z :暂停任务

  • jobs :查看当前任务

  • fg + 任务序号 :恢复任务

  • scp 用户名@主机:/home/handling/main.cpp :内网主机传输文件:

简易 vim

vim 初学者命令

vim 默认是认为我们是只看不编辑的,所以是 命令模式 Normal

当我们键入命令,就能执行对文本的操作:

模式切换:

  • ESC :退回到 Normal 命令模式
  • 命令模式 v : Vitrual模式,移动光标选择范围,运行命令针对选项范围。
  • : 冒号键入命令

插入命令

  • i 插入光标之前
  • a 插入光标之后
  • o 在当前行下插入新行

移动光标:

  • h : 左移

  • j : 下移

  • k : 上移

  • L : 右移动

  • w : 到下一个单词开头

  • e : 到下一个单词结尾

  • ^ : 移动行首非 制表符的字符

  • g_ : 移动行尾 非制表符的字符

  • 0 : 行首

  • $ ; 行尾

  • NG : 跳转到第 N 行

  • gg : 到第一行

  • G : 跳转文本最后一行

  • % : 光标移动到 与之匹配的括号

  • *与# : 匹配当前光标所在的单词, * 是移动到下一个匹配,#是上一个

选择区块:

Visual 模式

  • vi “ : 选择两个 ”“间不包括 引号的内容,
  • va" ; 包含 ”
  • vi ) :选择 () 内容,不包含 ()本身
  • va ) : 包含 ()

ctrl + v 进行块操作,选中块,如果需要插入则 设置大写 I ,进行多行插入,之后 esc 恢复 normal 状态插入成功。

拷贝与粘贴:

  • YY : 拷贝当前行
  • p : 粘贴寄存器的字符行

取消操作与重复操作:

  • u :取消上一次操作
  • ctrl + r :重复取消操作
  • . : 重复上一个改变文本的操作

查找:

  • / 查找的文本(RE) :向下查找
  • ? 查找的文本(RE) : 向上查找
  • n :跳到下一个为止, N :跳到上一个查找位置
  • fh : 行查找下一个字符h出现的位置,F反向
  • th : 到下一个 h 前的字符位置, T反向

删除:

  • x 删除当前光标字符
  • dd :删除光标所在行
  • dt“ : 删除从光标开始的行的内容,直到遇见 “

自动补全:

Insert模式

  • (ctrl + n 或 ctrl + p):能选择匹配到的单词

自动缩进:

  • = :选中区块自动缩进

多行连接:

  • J :选中行连接

分屏操作:

  • vsplit : 分屏
  • ctrl + w +w :光标多屏跳转
  • hide :隐藏光标所在屏
  • b main.h :同时编辑文件时将光标所在屏转到 main.h

退出:

  • wq : 保存退出
  • q!:强制退,不存盘
  • ZZ : 存盘退出

环境变量:

:set hlsearch :高亮查找
:set number : 显式行号

帮助

  • help :vim 使用规则
  • vimtutor :专门介绍 vim 的使用规则

vim 配置文件与环境变量

vim 环境变量配置在用户目录的 .vimrc中

环境变量配置:

服务器开发初始阶段

深入学习 C++

准备: 配置环境

Gcc下载: sudo apt-get install g++
make : 工程管理
cmake :更高的工程管理

关键字:KEY-WORDS

alignas 与 alignof (c++11):

alignas() 是能够显式设置其对齐字节数的。//默认设置为 最大成员字节数为对齐字节数
这种明确规定占用字节大小后,编写代码将更具有跨平台性。

而 alignof 是查看当前对象的 对齐字节数

struct alignas(4) A {
  int a;
  int b;
};

class 与 strcut 采用字节对齐来安排类内的内存,默认以最大的成员字节为 对齐字节数,未满均以对齐字节数的额外差值补齐。

bool:(平台不同影响不同)

建议:

  • 作为条件表达式,使用自身作为条件,而非 bool == false 。。。
  • bool 不要作为函数参数,因为传递的过程是非常难以理解的,使用 enum class 来替换。

char

在linux平台下是 -128 - 128 (signed char)
在其他平台下 0 - 256 (unsigned char)

constexpr : 常量表达式
可以作为函数返回值,或是其他变量等等(只要给定情况下,能在编译期间求值的话,均不会在运行期间求值)

const_cast

出现 const_cast 的情况即代表代码结构本身有问题(打破了正常的规则,对不变的性质做出改变)
展现 Linux C++服务器端编程的精华_第12张图片

nullptr (c++11)与 NULL和 0 的区别与联系

nullptr 是作为空指针类型进行表示的,类型是(std::nullptr_t),目的是在类型推断时避免 使用(NULL 和 0 推断结果的异样)。

  • 0 推断为 int类型
  • NULL 是宏定义,在 c 语言是 (void*)0, 在 c++中 是 0LL

展现 Linux C++服务器端编程的精华_第13张图片
或者

auto  p1 = 0;
decltype(p1) p2;  // p 是int类型

auto p1 = NULL;
decltpe(p1) p2; // p2 是 longlong类型

auto p1 = nullptr;
decltype(p1) p2; // p2 是指针类型:std::nullptr_t

(static_cast) (const_cast ) (reinterpret_cast) (dynamic_cast ) (c like cast) 五种类型转换的区别

四种类型各尽其职务,互相独立,操作之间不会存在相同规则

static_cast : 显式地进行 符合c++程序的 所有隐式转换规则,如 非const 转换为 const, int ->dobule ,可能会损失精度,当然也不能进行 const_cast 操作了,非隐式转换

const_cast : 只能改变运算对象的底层 const, 出现该 类型转换则违背了一定的不进行改变 的规则,需要检查代码是否合理

reinterpret_cast : 它可以无视种族隔离,随便搞。但就像生物的准则,不符合自然规律的随意杂交只会得到不能长久生存的物种。随意在不同类型之间使用reinterpret_cast,也会造成程序的破坏和不能使用。不能进行 const_cast 操作

dynamic_cast :用于将基类的指针或引用安全地转换为派生类的指针或引用。


static_assert (c++11)

在这里插入图片描述
编译时发现条件不满足, 输出 msg, 对一些行为的安全处理与警告信息的传递




OOP

前置声明:

  • 使用前置声明,能避免在头文件定义时使用其他头文件,额外地对 “两个类之间的先后存在”完美地解释。
  • 除非必须引用头文件达到现文件的实现(若在源文件实现,在源文件中引入),否则尽可能少引用,会减缓编译器编译速度

一般来说:

  1. 当实现一个目的时,需要清楚地明白引入的东西的大小或者里面有的东西,这时务必引入(作为基类,类成员或者标准库模板参数)
  2. 当作为 返回值,接口等等 使用前置声明即可

三五原则

在原则上一定要考虑好 自赋值与赋值前内存的管理问题

assert 预处理宏
assrt( expr) :我们使用 assert 处理一些行为上必须正确的事情。
对expr求值,如果表达式为假,assert 输出信息并终止程序的执行,如果表达式为真,则assert什么都不做

析构函数不要被默认合成

析构函数默认是 内联且 noexpect,因为内联展开的析构函数是非常庞大的,因此我们要阻止该合成方式。
内部声明,外部定义且非内联。

析构函数不能抛出异常

析构函数默认是 noexcept,抛出异常,会直接导致程序崩溃, std::terminal
如果我们强制 noexcept(false), 会在程序进行中出现很多不必要的问题。

鼓励构造函数抛出异常

构造函数结束后对象一定要是符合要求的,所以抛出异常是对要求的安全处理,避免后来使用造成不必要的问题。

vitrual 虚函数声明

vitrual 声明要权衡,因为 声明为 virtual 编译器就会对其做额外的事情。



智能指针

  • auto_ptr
  • shared_ptr
  • weak_ptr
  • unique_ptr
  • enable_share_from_this (CRTP)
  1. 智能指针的区别

auto_ptr 是c++11摒弃的智能指针,当指向空或者离开指定作用域时会销毁其管理的内存,存在拷贝操作,会将其内存转交,但如果使用转交后的智能指针会造成崩溃问题。可以说为了避免潜在的内存问题导致崩溃因此 c++11摒弃。

shared_ptr 是共享指针,拷贝其指针会进行内存共享,每个shared_ptr 都会保存引用计数,随着指针置空或者离开作用域销毁,其引用计数递减,递减为 0 则销毁其内存。

shared_ptr 循环引用导致的问题:


#include 
#include 
namespace mynamespace {
class woman;
class man {
  using Ptr = std::shared_ptr<woman>;
 public:
  man() = default;
  ~man() { std::cout << "man is died! "; }
  void set_womanPtr(Ptr p) { womanPtr_ = std::move(p); }
 private:
  Ptr womanPtr_;


};

class woman {
  using Ptr = std::shared_ptr<man>;
 public:
  woman() = default;
  ~woman() { std::cout << "woman is died! "; }
  void set_manPtr(Ptr p) { manPtr_ = std::move(p); }
 private:

  Ptr manPtr_;
};
}

int main() {
  std::shared_ptr<woman> woman_ptr = std::make_shared<woman>();
  std::shared_ptr<man> man_ptr = std::make_shared<man>();
  if(woman_ptr && man_ptr ) {
    woman_ptr->set_manPtr(man_ptr);
    man_ptr->set_womanPtr(woman_ptr);
  }
//woman_ptr 管理了一个woman动态内存 且其成员管理了 man 动态指针
//man_ptr 管理了一个man 动态指针,且其成员管理了 woman 动态指针
//其shared_ptr 的引用计数均为2
//离开作用域先销毁 man_ptr 其 管理man 的智能指针为1  ,woman 动态内存为 2
//离开作用域再销毁 woman_ptr 其 管理man 的智能指针为1  ,woman 动态内存为 1

}

这样两个类之间彼此互相存放对方的 shared_ptr,当进行相互引用时,就会导致循环引用内存无法释放,两个类对象之间的动态内存均不能被有效处理。

为了打破以上循环引用带来的问题:引入了 weak_ptr

weak_ptr 是一种不控制 所指向对象生存期的智能指针,它指向一个 shared_ptr的对象, 将 weak_ptr 绑定到 shared_ptr 不会改变 shared_ptr 的引用计数
一旦绑定的 shared_ptr 被销毁,对象会被释放,即使有 weak_ptr 指向对象,对象依然会被释放,因此 weak_ptr 的名字抓住了智能指针的 ”弱“ 共享对象的特点.

这样使用weak_ptr 作为 循环引用 的 ptr, 销毁该shared_ptr 就会有效地对对其内存进行释放,不会导致循环引用的问题。

unique_ptr : 独一无二指针,一个 unique_ptr ”拥有“ 它所指的对象,只能有一个 unique_ptr 指向一个给顶对象,当unique_ptr被销毁时,该所指的对象也被销毁。

  1. 将类自身传递给智能指针参数的解决办法

enable_share_from_this (CRTP): 从该enable_share_from_this<> 模板派生,就可以将其自身传递给智能指针参数。

使用 share_from_this() 从类到指针的转变。

  1. 智能指针与原生指针的效率与使用智能指针的建议

因为 智能指针是以时间空间效率为代价换取了安全可靠的内存管理,一般来说:

在执行 bool 转换当作条件表达式与解引用两种行为的操作时效率比较为:

  • 原生指针 :1.2
  • 智能指针:1.49
  • weak_ptr : 14 (有 lock 操作,尽量不使用 weak_ptr 来指向 shared_ptr, 代价比较高),weak_ptr 问题很多注意一下。

建议

  • 优先使用类实例,万不得已使用 unique_ptr, 实在不行使用 shared_ptr
  • 使用 const 引用传递共享指针
  • 多线程方面:
  • 其中在初始化智能指针时会进行 两次 new 行为,一次 new 其管理内存,一次 new 自身,因此使用 std::make_shared<>() 融合其new 的过程是时间效率高的。

STL

多线程编程

什么是多线程编程 ?

多线程(multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,提升对CPU的利用率,进而提升整体处理性能。

原理:

实现多线程是采用一种并发执行机制
并发执行机制原理:简单地说就是把一个处理器划分为若干个短的时间片,每个时间片依次轮流地执行处理各个应用程序,由于一个时间片很短,相对于一个应用程序来说,就好像是处理器在为自己单独服务一样,从而达到多个应用程序在同时进行的效果

它能做什么?

多线程的一个典型例子是:用资源管理器复制文件时,一方面在进行磁盘读写操作,同时一张纸不停地从一个文件夹飘到另一个文件夹,这个飘的动作实际上是一段视频剪辑,也就是说,资源管理器能够同时进行磁盘读写和播放视频剪辑。

也可以说,多线程应用于在应用程序分开并整合的工作,一部分在渲染视图,一部分在读写资源。

怎么用

结合各语言的多线程API接口进行编程即可。

可能出现的问题

  • 死锁

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程

  • 乱序

指因为多线程处理机制的问题导致了程序执行顺序的紊乱,其程序的不合理性。

  • 并发访问数据造成的问题

  • 低效率

C++11 新概念

  1. 高阶接口(async, future)
  2. 低阶接口 (thread, mutex)

多线程的工作:

分割问题并分开处理

这里使用一个 分区计算来完成这个工作例子:

使用到的API: Thread, clock,chrono

  • Thread(function)
  • thread.join() :等待该线程完成
  • thread.get_id() :得到 该线程id
  • std::this_thread::get_id() :得到运行该步骤的线程 id
  • std::this_thread::sleep_for(std::chrono::millseconds(100)) 暂停一段时间
  • clock() 计算当前时间,可以用来求程序两处之间的时间差
template <typename Iter ,typename  Fun>
double GetOperatorResult(Iter beg, Iter end, Fun func) {
  double res = 0.0;
  for(; beg != end; ++beg) {
    res += func(*beg);
    std::this_thread::sleep_for(std::chrono::milliseconds(1));
  }

  return res;
}

double HandleData(double data) {
  return sqrt(data) + pow(data, 5);
}

int main() {
  std::vector<double> dou_vec;
  dou_vec.reserve(10000);
  std::default_random_engine random_engine;
  std::uniform_real_distribution<double> real_distribution(1.0, 10000000.0);
  for (int i = 0; i < 10000; ++i) {
    dou_vec.push_back(real_distribution(random_engine));
  }

  auto beg = dou_vec.begin(), end = dou_vec.end();
  auto mid = dou_vec.begin() + dou_vec.size()/2;
  auto pre_sum = 0.0, back_sum = 0.0, thread_sum = 0.0;
  clock_t thread_beg_time = clock(), thread_end_time;

  std::thread t([&]() {
    pre_sum = GetOperatorResult(beg,mid,HandleData);
  });
  back_sum = GetOperatorResult(mid,end,HandleData);
  t.join();
  thread_sum = pre_sum + back_sum;
  thread_end_time = clock();
  std::cout << "thread Id: " << t.get_id() << " cost: " << thread_end_time - thread_beg_time << " result: "
            << thread_sum << std::endl;

  thread_beg_time = clock();
  thread_sum = GetOperatorResult(beg, end, HandleData);
  thread_end_time = clock();
  std::cout << "thread Id: " << std::this_thread::get_id() << " cost: " << thread_end_time - thread_beg_time << " result: "
            << thread_sum << std::endl;
  
  }

结果:

展现 Linux C++服务器端编程的精华_第14张图片
程序运行时可以开启的线程数(系统推荐)

//程序运行时,机器推荐可以开启的线程数量
unsigned int GetThreadCountFromHardWare() {
  return std::thread::hardware_concurrency();
}

多线程资源竞争问题

变量的改变通过:写入寄存器,改变其值,返回到内存,
如果一个线程将变量值写入寄存器,准备改变时,另一个线程也将变量值写入寄存器,再继续更改其值时,彼此线程之间改变的值不能都应用到其变量上,数据改变再返回到内存,会导致一个线程的操作失效的“效果”。

class OperatorCounter {
 public:
  OperatorCounter(): count_(0) { }

  unsigned int count() const { return count_; }
  void add_count() { count_++; }
 private:
  unsigned int count_;
};
void ResourceCompetitionCase() {
  //多线程编程在资源竞争(线程之间共享变量会出现很大的错误)
  std::vector<int> ivec;
  ivec.reserve(10000);
  std::default_random_engine random_engine;
  std::uniform_int_distribution<int> distribution(1, 10000);
  for(int i = 0; i != 1000; ++i) {
    ivec.push_back(distribution(random_engine));
  }

  int total_sum = 0;

  OperatorCounter main_counter;
  for (const auto &item : ivec) {
    total_sum += (item % 10);
    main_counter.add_count();
  }
  std::cout << "total_sum: "<< total_sum
            << "access count: " << main_counter.count() << std::endl;

  total_sum = 0;
  OperatorCounter threads_count;
  using cIter = std::vector<int>::const_iterator;
  cIter cbeg = ivec.cbegin(), cend = ivec.cend();
  cIter iter1 = cbeg + ivec.size()/3;
  cIter iter2 = cbeg + ivec.size()/3*2;


  auto oper_sum_func = []
      (cIter cbeg, cIter cend, OperatorCounter &counter, int &total_sum){
    for (; cbeg != cend; ++cbeg) {
      counter.add_count();
      total_sum += (*cbeg % 9);
    }
  };

  std::thread second_thread([&](){
    oper_sum_func(cbeg, iter1, threads_count, total_sum);
  });
  std::thread third_thread([&](){
    oper_sum_func(iter1, iter2, threads_count, total_sum);
  });

  oper_sum_func(iter2, cend, threads_count, total_sum);

  second_thread.join();
  third_thread.join();

  std::cout << "total_sum: "<< total_sum
            << "access count: " << threads_count.count() << std::endl;


}



资源竞争问题解决

1:不共享资源,每个分割的任务进行独立分配其资源,最终整合一起

class OperatorCounter {
 public:
  OperatorCounter(): count_(0) { }

  unsigned int count() const { return count_; }
  void add_count() { count_++; }
 private:
   int count_;
};

inline void SolveResourceCompetition1() {
  //多线程编程在资源竞争(线程之间共享变量会出现很大的错误)
  std::vector<int> ivec;
  ivec.reserve(10000);
  std::default_random_engine random_engine;
  std::uniform_int_distribution<int> distribution(1, 10000);
  for (int i = 0; i != 100000; ++i) {
    ivec.push_back(distribution(random_engine));
  }

  int total_sum = 0;

  OperatorCounter main_counter;
  for (const auto &item : ivec) {
    total_sum += (item % 10);
    main_counter.add_count();
  }
  std::cout << "total_sum: " << total_sum
            << "access count: " << main_counter.count() << std::endl;

  total_sum = 0;
  OperatorCounter threads_count;
  using cIter = std::vector<int>::const_iterator;
  cIter cbeg = ivec.cbegin(), cend = ivec.cend();
  cIter iter1 = cbeg + ivec.size() / 3;
  cIter iter2 = cbeg + ivec.size() / 3 * 2;

  auto oper_sum_func = []
      (cIter cbeg, cIter cend, OperatorCounter &counter, int &total_sum) {
    for (; cbeg != cend; ++cbeg) {
      counter.add_count();
      total_sum += (*cbeg % 10);
    }
  };

  //对任务分线程片,一片一片使用其独立设定的资源,最后整合输出
  OperatorCounter thread_count1;
  int total_sum1 = 0;
  std::thread second_thread([&]() {
    oper_sum_func(cbeg, iter1, thread_count1, total_sum1);
  });

  OperatorCounter thread_count2;
  int total_sum2 = 0;
  std::thread third_thread([&]() {
    oper_sum_func(iter1, iter2, thread_count2, total_sum2);
  });

  oper_sum_func(iter2, cend, threads_count, total_sum);

  second_thread.join();
  third_thread.join();

  std::cout << "total_sum: " << total_sum + total_sum1 + total_sum2
            << "access count: " << threads_count.count() + thread_count1.count() + thread_count2.count()
            << std::endl;

}

  1. 共享单一数据:原子操作 保证其数据操作过程中的完整性
    变量的改变通过:写入寄存器,改变其值,返回到内存。 原子操作保证其三个步骤绑定执行,从而保证数据的安全。

头文件: atomic


/* 原子操作:头文件 
 * 我们对其数据进行设置原子操作即可:
 *  std::atomic count_;
 *  std::atomic_int count_;
 */
class NewOperatorCounter {
 public:
  NewOperatorCounter(): count_(0) { }

  unsigned int count() const { return count_; }
  void add_count() { count_++; }
 private:
  std::atomic<int> count_;
};

inline void SolveResourceCompetition2() {
  //多线程编程在资源竞争(线程之间共享变量会出现很大的错误)
  std::vector<int> ivec;
  ivec.reserve(10000);
  std::default_random_engine random_engine;
  std::uniform_int_distribution<int> distribution(1, 10000);
  for(int i = 0; i != 100000; ++i) {
    ivec.push_back(distribution(random_engine));
  }

  std::atomic<int> total_sum;     //原子操作的数据
  total_sum = 0;

  NewOperatorCounter main_counter;
  for (const auto &item : ivec) {
    total_sum += (item % 10);
    main_counter.add_count();
  }
  std::cout << "total_sum: "<< total_sum
            << "access count: " << main_counter.count() << std::endl;

  total_sum = 0;
  NewOperatorCounter threads_count;
  using cIter = std::vector<int>::const_iterator;
  cIter cbeg = ivec.cbegin(), cend = ivec.cend();
  cIter iter1 = cbeg + ivec.size()/3;
  cIter iter2 = cbeg + ivec.size()/3*2;


  auto oper_sum_func = []
      (cIter cbeg, cIter cend, NewOperatorCounter &counter, std::atomic<int> &total_sum){
    for (; cbeg != cend; ++cbeg) {
      counter.add_count();
      total_sum += (*cbeg % 10);
    }
  };


  std::thread second_thread([&](){
    oper_sum_func(cbeg, iter1, threads_count, total_sum);
  });
  std::thread third_thread([&](){
    oper_sum_func(iter1, iter2, threads_count, total_sum);
  });

  oper_sum_func(iter2, cend, threads_count, total_sum);

  second_thread.join();
  third_thread.join();

  std::cout << "total_sum: "<< total_sum
            << "access count: " << threads_count.count() << std::endl;


}

代码顺序紊乱导致错误的解决办法

问题引入

使用数据的原子操作之后仍存有的缺陷:只能保证数据自身 的原子操作,但对于代码的有序性仍存在缺陷,不能被保证。
例如 a,b,其都能保证其原子操作,但是当 b 依赖于 a 的值,当 a 进行改变后,下一步b会根据a 的改变改变自身, 但因为a改变后切换线程,a再次被改变的话,此时b的值就有问题。

例如:当 ++a时,切换线程到主线程,主线程调用该函数 ++a,此时 a 被再次改变,新开线程的 b 就会出现错误。

展现 Linux C++服务器端编程的精华_第15张图片

互斥锁

在多任务操作系统中,同时运行的多个任务可能都需要使用同一种资源。比如说,同一个文件,可能一个线程会对其进行写操作,而另一个线程需要对这个文件进行读操作,可想而知,如果写线程还没有写结束,而此时读线程开始了,或者读线程还没有读结束而写线程开始了,那么最终的结果显然会是混乱的。

互斥锁被引入到编程中,用来保护共享数据的完整性,在同一时刻,仅有一个线程能访问被互斥锁标记的对象。

特点:

1. 原子性:把一个互斥量锁定为一个原子操作,这意味着如果一个线程锁定了一个互斥量,没有其他线程在同一时
间可以成功锁定这个互斥量;

2. 唯一性:如果一个线程锁定了一个互斥量,在它解除锁定之前,没有其他线程可以锁定这个互斥量;

3. 非繁忙等待:如果一个线程已经锁定了一个互斥量,第二个线程又试图去锁定这个互斥量,则第二个线程将被挂起
 (不占用任何cpu资源),直到第一个线程解除对这个互斥量的锁定为止,第二个线程则被唤醒并继续执行,
同时锁定这个互斥量。

互斥锁使用不当会导致很多问题:(无线锁死,或者程序崩溃),一般几种情况应该避免:

  • 未开锁前再次加锁
  • 忘记开锁
  • 异常时,不能及时开锁

下面我们谈论以下几个推荐做法:

第一: 正确的加锁方式应当是在执行写操作的具体部分加锁。如果在不需要加锁的地方加锁,或者块程序开始时加锁是违背多线程编程的,其与单线程执行无区别。

第二: mutex 应该声明为 mutable,任何情况下可变,因为加锁开锁(lock 与 unlock)是一个改变其mutex的过程。

第三:异常情况下应该保证其开锁。

在如下异常情况下是不会执行开锁代码的:

展现 Linux C++服务器端编程的精华_第16张图片
异常情况下安全开始的方式:仿类析构安全开锁方法

我们知道当类对象在程序抛出异常时也能保证其资源能得到安全释放(调用析构函数),我们将mutex 做成类的成员,析构函数写入开锁代码,就能保证其 mutex 能在程序异常时安全开锁。

std::lock_guard

简单来理解的话,lock_guard就是一个模板类,它会在其构造函数中加锁,而在析构函数中解锁,也就是说,只要创建一个lock_guard的对象,就相当于lock()了,而该对象析构时,就自动调用unlock()了。

展现 Linux C++服务器端编程的精华_第17张图片

死锁

死锁常常发生在多个进程或线程资源竞争上。

死锁是什么意思呢?举个例子,我和你手里都拽着对方家门的钥匙,我说:“你不把我的钥匙还来,我就不把你的钥匙给你!”,你一听不乐意了,也说:“你不把我的钥匙还来,我也不把你的钥匙给你!”就这样,我们两个人互相拿着对方的钥匙又等着对方先把钥匙拿来,然后就只能一直等着等着等着…最终谁也拿不到自己的锁,这就是死锁。

上节课我们知道利用 mutex 互斥锁能将指定资源进行锁定,仅让一个线程去访问,其他线程均在等待状态,
那么当一个特定区段有多把锁,且多个线程各占据其锁住的资源,造成部分线程一直处于等待其他线程开锁其所需资源的状态,这就引发了死锁的行为。

一个线程将A锁锁上,一个线程将B锁锁上,但当他们继续进行时发现,锁上A锁的线程发现了B锁的存在,需要等待B锁开锁才能继续进行,而锁上B锁的线程发现了A锁的存在,需要等待A锁的开锁才能继续进行,这时两个线程都处于漫长等待引发了死锁。

这里的 Func1 与 Func2 的行为就会引发彼此的线程等待,死锁行为

class NewNewOperatorCounter {

 public:
  NewNewOperatorCounter(): count_(0) { }

  unsigned int count() const { return count_; }
  void add_count() { count_++; }

  void Func1() {
    int i = 1000;
    while(--i) {
      std::lock_guard<std::mutex> lock1(mutex1_);
      std::lock_guard<std::mutex> lock2(mutex2_);
      std::cout << i << std::endl;
    }

  }
  void Func2() {
    int i = 1000;
    while(--i) {
      std::lock_guard<std::mutex> lock1(mutex2_);
      std::lock_guard<std::mutex> lock2(mutex1_);
    }

  }
 private:
  int count_;
  mutable std::mutex mutex1_;
  mutable std::mutex mutex2_;
};
//死锁的演示
void DeadlockDemo() {
  NewNewOperatorCounter operator_counter;
  std::thread t([&operator_counter] { operator_counter.Func1(); });
  operator_counter.Func2();
  t.join();
}

死锁解决办法

出现死锁的原因是 多锁的存在,且锁的顺序安排不妥当,假若两个线程,A线程走的是 a,b 锁的顺序,B线程走的是 b,a锁的顺序,很有可能当 A 线程锁住 a时,B线程锁住 b,之后 A线程想继续访问时发现 b锁被锁上了,B想继续访问发现 a锁被锁上了,这时就会处于一直等待的状态,等待其他线程将资源解锁。

如果锁的顺序都是 a,b 这样 B线程就只能等待 A线程资源使用结束时开锁后在进行访问,就不会造成死锁行为了,

也就是说我们只需要将指定锁进行排序一下,按照特定的顺序进行锁的安放,就能避免死锁行为。

@1 :顺序上锁
展现 Linux C++服务器端编程的精华_第18张图片

@2 :同时上锁

std::lock(mutex, mutex) 锁住一系列锁,按特定序列排序,
std::adopt_lock : 将指示 lock_guard 无须再次上锁。

展现 Linux C++服务器端编程的精华_第19张图片、、、

线程间通信

线程间资源竞争产生的阻塞与响应是非常常见的,如果不能充分去对线程的 阻塞与唤醒 的行为做出管理,CPU 虽然是非常忙碌的但利用率不高的,做的事情大多都是无意义的,不停切换等待。

一般来说,有两种管理线程统一 阻塞与唤醒 的行为:

  • (生产者-消费者们) 当一些功能未完成前,需要将线程统一处于阻塞状态,等正式开工时线程一起启动干活。
  • (生产者-单一消费者)或者特定安排,在特定条件将其余线程阻塞并交出其锁住的资源权,仅响应一个线程完成其工作。

下面我来介绍线程间通信所需的条件变量等等:

std::this_thread::yield()  :将当前线程的CPU 释放,也可以说将 该线程挂起处于等待状态,不使其格外忙碌
condition_variable: 条件变量,处于头文件condition_variable中

condition_variable.notify_one() : 将一个线程唤醒 (通知其一线程)
condition_variable.notify_all() : 将全部线程唤醒(通知全部线程)

cv.wait(unique_lck) : 将当前线程处于阻塞状态,其锁住的资源释放,当被唤醒时退出阻塞状态,重新 锁上资源
cv.wait(unique_lck, Pre): 将当前线程处于阻塞状态,其锁住的资源释放,
仅当被唤醒时其Pre谓词为 true时退出阻塞状态,重新 锁上资源



(生产者-消费者们):干活了,朋友们


volatile std::atomic<bool> signal(false);
std::vector<int> ivec;
std::mutex m_mutex;
std::condition_variable condition_var;

// 使用 条件变量 condition_variable 执行线程间通信,全通知
//其中 signal作为等待的全局变量,
// condition_var.wait(unique_lock);使得当前线程处于阻塞状态,并交出所有锁住的资源权,当被通知时会继续锁住资源
// conditon_var.notify_all() ;全线程唤醒。
void Worker(int id) {
  std::unique_lock<std::mutex> unique_lock(m_mutex);
  while(!signal) {  //等待信号量
    condition_var.wait(unique_lock);  //处于阻塞状态
  }
  std::cout << "thread " << id << std::endl;
}
void GoWork() {
  std::unique_lock<std::mutex> unique_lock(m_mutex);
  signal = true;
  condition_var.notify_one();
}


void InterThreadCommunicationCase() {
  auto kThreadCount = std::thread::hardware_concurrency(); //获取机器支持的线程数
  std::vector<std::thread> thread_container;
  for (int i = 0; i != kThreadCount; ++i) {
    thread_container.emplace_back(Worker, i);
  }

  GoWork();  //干活了
  for (auto &thread : thread_container) {
    if(thread.joinable())
      thread.join();
  }
}

生产者-单一消费者 :通知一个消费者去干活(消费)

//  condition_var.wait(lck,Pre):使得当前线程处于阻塞状态,直到谓词等于 true,在阻塞时交出资源的使用权
// condition_variable.notify_one(): 通知单个线程被唤醒
// 生产者生成10个物品,消费者逐个消费其物品, 物品消费完毕的标志:cargo = 0, 仍在消费shipment_available() == true
int cargo = 0;
bool shipment_available() {return cargo!=0;}

void consume (int n) {
  for (int i=0; i<n; ++i) {
    std::unique_lock<std::mutex> lck(m_mutex);
    condition_var.wait(lck,shipment_available);
    // consume:
    std::cout << cargo << '\n';
    cargo = 0;
  }
}
void MainProducerThread() {

  std::thread consume_thread(consume, 10);
  for (int i = 0; i != 10; ++i) {
    while(shipment_available())
      std::this_thread::yield();
    std::unique_lock<std::mutex> lck(m_mutex);
    cargo = i+1;
    condition_var.notify_one();
  }
  if(consume_thread.joinable())
    consume_thread.join();
}


偏僻问题

1. 如何判定一个类中是否有成员?

答:

  • 引入头文件

#include

class A {
 private:
  size_t  sz_;
};
int main() {
   std::cout<< std::is_empty<A>();
}
  • 创建带有内存成员的派生类模板继承于 该类,之后对比其派生类内存是否与派生类的成员(不包含基类成员)内存之和相同。

注意: 空类的 sizeof 是 1 ,在struct 或 class 中,默认拥有对齐字节(alignof)数,为最大的子元素的字节大小, 假设该 alignof 为 4,那么该struct 的 sizeof 等于 :指定子成员类型前后成员所占总字节(n/4+ n%4 ==0 ? 0: 1)和,加上所有指定子成员类型所占字节数,空类与 只有一个 1 个字节的成员的类 内存相同。


class A {

};

template<typename T>
class TempClass : T {
  char data[10];
};

template<typename T>
bool IsEmptyClass(const T &);

template<typename T>
bool IsEmptyClass(const T &obj) {
  return sizeof(TempClass<T>) == sizeof(char[10]);
}



2. sizeof 对于 类/结构体 的尺寸估测

class A {
 private:

};
int main() {


   std::cout << sizeof(A);
}

  • 如果是空成员类,则类在内存中保留了一个 字节的占位
  • 非空成员类,则类大小等于
struct A {
  char a;
  char d;
  int b;
  char c;
};
  • 在struct 或 class 中,默认拥有对齐字节(alignof)数,为最大的子元素的字节大小, 假设该 alignof 为 4,那么该struct 的 sizeof 等于 ,指定子成员类型前后成员所占总字节(n/4+ n%4 ==0 ? 0: 1)和,加上所有指定子成员类型所占字节数。

3. 当传递多动态内存参数时,拒绝在参数传递时就地开辟内存

class A {
 public:
  A() { throw runtime_error(""); }
};
class B {

};
void print(A *a, B *b) {
  delete a;
  delete b;
}
int main() {
  // 可能会出现 A 与 B  同时分配内存,当A调用构造函数时抛出异常,
  // A的析构未完成(构造失败),A 的内存会被系统回收
  // B 的资源就会消失了
  print(new A(), new B());

}

建议: 尽量以单线程的方式,顺序进行构造完毕后传入指针实参。

4. 模拟 扩容(预先足够分配内存)的类对象的动态内存管理

我们利用 定位 new 表达式,在指定(足够内存的地址)分配类对象,析构时只进行析构,不进行回收内存


class Object {
 public:
  Object() :p_value_(new int(4)){}
  ~Object() { delete p_value_; }
 private:
  int    data_[100];
  int   *p_value_;
};

char info[10000];
int main() {
  //定位new 表达式,只在info 构造,不开辟内存
  Object *s = new(info)Object();
 
  s->~Object();  //显式析构,不释放内存

 // delete  s;  错误,释放了未被构造的内存(多管闲事)


5. 移位超限与进制转换

移位操作:

int a = 0xFF
int b =  a << 33; //超出所能表达的位数 

与上面等价

int b = a << 1;   (a << n   == a << (n % a所表达的位数))

16 进制转换:

0x :代表前缀(标识作用)
ABCDEF: 10 -15

OxFF = F * 16^0 + F * 16 ^1

你可能感兴趣的:(Linux,C++服务器编程,linux,ubuntu,c++,shell,vim)