上编写和调试多线程程序

本文的环境:
Linux centos-7.shared 3.10.0-693.5.2.el7.x86_64 #1 SMP Fri Oct 20 20:32:50 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux
gcc 版本 4.8.5 20150623 (Red Hat 4.8.5-39) (GCC)

本文使用 POSIX 线程库,需引入头文件 pthread.h,在编译的时候要注意添加 -lpthread 参数。

POSIX 是一个标准,约定一个操作系统应该提供哪些接口,pthread 即为 posix thread。C++11、Python、Java 均内置了线程库。

线程创建

1
2
3
4
5
6
int pthread_create(
pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine) (void *),
void *arg
);

参数:
thread 是一个输出型参数,返回一个线程 id。
attr 可以设置线程属性,填 NULL 表示使用默认属性。
start_routine 是一个函数指针,是线程执行的入口函数。
arg 是 start_routine 的函数参数。
值得注意的是这个函数 arg 是不支持传递多个参数的(可变参数),如果需要传递多个函数就需要使用 struct 或者一些别的方式。

返回值:
成功返回 0,失败返回错误码。

栗子:
创建一个线程,传递一个参数,并在这个新线程内打印这个参数。
在这里插入图片描述

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
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

struct ThreadArg {
int num;
};

void* ThreadEntry(void* arg) {
while (1) {
printf("In ThreadEntry, %lu, arg %d\n", pthread_self(), ((struct ThreadArg*) arg)->num);
sleep(1);
}
}

int main() {
pthread_t tid;
struct ThreadArg ta;
ta.num = 20;
pthread_create(&tid, NULL, ThreadEntry, &ta);
while (1) {
printf("In Main Thread, %lu\n", pthread_self());
sleep(1);
}

return 0;
}

==注意:==
上面代码中使用了 pthread_self() 获取线程的 id,这是 POSIX 线程库提供的库函数,操作系统也提供了一个获取线程 id 的系统调用 gettid()

1
2
pthread_t pthread_self(void);
pid_t gettid(void);

但是当你在同一个线程调用两个函数的时候发现返回的并不是相同的值。

The thread ID returned by this call is not the same thing as a POSIX thread ID.
(i.e., the opaque value returned by pthread_self(3)).
对于单线程的进程,内核中tid==pid,对于多线程进程,他们有相同的pid,不同的tid。tid用于描述内核真实的pid和tid信息。
pthread_self返回的是posix定义的线程ID,man手册明确说明了和内核线程tid不同。它只是用来区分某个进程中不同的线程,当一个线程退出后,新创建的线程可以复用原来的id。

gettid() 和 pthread_self()的区别

线程终止

想让一个线程结束而不终止进程:
1、从线程处理函数 return
2、线程自己调用 pthread_exit 切腹自尽。

1
void pthread_exit(void *retval);

参数:输入 & 输出型参数。
==注意==:pthread_exit 参数所指向的内存单元必须是全局的或者是用malloc 分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。

3、兄弟线程调用 pthread_cancel 终止同一进程中的另一个线程。

1
int pthread_cancel(pthread_t thread);

参数:线程 id
返回值:成功返回 0,失败返回错误码。
==注意==:这个 pthread_cancel 是温和的终止一个线程而不是强制 kill 掉,抽象一个例子就是你在召唤师峡谷杀敌ing,你妈喊你吃饭,你可能得等一会才过去吃饭。

线程等待

为什么要线程等待?
例如,计算一个很大的矩阵相乘,可以使用多线程方式来计算,每个线程计算其中的一部分,最终等待所有的线程执行完,主线程汇总结果,这里就用 pthread_join 来保证逻辑。

1
int pthread_join(pthread_t thread, void **retval);

参数:thread 线程 id,retval 指向一个指针,这个指针指向线程的返回值,不关注线程的返回值可以填 NULL。
返回值:成功返回 0,失败返回错误码。
调用该函数的线程将挂起等待,直到 id 为 thread 的线程终止。thread 线程以不同的方法终止,通过 pthread_join 得到 的终止状态是不同的:
1、如果thread线程通过 return 返回, value_ ptr 所指向的单元里存放的是thread线程函数的返回值。
2、如果thread线程被别的线程调用 pthread_cancel 异常终掉, value_ ptr 所指向的单元里存放的是常数 PTHREAD_CANCELED。
3、如果thread线程是自己调用 pthread_exit 终止的,value_ptr 所指向的单元存放的是传给 pthread_exit 的参数。

