0%

线程池的好处

线程的开启也是一个相对缓慢的过程。

  • 如果不用线程池,在系统运行过程中实时开启线程,销毁线程。会极大增加系统的负载。导致系统的实时性减低,业务处理能力也会降低
  • 服务器启动时,就事先创建好线程池,当业务来到时,直接从线程池取出一个线程来执行任务。任务完成后直接把线程归还给池,用于后续重复使用。

线程池的模式

  • 固定大小
  • 可变大小

线程同步

并发编程的基本问题:

  • 互斥
  • 通信

我们希望有一段代码被原子式执行,由于单处理器的任务切换(中断)或者多处理器的并行,我们不能保证。

所以需要一种机制来保证原子性—–锁

操作系统给上层提供了一个API,使得应用程序可以使用锁来达到代码执行的原子性。

1
2
3
4
5
lock_t mutex; // some globally-allocated lock 'mutex'
...
lock(&mutex);
... // 临界区
unlock(&mutex);

调用者调用lock尝试获取锁,如果没有其他现场持有锁,线程就会获得锁,进入临界区。否则,他就会阻塞。直到锁可用。(具体实现比这复杂)

锁的实现原理

锁定实现要求:

  • ​ 提供互斥(保证临界区执行的原子性)
  • 公平性(不会有饿死线程)
  • 性能

方式1:控制中断

缺点:

  • 需要应用进程有特权操作,因为中断需要特权操作
  • 只适合单处理器,多处理器无法保证互斥
  • 会导致中断丢失
  • 效率低。

方式 2:自旋锁

需要硬件指令支持:test and set 指令 (原子交换)

假设无硬件支持,我们很容易想到的一个锁(就是变量)实现是:(测试和设置不是原子操作)

使用一个变量来标志锁是否被占有。这样进程在进入临界区时,会:

  • 检查标志,如果可以进入就进入临界区吗,设置锁标志。不能就阻塞等待。
  • 在退出临界区时,清除标志。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef struct lock_t { int flag; } lock_t;
void init(lock_t *mutex) {
// 0 -> lock is available, 1 -> held
mutex->flag = 0;
}
void lock(lock_t *mutex) {
while (mutex->flag == 1) // TEST the flag
; // spin-wait (do nothing)
mutex->flag = 1;
// now SET it!
}
void unlock(lock_t *mutex) {
mutex->flag = 0;
}

不能保证互斥:

  • 如果线程1在测试时,被中断到线程2,

  • 线程2执行测试并成功设置了标志。此时又被中断

  • 线程1,此时测试也会成功。两者同时进入了临界区

性能问题:

自旋会导致白白浪费CPU资源

test and set 硬件(原子交换)指令,我们可以实现一个满足要求的最简单的锁—-自旋锁

test and set 的原理:

1
2
3
4
5
6
7
// 这就是一个交换操作,只不过硬件保证了他的原子性。
int TestAndSet(int *old_ptr, int new) {

int old = *old_ptr; // 获取旧值
*old_ptr = new;// 设置新值
return old;// 返回旧值。
}

自旋锁的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef struct lock_t
{
int flag;
} lock_t;

void init(lock_t *lock) {
// 0 indicates that lock is available, 1 that it is held
lock->flag = 0;
}
void lock(lock_t *lock) {
while (TestAndSet(&lock->flag, 1) == 1); // spin-wait (do nothing)
}

void unlock(lock_t *lock) {
lock->flag = 0;
}

工作场景模拟:

  • 当调用lock时,如果没有线程持有锁,flag=0,原子交换指令flag=1,返回旧flag=0,循环为假,跳出,获得锁。(这是原子操作,不会被中断)

  • 如果在执行完原子指令之后,被中断(此时flag=1,)别的线程如果执行原子交换指令,while循环一致为真,导致自旋。保证了互斥

  • 只有在第一个线程结束执行,调用unlock时,才会解除自旋。

自旋锁:

保证了互斥,但没有保证公平。

有可能某个线程会一直自旋下去,导致饿死。

