Link
Free English Class! Topic: Hair! 🧔🏽💇✂️ (Lesson Only)
Words/Phrases
eyelashes
makeup
hairstyle/hairdo
get my hair done
hair gel
buzz cut
helmet
When you have a lot of hair, the helmet makes your hair flat.
hair colouring
Free English Class! Topic: Hair! 🧔🏽💇✂️ (Lesson Only)
eyelashes
makeup
hairstyle/hairdo
get my hair done
hair gel
buzz cut
helmet
When you have a lot of hair, the helmet makes your hair flat.
hair colouring
From ChatGPT:
In general, when a program enters a signal handler in a multi-threaded environment, the behavior regarding other threads depends on how the signal handler is set up and the specific signal that is being handled.
使用脚本一键监查系统是否安装了 TBB
1 | #!/bin/bash |
从包管理器安装:
1 | sudo apt update |
用源码安装
如果需要最新版本,可以从 GitHub 获取 oneTBB
1 | git clone https://github.com/oneapi-src/oneTBB.git |
默认会安装到 /usr/local/include/ 和 /usr/local/lib/。
官方文档:https://uxlfoundation.github.io/oneTBB/
a. 线程池 + 任务队列
b. 工作窃取机制
优点:
特性 | std::thread | TBB |
---|---|---|
** 线程数量 ** | 每个任务可能创建新线程 | 线程池固定线程数 |
** 调度开销 ** | 每次创建 / 销毁线程成本高 | 任务分配成本低,线程重用 |
** 负载均衡 ** | 需要手动管理任务分配 | 自动工作窃取,动态均衡 |
** 粒度控制 ** | 线程粒度粗,任务小不能充分利用 CPU | 任务粒度可以小,TBB 自动调度 |
** 锁竞争 ** | 共享队列 / 资源容易产生大量锁竞争 | 工作窃取减少锁竞争,性能更高 |
TBB 高效的核心原因:
所以,用 TBB 写 parallel_for 或 flow::graph 时,即使任务非常小,CPU 核心也能被充分利用,而直接 std::thread 很可能线程创建成本比任务本身还高,性能反而下降。
task_group 的本质
1 | #include <tbb/tbb.h> |
测试:
1 | $ mkdir build && cd build && cmake .. && make |
从这两行日志可以看出,arena 和 group 重用了同一个线程 ID ,说明它们同属于同一个全局线程池。
1 | Task arena thread 140667167639104 is running. |
进一步,我们发现全局线程池中的线程总数是自适应的,比如本例就是 10
个,既不是 task_group
的 8
个,
也不是 task_arena
的 4
个:
TODO
1 | $ cat result.txt | grep running | sort | uniq | wc -l |
TBB 是基于任务,不是基于线程。但是如果你想修改 TBB 的线程数,有两种方法:
TBB_NUM_THREADS
进行全局设置:1 | export TBB_NUM_THREADS=4 |
TODO: It doesn’t seem to work!
tbb::task_arena
or tbb::task_scheduler_init
(Deprecated) 进行线程隔离。TBB will use this setting locally within the scope of the tbb::task_arena
.
1 | #include <tbb/pipeline.h> |
task_arena 默认 不会去窃取全局线程池的线程
tbb::task_arena 是 局部线程池的抽象,可以指定线程数量和优先级。
在 arena.execute(…) 里执行的任务:
优先使用 task_arena 自己的线程(如果 arena 里有空闲线程)
不会去窃取全局线程池的线程
也就是说,task_arena 内的任务和全局线程池是相对隔离的。
如果 task_arena 内的线程空闲不足,默认不会去全局线程池窃取线程。
但是 TBB 内部可能会将一些未使用的线程调度给 arena,但这属于内部优化,不等同于直接窃取整个全局线程池。
总体原则:arena 控制自己的线程数,不影响全局线程池。
特性 | 全局线程池 | task_arena |
---|---|---|
线程池数量 | 默认一个 | 每个 arena 独立,可自定义线程数 |
窃取行为 | 工作窃取机制(线程之间互窃) | 默认只在 arena 内窃取,不窃取全局 |
嵌套并行 | 嵌套任务复用全局线程 | 嵌套任务复用 arena 线程 |
适用场景 | 大多数并行调用 | 局部控制线程数、避免与全局任务竞争 |
parallel_for
API: parallel_for
my_parallel_for
模拟 parallel_for
的实现:测试结果:
1 | $ ./test_parallel_for |
可见,data 2
是由主线程处理的。也就是说,parallel_for
虽然被称为 a blocking parallel construt,但线程等待所有任务完成期间是非阻塞的,它还可以充当工作线程执行任务池中的任务。
代码模拟 parallel_for
的 wait
:
当追求性能时,推荐以逻辑任务(logical tasks)而不是线程(threads)来编程,有以下原因:
TODO
每个线程都有自己的双端队列,头部称为 top (也称顶部),尾部称为 bottom (也称底部)。
队列的底部是队列的最深处(最末处),底部任务是最新的,顶部任务是最旧的。
深度优先有以下好处:
热点缓存命中
:最新的任务的缓存是最热的,所以优先执行新任务。最小化空间
:广度优先会同时创建指数级数量的共存节点,而深度优先虽然也会创建相同数量的节点,但是只有线性数目的节点会同时共存,因为它创建了其他就绪任务的栈。生产:当线程产生一个任务时,将其放置到线程自身所有的 deque 的尾部。
消费:当线程执行任务时,根据以下规则顺序选取一个任务:
规则 1 被称为 “任务调度绕行(Task Scheduler Bypass)”。
规则 2 是深度优先,这使得当前线程得以不断执行最新的任务直至其完成所有工作。
规则 3 是临时的广度优先,它将潜在的并行转化为实际的并行。
一个任务从产生到被执行涉及以下步骤:
- 将新任务加入线程的 deque 。
- 执行当前任务直至完成。
- 从线程 deque 获取一个任务执行,除非该任务被其他线程窃取走了。
其中,步骤 1 和 步骤 3 会引入不必要的 deque 操作,甚至更糟的是,允许窃取会损害局部性而不会增加显著的并行性。
任务调度器绕行技术可以直接指向下一个要被执行的任务,而不是生产该任务,从而避免了上述问题。
因为根据 “规则 1”,上一个任务产生的新任务会称为第一个备选任务。
此外,该技术几乎保证了该新任务被当前线程执行,而不是其他线程。
注意:当前唯一能使用该优化技术的是使用 tbb::task_group
。
Guiding Task Scheduler Execution
默认情况下,任务计划程序会尝试使用所有可用的计算资源。在某些情况下,您可能希望将任务计划程序配置为仅使用其中的一些资源。
注意:指导任务调度程序的执行可能会导致可组合性问题。
TBB 提供 task_arena
接口,通过以下方式指导任务在 arena (竞技场)内被执行:
如果当前线程被 parallel_for
“阻塞”(不是真正的阻塞,只能称为 a blocking parallel construct),那么该线程被允许拿取第一个循环的任务来执行。这会导致即使是同一个线程内,也可出现乱序执行的情况。在大多数情况下,这没有什么危害。
但是少数情况可能出现错误,例如一个 thread-local 变量可能会在嵌套并行构造之外意外被更改:
在其它场景下,这种行为可能会导致死锁或其他问题。在这些情况下,需要更有力地保证线程内的执行次序。为此,TBB 提供了一些隔离并行构造的执行的方法,以使其任务不会干扰其他同时运行的任务。
其中一种方法是在单独的 task_arena
中执行内层循环:
然而,使用单独的 arena 进行工作隔离并不总是方便的,并且可能会产生明显的开销。为了解决这些缺点,TBB 提供 this_task_arena::isolate
函数,通过限制调用线程仅处理在函数对象范围内(也称为隔离区域)安排的任务,来隔离地运行一个用户提供的函数对象。
当一个线程进入一个任务等待调用或(等待)在一个隔离区域内的阻塞并行结构时,该线程只能执行在该隔离区域内生成的任务及其由其他线程生成的子任务(换句话说,即使子任务是由其他线程生成的,只要属于当前隔离区域,当前线程也可以执行这些任务)。线程被禁止执行任何外层任务或属于其他隔离区域的任务。
下面的示例展示了 this_task_arena::isolate
的使用,以保证在嵌套的并行结构调用时, thread-local 变量不会被意外修改:
** 补充:** 让我们通过一个简单的例子来说明隔离区域内其他线程如何生成子任务,并且这些子任务可以由当前线程执行。
假设我们有一个隔离区域,其中有两个线程:线程 A 和线程 B。我们在这个隔离区域内生成了一些任务,并且这些任务可能会生成子任务。
在这个例子中:
taskA 和 taskB 是在隔离区域内生成的任务。
taskA 生成了两个子任务 Subtask A1 和 Subtask A2。
taskB 生成了两个子任务 Subtask B1 和 Subtask B2。
假设线程 A 执行了 taskA,线程 B 执行了 taskB。在隔离区域内,线程 A 和线程 B 可以执行彼此生成的子任务。例如,线程 A 可以执行 Subtask B1 或 Subtask B2,而线程 B 可以执行 Subtask A1 或 Subtask A2,只要这些子任务属于同一个隔离区域。
严格来说,TBB 并不是完全无锁,但它尽量采用 无锁(lock-free)设计 来提高性能。下面详细说明:
a. 无锁设计的部分
工作窃取双端队列(deque)
并行算法(parallel_for, parallel_reduce)
轻量级任务对象(task)引用计数
总结:TBB 在任务调度和工作窃取上,尽量用无锁和原子操作,保证高性能并发执行。
b. 仍然存在锁的场景
队列扩容(grow)
某些并行容器
全局管理或 arena 初始化
特性 | 是否无锁 | 说明 |
---|---|---|
工作窃取 deque(尾部 push/pop + 头部 steal) | ✅ 无锁(原子操作) | 性能关键部分 |
任务引用计数 | ✅ 原子操作 | 保证任务生命周期安全 |
并行算法内部调度 | ✅ 大部分无锁 | 依赖原子和无锁队列 |
队列扩容 | ⚠️ 使用轻量锁 | 极少发生,性能影响有限 |
并行容器 | ⚠️ 部分锁 | 保障线程安全,复杂容器需要 |
是的,即使你只调用一次 TBB 并行函数,线程池创建后在整个程序运行期间也不会自动销毁或减少线程数。具体说明如下:
TBB 默认全局线程池是在 第一次使用 TBB 并行接口时创建的。
线程数固定(通常等于 CPU 核心数),整个程序运行期间一直存在。
不会因为你长时间没有提交任务而销毁线程,也不会自动减少线程数。
长时间不使用 TBB 时,线程 处于空闲等待状态,不会占用 CPU。
内存占用仍然存在,因为线程堆栈和线程管理结构仍在。
一旦再次调用 TBB 并行函数,线程立即复用,无需重新创建。
如果希望线程在长时间不使用时释放资源,可以:
手动控制线程池生命周期(较复杂,需深入 TBB 内部 API,不推荐)
程序设计上在长时间空闲时退出进程,释放所有线程和内存
一般服务器程序会让线程池持续存在,利用 TBB 的工作窃取和线程复用优势,提高后续任务性能。
✅ 总结
TBB 线程池线程数固定,不会因长时间不使用而减少。
线程空闲时不占用 CPU,但占用内存和线程管理资源。
这种设计是为了 提高任务再次执行的响应速度,适合服务器或长期运行的高性能程序。
如果你的程序有大量 I/O 操作,使用 TBB 的一些特点和注意事项需要特别留意,因为 TBB 是为 CPU 密集型任务 和 任务并行化 设计的。具体分析如下:
结论:TBB 默认不适合大量阻塞 I/O 的场景
问题 | 原因 | 后果 |
---|---|---|
CPU 利用率低 | 阻塞 I/O 占用线程 | 任务等待,性能下降 |
线程不足 | 阻塞线程占用全局线程池 | 并行度降低,任务调度受阻 |
死锁风险 | TBB 线程池线程被长时间阻塞 | 如果任务依赖其他并行任务,可能互相等待 |
a. 分离 I/O 线程
不要在 TBB 线程池中直接做阻塞 I/O
可以开独立线程或线程池专门处理 I/O
TBB 线程只处理 CPU 密集型任务
b. 使用异步 I/O
对网络 / 文件 I/O 使用非阻塞或异步 API(如 asio、io_uring)
任务提交给 TBB 时 立即返回,I/O 完成通过回调或 future 处理
c. task_arena + 限制线程
如果必须在 TBB 线程里做少量阻塞 I/O,可以创建一个 局部 task_arena,限制线程数,避免阻塞全局线程池
d. 混合模型
CPU 密集任务用 TBB
阻塞 I/O 用专门线程池 / 异步框架
最后用 future/promise 或 task_group 协调结果
TBB 是 CPU 密集型并行框架,不适合大量阻塞 I/O
阻塞 I/O 会占用线程池线程,降低并行度
建议:
分离 I/O 线程或线程池
使用异步 I/O
TBB 只处理计算任务
gcc是一个编译套件,包含c、c++、Fortran语言的编译器。
glibc是一个library,为C程序提供基础公共功能,包括系统调用、数学函数和其他核心组件。
Linux平台和vscode似乎都依赖glibc,如果擅自将LD_LIBRARY_PATH
更改为其他版本的glibc路径,则bash会直接crash。
1 | $ cd glibc-v2.34/Linux/RHEL7.0-2017-x86_64/bin && ls |
1 | # 从上可知,ldd是glibc的核心组件之一 |
1 | $ locate libc.so |
Ubuntu平台
1 | sudo apt-get install lib6 |
RedHat平台
1 | sudo yum install glibc |
1 | $ strings /usr/lib/libstdc++.so.* | grep LIBCXX |
如果你有一个使用了libstdc++的特定的binary或application,可以用下面的命令来检查其版本:
1 | $ ldd <your_binary_or_application> | grep libstdc++ |
使用vscode的“Remote SSH”工具试图连接到Linux时,可能会报错如下:
Warning: Missing GLIBCXX >= 3.4.25! from /usr/lib64/libstdc++.so.6.0.19
Warning: Missing GLIBC >= 2.28! from /usr/lib64/libc-2.17.so
Error: Missing required dependencies. Please refer to our FAQ https://aka.ms/vscode-remote/faq/old-linux for additional information.
这是因为Linux系统上的glibc版本中不包含GLIBCXX_3.4.25及以上的版本。此时需要降级vscode(建议做法)或升级glibc(似乎很难)。
See this example
1 | char ** backtrace_symbols (void *const *buffer, int size) |
The return value of backtrace_symbols is a pointer obtained via the malloc function, and it is the responsibility of the caller to free that pointer. Note that only the return value need be freed, not the individual strings.
Question: Why does it say “only the return value need be freed, not the individual strings”?
Let us observe the defintion of the malloc
/free
functions first:
1 | void *malloc( size_t size ); |
free
takes a void*
pointer to deallocate the memory, it doesn’t care what type it is, even if it is a multi-level pointer. It means that malloc
has stored the memory size in some place and free
will find it beforing deallocate the memory.
Let us return the question. The memory pointer returned by backtrace_symbols
is the char**
type, it must be a whole block contigunous memory using malloc
and might be enforced to be transformed as char**
pointer when returing. So when we free
the memory block, the Linux kernel find its actual memory size and deallocate it.
Example:
1 | #include <string.h> |
More elegant but less economical code:
1 | #include <string.h> |
Options:
-t, --field-separator=SEP
use SEP instead of non-blank to blank transition
-k, --key=POS1[,POS2]
start a key at POS1 (origin 1), end it at POS2 (default end of line)
-h, --human-numeric-sort
compare human readable numbers (e.g., 2K 1G)
-n, --numeric-sort
compare according to string numerical value
print the number of processing units avaiable.
read the binary file.
Notes: byte order
1 | $ echo -n "ABCD" | xxd |
Can be used to compare binary or non-binary files.
compare two sorted files line by line.
1 | $ cat file1.txt |
The file must be sorted before using the comm
command. Otherwise it will complain that:
comm: file 1 is not in sorted order
and cannot work correctly. For example,
1 | $ cat file1.txt |
Syntax:
diff -u file1 file2
Options:
-e, --ed
output an ed script
-u, -U NUM, --unified[=NUM]
output NUM (default 3) lines of unified context
(that is, print NUM lines before and after the difference line)
Use a GUI to display the differences.
Prints less information comparing to diff
.
Syntax:
cmp file1 file2
struct/union/array默认支持列表初始化。
Struct and union initialization
Array initialization
1 | #include <bits/stdc++.h> |
1 | #include <bits/stdc++.h> |
[ ]
和test
是bash的内部命令,[[ ]]
是shell的条件判断关键字。
1 | $ type [ |
[ ]
和test
是等价的,用于评估条件表达式。可以使用man [
或help [
查阅帮助文档。
1 | $ help [ |
[[ ]]
关键字可以屏蔽shell特殊符号,比如&&
、||
、>
和<
可以被认为是条件判断符而不是重定向符。
[ ]
中使用-a
和-o
表示逻辑与和逻辑或,[[ ]]
中则使用&&
和||
。
$()
用于命令替换。(( ))
:在比较过程中使用高级数学表达式。请阅读:All about {Curly Braces} in Bash
${}
用于引用变量。
与$var
相比,${var}
是一种消除歧义的措施,比如:
1 | $ var=abc |
{}
表示分组。