线程分离

类似于忽略 SIGCHLD 信号,分离后就代表线程的死活不管了,也就不用 pthread_join 回收了。

1
int pthread_detach(pthread_t thread);

可以自己把自己分离出去,也可以被兄弟线程分离。
pthread_detach(pthread_self());

使用 gdb 调试多线程程序

0、使用 gdb 调试多线程程序

使用 gdb 调试一个程序需要在编译时候加上 ==-g== 选项,为什么?百度去。

1
gdb attach 28966
1
2
3
info thread # 查看所有线程信息
bt # 查看当前线程调用栈
thread 2 # 切换当前线程

在这里插入图片描述

1、查看一个程序的所有线程信息

1
ps -eLf | grep a.out

参数:-L 表示 LWP,这里 ps 得到的线程 id 是和 gettid 一样的。
在这里插入图片描述

2、查看一个程序依赖哪些库

1
ldd a.out

在这里插入图片描述
3、查看一个进程中有几个线程和线程调用栈

1
pstack 27779

在这里插入图片描述


EOF


【操作系统】线程栈如何分配

测试环境:
Linux centos-7.shared 3.10.0-693.5.2.el7.x86_64 #1 SMP Fri Oct 20 20:32:50 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux

一个进程的虚拟地址空间一般可以大致划分为代码区(text)、只读数据区(rodata)、初始化数据区(data)、为初始化数据区(bss)、堆(heap)、共享内存区(.so,mmap的地方)、栈(stack)、内核区(kernel)。

在这里插入图片描述

对于 Linux 进程或者说主线程,其 stack 是在 fork 的时候生成的,实际上就是复制了父亲的 stack 空间地址,然后写时拷贝 (cow) 以及动态增长。
然而对于主线程生成的子线程而言,其 stack 将不再是这样的了,而是事先固定下来的。
线程栈不能动态增长,一旦用尽就没了,这是和生成进程的 fork 不同的地方。

==线程(非主线程)的栈的大小是固定的==,其会在空闲的堆(堆顶附近自顶向下分配)或者是空闲栈(栈底附近自底向上分配),因此线程栈局部函数中分配的变量是存放到各自分配的栈空间,因此可以说是线程私有的,又因为该线程栈的边界是设定好的,因此该线程栈的大小的固定的。

测试

ulimit -a 查看操作系统的相关限制:
可以看到 stack size 的限制是 8192kb 也就是 8MB。
注意这里的 8MB 是指每个被创建的 thread 的 stack 都是这么大。

1
2
3
4
[parallels@centos-7 LinuxCode]$ ulimit -a
......
stack size (kbytes, -s) 8192
......

测试代码:
创建了 3 个 thread,执行 ThreadEntry。
编译后跑起来!

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
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

void*
ThreadEntry(void* args)
{
(void) args;
while (1)
{
sleep(1);
}
}

int main()
{
pthread_t tid1, tid2, tid3;
pthread_create(&tid1, NULL, ThreadEntry, NULL);
pthread_create(&tid2, NULL, ThreadEntry, NULL);
pthread_create(&tid3, NULL, ThreadEntry, NULL);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
pthread_join(tid3, NULL);
return 0;
}

==从 heap 的顶部向下分配。==
ps aux | grep a.out 查看 pid
cat /proc/[pid]/maps 这个显示进程映射了的内存区域和访问权限。
可以看到:在 heap 下面连续的几个属性为 rw-p 的地址大小刚好都为 8192kb。并且每个都在边界穿插了一个大小为 1000H(4096kb) 的边界空间。

在这里插入图片描述

==从 stack 底部向上分配==
ulimit -s unlimited 设置 stack size 为 unlimited,注意虽然设置了stack size为无限,但是实际上其并不是无限的,而也是固定大小的线程栈,大小为1mb。
然后 cat /proc/[pid]/maps 查看虚拟地址空间的映射。
可以看到,这种情况下线程栈是分配在 stack 底附近,自底向上生长的。
在这里插入图片描述
在这里插入图片描述