compare-and-exchange 硬件支持的原子指令

1
2
3
4
5
6
7
8
// 如果满足期望的值,则会将锁交换。否则,什么也不做。返回锁原来的值。
int CompareAndSwap(int *ptr, int expected, int new)
{
int actual = *ptr;
if (actual == expected)
*ptr = new;
return actual;
}

使用这条指令来实现自旋锁和上面的指令差不多:

1
2
3
void lock(lock_t *lock) {
while (CompareAndSwap(&lock->flag, 0, 1) == 1); // spin
}

在无等待同步(wait-free synchronization)时,这个指令比test and set强大、

链接的加载(load-linked)和条件式存储(store-conditional)指令 原子指令

连接加载:从内存中取出一个值到寄存器,(可以理解为解引用一个地址。)

条件式存储:只有上次从地址A取出的值,自从上次取出之后,没有被人家更改过(也就是说这个值还是那个值),才会去操作这个地址。)

ticket 锁

获取并增加fetch-and-add 原子指令

它能原子地返回特定地址的旧值,并且让该值自增一。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int FetchAndAdd(int *ptr) {
int old = *ptr;
*ptr = old + 1;
return old;
}
typedef struct lock_t {
int ticket;//车票
int turn; //(轮到自己)
} lock_t;

void lock_init(lock_t *lock) {
lock->ticket = 0;
lock->turn 0;
}

void lock(lock_t *lock) {
int myturn = FetchAndAdd(&lock->ticket);
while (lock->turn != myturn); // spin
}

void unlock(lock_t *lock) {
FetchAndAdd(&lock->turn);
}

相当于先买票,再排队。

使用两个变量,构建锁:

获取锁的方式:

  • 先买票(调用获取并增加指令),此时自己的lock->ticket 为1,表示自己有票了。
  • 再去排队(while循环),如果轮到自己了,就可以获得锁。

既可以保证互斥也可以保证公平性。

自旋过长时间的应对方法 —- yield()系统调用

在以上公平互斥锁的基础上,还有一个问题:

如果处于临界区中的线程被中断,其他线程就得长时间自旋等待。

解决办法:

  • yield()系统调用:在线程发现锁被别人持有时,主动调用yield让出锁,是自己进入就绪态。

在两个线程下工作的很好。

缺点:如果在许多线程下反复竞争一把锁时,一个线程长时间持有锁,会导致其他线程全部让出锁,知道持有锁的继续执行。

解决让出过多问题 —- 使用等待队列,睡眠代替自旋。

前面一些方法的真正问题是存在太多的偶然性。调度程序决定如何调度。如果调度不合理,线程或者一直自旋(第一种方法),或者立刻让出 CPU(第二种方法)。无论哪种方法,都可能造成浪费,也能防止饿死。

因此,我们必须显式地施加某种控制,决定锁释放时,谁能抢到锁(排队)。为了做到这一点,我们需要操作系统的更多支持,并需要一个队列来保存等待锁的线程

睡眠与唤醒。

一个线程如果尝试获得锁,失败,自己会陷入睡眠。等到获得锁的线程结束执行时,他会唤醒正在睡眠的线程队列。队列中如果没有人有紧急情况,就会按排队顺序接着下一个线程执行。

例如,Linux 提供了 futex,具体来说,每个 futex 都关联一个特定的物理内存位置,也有一个事先建好的内核队列。调用者通过futex 调用(见下面的描述)来睡眠或者唤醒。
具体来说,有两个调用。调用 futex_wait(address, expected)时,如果 address 处的值等于expected,就会让调线程睡眠。否则,调用立刻返回。调用 futex_wake(address)唤醒等待队列中的一个线程。图 28.9 是 Linux 环境下的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
void mutex_lock(int *mutex)
{
int v;
/*首先测试mutixc值为是否0,如果是,说明没人等待,并且锁是空闲的。也就是说只有我自己想要锁。那我直接拿。并把最高位设置为1.*/

if (atomic_bit_test_set(mutex, 31) == 0)
return;


// 否则,等待者加1.
atomic_increment(mutex);
while (1)
{
// 再次循环尝试获得锁。
if (atomic_bit_test_set(mutex, 31) == 0)
{
// 如果,获得了,等待者减一。
atomic_decrement(mutex);
return;
}
/* We have to wait now. First make sure the futex value
we are monitoring is truly negative (i.e. locked).*/
v = *mutex;
if (v >= 0)
continue;
// 如果执行到这,说明锁已经没了,我们需要等待。
futex_wait(mutex, v);
}
}

