自己买服务器能在wordpress建网站,深圳宝安区属于什么档次,营销网络图,自己做外贸购物网站目录
前言
一、线程资源区
1.1 线程私有资源
1.2 线程共享资源
1.3 原生线程库
二、线程控制接口
2.1 线程创建
2.1.1 创建一批线程
2.2 线程等待
2.3 终止线程
2.4 线程实战
2.5 其他接口
2.5.1 关闭线程
2.5.2 获取线程ID
2.5.3 线程分离
三、深入理解线程
…目录
前言
一、线程资源区
1.1 线程私有资源
1.2 线程共享资源
1.3 原生线程库
二、线程控制接口
2.1 线程创建
2.1.1 创建一批线程
2.2 线程等待
2.3 终止线程
2.4 线程实战
2.5 其他接口
2.5.1 关闭线程
2.5.2 获取线程ID
2.5.3 线程分离
三、深入理解线程
3.1 理解线程库及线程ID
3.2 理解线程独立栈
3.3 理解线程局部存储 前言 线程是进程内部的一个执行流作为 CPU 运行的基本单位对于线程的合理控制与任务的执行效率息息相关因此掌握线程基本操作线程控制是很有必要的 一、线程资源区
1.1 线程私有资源
Linux 中没有真线程只有复用 PCB 设计思想的 TCB 结构 因此 Linux 中的线程本质上就是 轻量级进程LWP一个进程内的多个线程看到的是同一个进程地址空间所以所有的线程可能会共享进程的大部分资源
但是如果多个执行流多个线程都使用同一份资源如何确保自己的相对独立性呢 相对独立性线程各司其职不至于乱成一锅粥 显然多线程虽然共同 “生活” 在一个进程中但也需要有自己的 “隐私”而这正是 线程私有资源
线程私有资源
线程 ID 内核观点中的 LWP一组寄存器: 线程切换时当前线程的上下文数据需要被保存线程独立栈: 线程在执行函数时需要创建临时变量错误码 errno: 线程因错误终止时需要告知父进程信号屏蔽字: 不同线程对于信号的屏蔽需求不同调度优先级: 线程也是要被调度的需要根据优先级进行合理调度
其中线程 最重要 的资源是 一组寄存器体现切换特性和独立栈体现临时运行特性
这两个资源共同构成了最基本的线程
1.2 线程共享资源
除了上述提到的 线程私有资源 外多线程还共享着进程中的部分资源
共享的定义不需要太多的额外成本就可以实现随时访问资源
基于 多线程看到的是同一块进程地址空间理论上 凡是在进程地址空间中出现的资源多线程都是可以看到的
但实际上为了确保线程调度、运行时的独立性只能共享部分资 这也就是线程中的栈区称作 “独立栈” 的原因某块栈空间属于某个线程其他线程是可以访问的为了确保独立性并不会这样做 在 进程地址空间 中诸如 共享区、全局数据区等 这类天生自带共享属性的区域支持 多线程共享 在 Linux 中多线程共享资源如下
线程共享资源
共享区、全局数据区、字符常量区、代码区: 常规资源共享区文件描述符表: 进行 IO 操作时无需再次打开文件每种信号的处理方式: 多线程共同构成一个整体信号的处理动作必须统一当前工作目录: 即使是多线程也是位于同一工作目录下用户 ID 和 组 ID: 进程属于某个组中的某个用户多线程也是如此
其中线程 较重要 的共享资源是文件描述符表
涉及 IO 操作时多线程 多路转接 非常实用
1.3 原生线程库 在之前编译多线程相关代码时我们必须带上一个选项-lpthread否则就无法使用多线程相关接口 带上这个选项的目的很简单使用 pthread 原生线程库
接下来对 原生线程库 进行一个系统性的理解
首先在 Linux 中是没有真正意义上的线程的有的只是通过进程模拟实现的线程LWP
站在操作系统角度并不会提供对线程控制的相关接口最多提供轻量级进程操作的相关接口
但是对于用户来说只认识线程并不清楚轻量级进程
所以为了使用户能愉快的对线程进行操作就需要对系统提供的轻量级进程操作相关接口进行封装对下封装轻量级进程操作相关接口对上给用户提供线程控制的相关接口
这里很好的体现了计算机界的哲学通过添加一层软件层解决问题 在 Linux 中封装轻量级进程操作相关接口的库称为 pthread 库即 原生线程库这个库文件是所有 Linux 系统都必须预载的用户使用多线程控制相关接口时只需要指明使用 -lpthread 库即可正常使用多线程控制相关接口 二、线程控制接口
2.1 线程创建
要想控制线程得先创建线程对于 原生线程库 来说创建线程使用的是 pthread_create 这个接口
#include pthread.hint pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);先来认识一下函数中涉及的参数
参数1 pthread_t*线程 ID用于标识线程其实这玩意本质上就是一个 unsigned long int 类型
注pthread_t* 表明这是一个输出型参数旨在创建线程后获取新线程 ID
参数2 const pthread_attr_t*用于设置线程的属性比如优先级、状态、私有栈大小这个参数一般不考虑直接传递 nullptr 使用默认设置即可
参数3 void *(*start_routine) (void *)这是一个很重要的参数它是一个 返回值为 void* 参数也为 void* 的函数指针线程启动时会自动回调此函数类似于 signal 函数中的参数2
参数4 void*显然这个类型与回调函数中的参数类型匹配上了而这正是线程运行时传递给回调函数的参数
返回值 int创建成功返回 0失败返回 error number
明白创建线程函数的各个参数后就可以尝试创建一个线程了
#include iostream
#include unistd.h
#include pthread.husing namespace std;void* threadRun(void *arg)
{while(true){cout 我是次线程我正在运行... endl;sleep(1);}return nullptr;
}int main()
{pthread_t t;pthread_create(t, nullptr, threadRun, nullptr);while(true){cout 我是主线程 我创建了一个次线程 t endl;sleep(1);}return 0;
}可以通过 ps -aL 查看正在运行中的线程信息 接下来进行溯源
1.如何验证 原生线程库 存在
现在我们已经得到了一个链接 原生线程库 的可执行程序可以通过 ldd 可执行程序 查看库的链接情况
ldd mythread可以看到原生线程库路径 /lib64/libpthread.so.0 足以证明原生线程库确确实实的存在于我们的系统中
2.为什么打印的次线程 ID 如此长并且与 ps -aL 查出来的 LWP 不一致
很长是因为它本质上是一个无符号长整型至于为什么显示不一致的问题需要到后面才能解答
3.程序运行时主次线程的运行顺序
线程的调度机制源于进程而多进程运行时谁先运行取决于调度器因此主次线程运行的先后顺序不定具体取决于调度器的调度
2.1.1 创建一批线程
#include iostream
#include unistd.h
#include pthread.husing namespace std;#define NUM 5void* threadRun(void *name)
{while(true){cout 我是次线程 (char*)name endl;sleep(1);}return nullptr;
}int main()
{pthread_t pt[NUM];for(int i 0; i NUM; i){// 注册新线程的信息char name[64];snprintf(name, sizeof(name), thread-%d, i 1);pthread_create(pt i, nullptr, threadRun, name);}while(true){cout 我是主线程我正在运行 endl;sleep(1);}return 0;
}细节传递 pthread_create 的参数1时可以通过 起始地址偏移量 的方式进行传递传递的就是 pthread_t*
预期结果打印 thread-1、thread-2、thread-3 …
实际结果确实有五个次线程在运行但打印的结果全是 thread-5 原因char name[64] 属于主线程中栈区之上的变量多个线程实际指向的是同一块空间最后一次覆盖后所有线程都打印 thread-5 这是由于多线程共享同一块区域引发的问题解决方法就是在堆区动态匹配空间使不同的线程读取不同的空间这样就能确保各自信息的独立性
#include iostream
#include unistd.h
#include pthread.husing namespace std;#define NUM 5void* threadRun(void *name)
{while(true){cout 我是次线程 (char*)name endl;sleep(1);}delete[] (char*)name;return nullptr;
}int main()
{pthread_t pt[NUM];for(int i 0; i NUM; i){// 注册新线程的信息char *name new char[64];snprintf(name, 64, thread-%d, i 1);pthread_create(pt i, nullptr, threadRun, name);}while(true){cout 我是主线程我正在运行 endl;sleep(1);}return 0;
}显然线程每次的运行顺序取决于调度器
注意次线程由主线程创建主线程就得对他们负责必须等待他们运行结束类似于父子进程间的等待机制如果不等待就会引发僵尸进程问题不过线程这里没有僵尸线程的概念直接影响就是次线程也全部退出了主线程结束了整个进程的资源都得被释放次线程自然也就无法继续运行了
2.2 线程等待
主线程需要等待次线程在 原生线程库 中刚好存在这样一个接口 pthread_join用于等待次线程运行结束
#include pthread.hint pthread_join(pthread_t thread, void **retval);照例先来看看参数部分
参数1 pthread_t待等待的线程 ID本质上就是一个无符号长整型类型这里传递是数值并非地址
参数2 void**这是一个输出型参数用于获取次线程的退出结果如果不关心可以传递 nullptr
返回值成功返回 0失败返回 error number
函数原型很简单使用也很简单我们可以直接在主线程中调用并等待所有次线程运行结束
#include iostream
#include unistd.h
#include pthread.husing namespace std;#define NUM 5void* threadRun(void *name)
{while(true){cout 我是次线程 (char*)name endl;sleep(1);}delete[] (char*)name;return nullptr;
}int main()
{pthread_t pt[NUM];for(int i 0; i NUM; i){// 注册新线程的信息char *name new char[64];snprintf(name, 64, thread-%d, i 1);pthread_create(pt i, nullptr, threadRun, name);}// 等待次线程运行结束for(int i 0; i NUM; i){int ret pthread_join(pt[i], nullptr);if(ret ! 0)cerr 等待线程 pt[i] 失败! endl;}cout 所有线程都退出了 endl;return 0;
}2.3 终止线程
线程可以被创建并运行也可以被终止线程终止方式有很多种
比如 等待线程回调函数执行结束次线程运行五秒后就结束了然后被主线程中的 pthread_join 等待成功次线程使命完成
void* threadRun(void *name)
{// 只让次线程运行五秒int n 5;while(n--){cout 我是次线程 (char*)name endl;sleep(1);}delete[] (char*)name;return nullptr;
}还有一种方法是 在次线程回调方法中调用 exit() 函数但这会引发一个大问题只要其中一个线程退出了其他线程乃至整个进程都得跟着退出显然这不是很合理不推荐这样玩多线程
void* threadRun(void *name)
{while(true){cout 我是次线程 (char*)name endl;sleep(1);// 直接终止进程退出码设为 10exit(10);}delete[] (char*)name;return nullptr;
}每个线程顶多存活一秒存活在同一秒中就被终止了通过 echo $? 查询最近一次退出码正是 10
其实 原生线程库 中有专门终止线程运行的接口 pthread_exit专门用来细粒度地终止线程谁调用就终止谁不会误伤其他线程
#include pthread.hvoid pthread_exit(void *retval);仅有一个参数 void*用于传递线程退出时的信息
这个参数名叫 retvalpthread_join 中的参数2也叫 retval两者有什么不可告人的秘密吗 答案是这俩其实本质上是同一个东西pthread_join 中的 void **retval 是一个输出型参数可以把一个 void * 指针的地址传递给 pthread_join 函数当线程调用 pthread_exit 退出时可以根据此地址对 retval 赋值从而起到将退出信息返回给主线程的作 所以比较完善的多线程操作应该是这样的
#include iostream
#include unistd.h
#include pthread.husing namespace std;#define NUM 5void* threadRun(void *name)
{cout 我是次线程 (char*)name endl;sleep(1);delete[] (char*)name;pthread_exit((void*)EXIT);// 直接return EXIT 也是可以的// return (void*)EXIT;
}int main()
{pthread_t pt[NUM];for(int i 0; i NUM; i){// 注册新线程的信息char *name new char[64];snprintf(name, 64, thread-%d, i 1);pthread_create(pt i, nullptr, threadRun, name);}// 等待次线程运行结束void *retval nullptr;for(int i 0; i NUM; i){int ret pthread_join(pt[i], retval);if(ret ! 0)cerr 等待线程 pt[i] 失败! endl;cout 线程 pt[i] 等待成功退出信息是 (const char*)retval endl;}cout 所有线程都退出了 endl;return 0;
} 既然线程复用进程的设计思想为什么线程退出时不需要考虑是否正常退出、错误码是什么之类的 因为线程是进程的一部分在进程中获取线程的错误信息等是无意义的前面说过如果一个线程因错误而被终止了那么整个进程也就都活不了了错误信息甄别交给父进程去完成因此 pthread_join 就没必要关注线程退出时的具体状态了如果次线程有信息要交给主线程可以通过 retval 输出型参数获取 2.4 线程实战
无论是 pthread_create 还是 pthread_join他们的参数都有一个共同点包含了一个 void* 类型的参数这就是意味着我们可以给线程传递对象并借此进行某种任务处理
比如我们先创建一个包含一下信息的线程信息类用于计算 [0, N] 的累加和
线程名字包含 ID线程编号线程创建时间待计算的值 N计算结果状态
为了方便访问成员权限设为 public
#include pthread.hint pthread_cancel(pthread_t thread);结论多线程可以传递对象指针自由进行任务处理
2.5 其他接口
2.5.1 关闭线程
线程可以被创建自然也可以被关闭可以使用 pthread_cancel 关闭已经创建并运行中的线程
#include pthread.hint pthread_cancel(pthread_t thread);参数1 pthread_t被关闭的线程 ID
返回值成功返回 0失败返回一个非零的 error number
这里可以直接模拟关闭线程的场景
#include iostream
#include string
#include ctime
#include unistd.h
#include pthread.husing namespace std;void *threadRun(void *arg)
{const char *ps static_castconst char*(arg);while(true){cout 线程 ps 正在运行 endl;sleep(1);}pthread_exit((void*)10);
}int main()
{pthread_t t;pthread_create(t, nullptr, threadRun, (void*)Hello Thread);// 3秒后关闭线程sleep(3);pthread_cancel(t);void *retval nullptr;pthread_join(t, retval);// 细节使用 int64_t 而非 uint64_tcout 线程 t 已退出退出信息为 (int64_t)retval endl;return 0;
}次线程可以自己关闭自己吗答案是可以的但貌似关闭后主线程没有正常等待整个进程一四正常结束退出码为 0次线程可以关闭主线程吗答案是不可以类似于 kill -9 无法终止 1 号进程
2.5.2 获取线程ID
线程 ID 是线程的唯一标识符可以通过 pthread_self 获取当前线程的 ID
#include pthread.hpthread_t pthread_self(void);返回值当前线程的 ID
#include iostream
#include string
#include ctime
#include unistd.h
#include pthread.husing namespace std;void *threadRun(void *arg)
{cout 当前次线程的ID为 pthread_self() endl;return nullptr;
}int main()
{pthread_t t;pthread_create(t, nullptr, threadRun, nullptr);pthread_join(t, nullptr);cout 创建的次线程ID为 t endl;return 0;
}2.5.3 线程分离
父进程需要阻塞式等待子进程退出主线程等该次线程时也是阻塞式等待父进程可以设置为 WNOHANG变成轮询式等待避免自己一直处于阻塞次线程该如何做才能避免等待时阻塞呢
答案是 分离 Detach 线程在被创建时默认属性都是 joinable 的即主线程需要使用 pthread_join 来等待次线程退出并对其进行资源释放实际上我们可以把这一操作留给系统自动处理如此一来主线程就可以不必等待次线程也就可以避免等待时阻塞了这一操作叫做 线程分离 原生线程库 提供的线程分离接口是 pthread_detach
#include pthread.hint pthread_detach(pthread_t thread);参数1 pthread_t待分离的线程 ID
返回值成功返回 0失败返回 error number
线程分离的本质是将 joinable 属性修改为 detach告诉系统线程退出后资源自动释放
注意 如果线程失去了 joinable 属性就无法被 join如果 join 就会报错
接下来简单使用一下 线程分离
#include iostream
#include string
#include ctime
#include unistd.h
#include pthread.husing namespace std;void *threadRun(void *arg)
{int n 3;while(n){cout 次线程 n-- endl;sleep(1);}
}int main()
{pthread_t t;pthread_create(t, nullptr, threadRun, nullptr);pthread_detach(t);int n 5;while(n){cout 主线程 n-- endl;sleep(1);}return 0;
}建议将 pthread_detach 放在待分离线程的 线程创建 语句之后如果放在线程执行函数中可能会因为调度优先级问题引发错误未知结果
线程被创建后谁先执行不确定
总之线程被分离后主线程就可以不必关心了即不需要 join 等待是否分离线程取决于具体的应用场景 三、深入理解线程
3.1 理解线程库及线程ID
在见识过 原生线程库 提供的一批便利接口后不由得感叹库的强大如此强大的库究竟是如何工作的呢 原生线程库本质上也是一个文件是一个存储在 /lib64 目录下的动态库要想使用这个库就得在编译时带上 -lpthread 指明使用动态库 程序运行时原生线程库 需要从 磁盘 加载至 内存 中再通过 进程地址空间 映射至 共享区 中供线程使用 由于用户并不会直接使用 轻量级进程 的接口于是 需要借助第三方库进行封装类似于用户可能不了解系统提供的 文件接口从而使用 C语言 封装的 FILE 库一样
对于 原生线程库 来说线程不止一个因此遵循 先描述再组织 原则在线程库中创建 TCB 结构类似于 PCB其中存储 线程 的各种信息比如 线程独立栈 信息 在内存中整个 线程库 就像一个 “数组”其中的一块块空间聚合排布 TCB 信息而 每个 TCB 的起始地址就表示当前线程的 ID地址是唯一的因此线程 ID 也是唯一的
因此我们之前打印 pthread_t 类型的 线程 ID 时实际打印的是地址不过是以 十进制 显示的可以通过函数将地址转化为使用 十六进制 显示
#include iostream
#include string
#include unistd.h
#include pthread.husing namespace std;string toHex(pthread_t t)
{char id[64];snprintf(id, sizeof(id), 0x%x, t);return id;
}void *threadRun(void *arg)
{cout 我是[次线程]我的ID是 toHex(pthread_self()) endl;return (void*)0;
}int main()
{pthread_t t;pthread_create(t, nullptr, threadRun, nullptr);pthread_join(t, nullptr);cout 我是[主线程]我的ID是 toHex(pthread_self()) endl;return 0;
}线程 ID 确实能转化为地址虚拟进程地址空间上的地址
注意 即便是 C11 提供的 thread 线程库在 Linux 平台中运行时也需要带上 -lpthread 选项因为它本质上是对 原生线程库 的封装
3.2 理解线程独立栈
线程 之间存在 独立栈可以保证彼此之前执行任务时不会相互干扰可以通过代码证明
多个线程使用同一个入口函数并打印其中临时变量的地址
#include iostream
#include string
#include unistd.h
#include pthread.husing namespace std;string toHex(pthread_t t)
{char id[64];snprintf(id, sizeof(id), 0x%x, t);return id;
}void *threadRun(void *arg)
{int tmp 0;cout thread toHex(pthread_self()) tmp: tmp endl;return (void*)0;
}int main()
{pthread_t t[5];for(int i 0; i 5; i){pthread_create(t i, nullptr, threadRun, nullptr);sleep(1);}for(int i 0; i 5; i)pthread_join(t[i], nullptr);return 0;
}
可以看到五个线程打印 “同一个” 临时变量的地址并不相同足以证明 线程独立栈 的存在 存在这么多 栈结构CPU 在运行时是如何区分的呢
答案是 通过 栈顶指针 ebp 和 栈底指针 esp 进行切换ebp 和 esp 是 CPU 中两个非常重要的 寄存器即便是程序启动也需要借助这两个 寄存器 为 main 函数开辟对应的 栈区 除了移动 esp 扩大栈区外还可以同时移动 ebp 和 esp 更改当前所处栈区 所以多线程中 独立栈 可以通过 ebp 和 esp 轻松切换并使用 如果想要在栈区中开辟整型空间可以使用 ebp - 4 定位对应的空间区域并使用其他类型也是如此原理都是 基地址 偏移量 注意
所有线程都要有自己独立的栈结构独立栈主线程中用的是进程系统栈次线程用的是库中提供的栈多个线程调用同一个入口函数回调方法其中的局部变量地址一定不一样因为存储在线程独立栈中
3.3 理解线程局部存储
线程 之间共享 全局变量对 全局变量 进行操作时会影响其他线程
#include iostream
#include string
#include unistd.h
#include pthread.husing namespace std;int g_val 100;string toHex(pthread_t t)
{char id[64];snprintf(id, sizeof(id), 0x%x, t);return id;
}void *threadRun(void *arg)
{cout thread: toHex(pthread_self()) g_val: g_val g_val: g_val endl;return (void*)0;
}int main()
{pthread_t t[3];for(int i 0; i 3; i){pthread_create(t i, nullptr, threadRun, nullptr);sleep(1);}for(int i 0; i 3; i)pthread_join(t[i], nullptr);return 0;
}在三个线程的影响下g_val 最终变成了 103 如何让全局变量私有化呢即每个线程看到的全局变量不同
可以给全局变量加 __thread 修饰修饰之后全局变量不再存储至全局数据区而且存储至线程的 局部存储区中
__thread int g_val 100;“全局变量” 地址变大是因为此时它不再存储在 全局数据区 中而且存储在线程的 局部存储区 中线程的局部存储区位于 共享区并且 共享区 的地址天然大于 全局数据区 注意 局部存储区位于共享区中可以通过 __thread 修饰来改变变量的存储位置