结论

==不管线程栈是在堆分配还是在栈分配,其都是固定大小的,有边界的。==


参考:
https://blog.csdn.net/qq_16097611/article/details/82592873
https://blog.csdn.net/yangkuanqaz85988/article/details/52403726
https://blog.csdn.net/lijzheng/article/details/23618365

EOF


【操作系统】线程基础知识

本文测试代码的运行环境:
Centos7 x86_64
Kernel 3.10.0-693.5.2.el7.x86_64
gcc 版本 4.8.5
CPU:2 核

线程概念

线程是运行在进程之中的一个处理任务的分支,一个进程都包括一个主线程。
进程:资源分配、管理的基本单位(管理内存、管理打开的文件等)。
线程:调度、执行的基本单位。
在 Linux 中线程也叫做轻量级进程 ==LWP==。
每次创建一个新的进程,会分配一个新的虚拟地址空间。
每次创建一个新的线程,线程共用原来的虚拟地址空间。

从 Linux 内核的角度来说,其实它并没有线程的概念。Linux 把所有线程都当做进程来实现,它将线程和进程不加区分的统一到了 task_struct 中。

线程之间共用的资源

  1. 虚拟地址空间。
  2. 文件描述符表。

线程创建的时候,加上了 CLONE_VM 标记,这样==线程的内存描述符将直接指向父进程的内存描述符。==

1
2
3
4
5
if (clone_flags & CLONE_VM) {
// current 是父进程而 tsk 在 fork() 执行期间是共享子进程
atomic_inc(&current->mm->mm_users);
tsk->mm = current->mm;
}

线程之间不共用的资源

  1. 栈。
  2. 上下文信息(寄存器信息)。
  3. errno(每个线程有自己单独的错误码)。

对于 Linux 进程或者说主线程,其 stack 是在 fork 的时候生成的,实际上就是复制了父亲的 stack 空间地址,然后写时拷贝 (cow) 以及动态增长。
然而对于主线程生成的子线程而言,其 stack 将不再是这样的了,而是事先固定下来的。
线程栈不能动态增长,一旦用尽就没了,这是和生成进程的 fork 不同的地方。

线程的优点

相比于进程来说:

  1. 创建和销毁开销更小。
  2. 切换调度的开销更小。
  3. 线程占用的资源更小。

多线程程序能够充分利用多核处理器。
栗子:==因为我的虚拟机是 2 核的,所以 CPU 最多使用是 200%==
在这里插入图片描述

线程的缺点

  1. 程序的健壮性降低,一个线程的异常终止会导致整个进程异常终止。
  2. 编程 && 调试难度增加(引入了线程安全问题)。

线程的用途

  1. 提升 CPU 密集型程序执行效率。
  2. 提高 IO 密集型程序的体验。
    • 通过网络进行 IO。
    • 响应 UI 界面。

EOF


Redis基础

Redis 5.0.7。

Redis 常用命令、5 种数据类型。

阅读更多
Docker实现基础技术

Docker 是一个使用 Linux Namespace 和 Cgroups 的虚拟化工具。

Linux Namespace 和 Cgroups 是什么?有什么用?在 Docker 中是怎么被使用的?

阅读更多
从B站偷小火箭

非专业前端,自己写不出类似的动画,也懒得看,就直接取现成的吧~ 效果如下:

QQ20200111-220049-HD.gif

阅读更多
FFmpeg常用命令
  1. 基本信息查询命令

  2. 录制命令

  3. 分解/复用命令(对不同文件格式的转化)

  4. 处理原始数据命令

  5. 裁剪与合并命令

  6. 图片/视频互转命令

  7. 直播相关命令(推流)

  8. 各种滤镜命令
阅读更多
WebRTC之实现1v1音视频通话

打通一下 1v1 音视频通话的流程。

阅读更多
WebRTC端对端传输基本流程

WebRTC 端对端连接的基本流程解析。

阅读更多
WebRTC获取音视频设备

使用 navigator.mediaDevices.enumerateDevices() 即可获取音视频设备相关信息。

创建如下文件:

1
2
3
.
├── client.js
└── index.html
阅读更多