void mutex_unlock(int *mutex)
{
/* Adding 0x80000000 to the counter results in 0 if and only if
there are not other interested threads */
if (atomic_add_zero(mutex, 0x80000000))
return;
/* There are other threads waiting for this mutex,
wake one of them up.
*/
futex_wake(mutex);
}

这段代码来自 nptl 库(gnu libc 库的一部分)[L09]中 lowlevellock.h,它很有趣。基本上,它利用一个整数,同时记录锁是否被持有(整数的最高位),以及等待者的个数(整数的其余所有位)。因此,如果锁是负的,它就被持有(因为最高位被设置,该位决定了整数的符号)。这段代码的有趣之处还在于,它展示了如何优化常见的情况,即没有竞争时:只有一个线程获取和释放锁,所做的工作很少(获取锁时测试和设置的原子位运算,释放锁时原子的加法)。

两阶段锁

两阶段锁的第一阶段会先自旋一段时间,希望它可以获取锁。
如果第一个自旋阶段没有获得锁,第二阶段调用者会睡眠,直到锁可用。

计算机系统中的并发

多处理器:有多个处理器插座。

多核:一个处理器插座,只能有一个处理器安装,一个处理器有多个核心(独立执行计算的单元),每个核心可以有多个硬件线程(hart)

何为并发?

并发:单个系统在同时执行多个独立的任务。

  • 硬件并发:一台计算机系统提供多个核心、单个CPU提供多个硬件线程(hart)。
  • 软件并发:单个核心执行任务切换(task switch)。

如果只有纯硬件并发,那么计算有多少硬件线程,就只能执行多少任务。

而软件并发,理论上可以在迷惑应用程序和用户的前提下,“同时”执行多个任务。

因此,现代计算机系统的并发是二者的结合。

硬件并发和软件并发还是有着许多不同:

  • 内存模型的假设
  • 任务切换的负担

并发的方式(手段)

  • 多进程(每个进程只有一个执行线程)
  • 多线程(每个进程有多个执行线程)

多进程并发

每个进程独立获得资源,进程之间互不干扰。

通过进程间通信手段进行通信

  • 信号
  • 共享内存
  • 管道
  • 信号量
  • 消息队列

缺点:

  • 进程切换开销大,导致速度慢
  • 进程管理需要操作系统维持(通过维持某些数据结构)

优点:

  • 各个任务之间独立,安全性高。

多线程并发

在单个进程中执行多个线程。

  • 线程之间共享同一进程的大部分资源
  • 但可以有自己的执行序列(PC寄存器),因此,可以同时独立完成多个任务。

缺点:

  • 同一个进程的共享数据的访问,要被正确同步。

多个单线程/进程间的通信(包含启动)要比单一进程中的多线程间的通信(包括启动)的开销大,若不考虑共享内存可能会带来的问题,多线程将会成为主流语言(包括C++)更青睐的并发途径。此外,C++标准并未对进程间通信提供任何原生支持,所以使用多进程的方式实现,这会依赖与平台相关的API。因此,本书只关注使用多线程的并发,并且在此之后所提到“并发”,均假设为多线程来实现。

并发与并行

不同的关注点

并行关注点:更注重性能,如何利用当前硬件的所有资源,来提高任务处理速度。

并发关注点:任务分离,任务响应。

关注点分离

互相独立的功能模块,可以在同一应用(进程)中分离,并行执行完成的任务,再进行信息的交换(需要手动处理交互逻辑)。

这种思想使得,线程的数量不再依赖CPU中可用(hart)的数量,因为对线程的划分是基于概念上的设计,而不是一种增加吞吐量的尝试(根本目的并不是为了能够处理更多任务,但这样做确实大部分情况下都能增加吞吐量。)。

并发的方式

  • 指令级并行
  • 数据级并行
  • 线程级并性(任务并行)
  • 多机并行

博客搭建的步骤

要搭建个人博客,首先要了解具体要实施哪些步骤,需要哪些工具,在了解了这些之后,再根据自己的需要,就可以很清晰的搭建自己的理想博客了。

前置知识

  • 静态网站和动态网站

静态网站:内容如果不被发布者修改源代码后,重新编译,重新生成。网站上的资源永远不会改变。(比如,一篇博客文章,不与用户有任何数据交互。)

动态网站:底层与数据库交互,有一些放在数据库中的动态资源是可以随着你的请求而产生变化。(比如: 一个登录页面。需要与底层数据库进行数据交换。)

  • 网页生成器

我们在自己的电脑上写完了自己文章之后,我们想要把他放到网上供别人在线查看的话,基本上用户都会使用基于web的浏览器在线浏览你的博客。然而浏览器都是基于html的web页面。对于md格式的文本文件,浏览器无法直接展示在他的界面上。(也不是无法展示,而是他会以纯文本文件的形式展示。),如果你直接把这些文件放到网上(比如一个公开服务器上或者托管平台上)供其他人访问,他们看到的只能是界面丑陋,晦涩生硬的纯文本。

而html就是用来渲染这些文本,让他们更生动(比如有颜色,有图片,格式多样)更方便用户阅读。所以,我们写的博客要是能够转换成web页面,然后再放在网上让别人阅读,那就好了。

我能想到的最笨的写博客的方法就是,你直接把你的内容嵌套到html文件里。这样也就不用任何第三方工具了。(但这既费力又需要很强的知识储备。没人愿意这样干。)

所以静态网站生成器就是干这个工作的:他会把你的md文件,自动嵌入到web页面中。不用你自己手动做这些操作,你要做的就是使用这些工具,遵循他们的格式要求就可以生成一个静态网站了。

  • 部署

上面说的网页生成器所做的事情是转换,资源仍然在你的本地电脑上,我们如何将资源放到网上呢?这个基本常识就是(将资源放到大家可以访问的Ip地址上,也就是自己的网站了。)

能做这件事的有很多:

  • 自己购买一个云服务器,然后,将生成的静态资源直接放服务器的磁盘上,别人就可以通过IP地址访问你的资源。
  • 通过一些托管平台(比如GitPage)本质上和服务器是一样的,只不过是别人帮你托管。
  • 其他方式

完成上面所有步骤,网站部署大致流程就完成了。剩下的就是一些细节问题(比如,如何写博客,如何使用生成器生成自己想要的网站样式(模板))等等问题。

正式搭建

工具:使用md写博客,使用hexo生成网页,使用GItPage托管(部署)网站。

hexo 的使用

在有自己博客的前提下,开始下载hexo,生成静态网页。

  • 安装:hexo是使用js写的平台,需要安装nodejs,并使用nodejs 包管理工具,NPM安装hexo软件。
1
npm install -g hexo-cli
  • hexo是一个博客框架,首先需要生成一个初始博客模板(框架),命令为:
1
2
hexo init blog_dir_name && cd my-blog
# blog_dir_name 是你想要放置你的网站的根目录
  • 这个框架就是hexo为你生成的一个静态网页模板,你可以在本地直接测试他的生成效果。你自己的网页基本也是这样。如果测试效果满意,就可以推送到服务器上,网站就建成了。本地测试命令(需要进入到博客根目录中)。
1
2
3
hexo server 
# 可以简写为hexo s
INFO Hexo is running at http://0.0.0.0:4000/. Press Ctrl+C to stop.

这会在本地启动服务器,你可以通过浏览器访问相应的端口进行访问。

  • 创建你的第一篇文章(帖子)

开始新帖子时,最好使用“草稿”功能。默认情况下,草稿不会发布,因此您可以自由更改其他帖子,同时保持未完成的草稿不公开。以下命令

1
2
hexo new draft "My First Blog Post"
# creates -> ./source/_drafts/My-First-Blog-Post.md

草稿帖子都会放在./source/_drafts/目录下,默认名是你的标题名。

使用 hexo new 命令时,可以使用除“draft”之外的其他模板。查看 ./scaffolds/ 文件夹并阅读 Hexo 文档

  • 要编辑草稿,请导航至 ./source/_drafts/My-First-Blog-Post.md 并使用您最喜欢的 Markdown 编辑器打开该文件。
    让我们在您的新帖子中添加副标题和一些段落内容……
1
2
3
4
5
6
---
title: My First Blog Post
tags:
---
## Hello there
This is some content.

位于 Markdown 文件顶部的破折号之间的内容称为“front-matter”。它是 Hexo 和活动主题使用的元数据。有关更多信息,请参阅 Front-Matter 上的 Hexo 文档

保存对 Markdown 文件的更改将被正在运行的 hexo 服务器自动检测并重新生成为静态 HTML 文件,但您必须刷新浏览器才能查看更改。

如果您不喜欢每次都手动刷新浏览器,hexo-livereload 或 hexo-browsersync 插件可以自动执行此操作。

  • 安装 hexo-browsersync 插件(我个人最喜欢的):
1
2
$ npm install hexo-browsersync --save
$ hexo server --draft --open # 重启服务器

其他 Hexo 插件可以使用 npm 以相同的方式轻松安装。许多插件都具有可以在项目的 _config.yml 文件中进行调整的配置。您需要查阅每个插件的文档以了解其特定的配置属性。对于 hexo-browsersync,默认设置可以正常工作,不需要编辑 _config.yml 文件。

一些写作技巧

  • 显示更少的内容

假设您有一篇很长的文章,并且不喜欢整篇文章显示在列表页面中……
您可以使用 <!-- more --> 在 Markdown 中标记一个位置,以将其从列表页面中隐藏。它将被替换为“阅读更多”链接,该链接将打开文章的其余内容。

1
2
3
4
5
6
7
8
9
---
title: My First Blog Post
tags:
---
This is a summary of the post.
<!-- more -->
## Hello there
This is some content.

  • 插入图像

图像和其他资源文件可以放置在 ./source/ 文件夹下的子目录中。使用这张来自维基百科的原始 A-Team 图片作为测试。下载并保存到此路径:
./source/images/Ateam.jpg

编辑您的原始帖子,插入引用 /images/Ateam.jpg 的 Markdown 图像链接:

1
2
3
4
5
6
7
8
---
title: My First Blog Post
tags:
---
## Hello there
This is some content.

![I love it when a plan comes together.](/images/Ateam.jpg)

您应该在浏览器中看到类似这样的内容:

Assets(就是你的图片什么的东西) can also be organized in folders for each post. It requires enabling the post_asset_folder: true setting in _config.yml and some knowledge of its relative path referencing behavior. See the Hexo documentation on Asset Folders for more information.

  • 发布草稿

当需要将草稿移至“实时”帖子以供全世界查看时,请使用 hexo 发布命令:

1
hexo publish My-First-Blog-Post

运行此命令时会发生一些事情:

  1. markdown 文件 My-First-Blog-Post.md 从 ./source/_drafts/ 移动到 ./source/_posts。

  2. 文件的“front-matter”更改为包含发布日期:

1
2
3
4
5
6
---
title: My First Blog Post
date: 2024-06-02 21:45:16 # <----
tags:
---
...

最后,准备整个站点以进行部署。运行hexo generate命令:

1
2
 hexo generate
# generates -> ./public/

运行网站所需的所有内容都将放置在 ./public 文件夹中。您已准备好将此文件夹传输到您的公共网络服务器或 CDN。

现在您已经了解了一般工作流程,接下来探索一些增强网站的其他方法:

And to see the options for a particular command, use hexo help {command}:

1
2
$ hexo help new
$ hexo help server

参考:

开始使用Hexo博客框架