广州市南沙住房和建设局网站,网络推广团队分工,山东企业网站备案,附近推广用哪些软件文章目录 1. 线程概念什么是线程Linux中的线程线程的优点线程的缺点线程的独立资源和共享资源 2. 线程控制Linux的pthread库用户级线程 #x1f4dd; 个人主页 #xff1a;超人不会飞)#x1f4d1; 本文收录专栏#xff1a;《Linux》#x1f4ad; 如果本文对您有帮助… 文章目录 1. 线程概念什么是线程Linux中的线程线程的优点线程的缺点线程的独立资源和共享资源 2. 线程控制Linux的pthread库用户级线程 个人主页 超人不会飞) 本文收录专栏《Linux》 如果本文对您有帮助不妨点赞、收藏、关注支持博主我们一起进步共同成长 1. 线程概念
什么是线程
理解线程需要和进程的概念紧密联系。
线程是一个执行分支执行粒度比进程更细调度成本更低进程是分配系统资源的基本单位线程是CPU调度的基本单位。线程是运行在进程中的一个执行流本质上是在进程的地址空间中运行一个进程至少包含一个线程称为主线程。
Linux中的线程 线程是操作系统中的抽象概念用于实现多任务并发执行。不同的操作系统可以有不同的线程实现方法和模型。例如在Windows操作系统中与进程PCB对标的构建了描述线程的数据结构 —— 线程控制块但这样子设计有以下几个缺点 创建线程在Windows中开销较大因为它涉及到较多的内核资源和数据结构的分配线程与进程无法统一组织起来线程的调度效率低 Linux的设计者发现线程控制块与进程控制块PCB大部分描述属性相同且进程与其内部创建的线程看到的都是同一个地址空间。因此在Linux中线程控制块直接复用了PCB的代码也就是说Linux底层并没有真正的“线程”这种复用之后的线程称之为轻量级进程。 每个轻量级进程后面直接称为线程都有自己的一个编号——LWP同一个进程中的各个线程具有相同的PID。
那我们之前讨论的进程是什么这里都是轻量级进程的话需要另有一个进程PCB来管理整个进程吗 答案是不用。事实上在Linux中因为每个进程都至少有一个线程即主线程主执行流这个线程的LWP和PID是相同的因此我们之前讨论的进程PCB实际上就是这个主线程的task_struct。 ps -aL命令查看系统中的轻量级进程。
测试在一个进程中创建了10个线程并用ps -aL命令查看。可以看到有一个主线程和10个新线程主线程的PID和LWP相同。 线程的调度成本低于进程是因为同一个进程中的线程共享同一个地址空间因此这些线程的调度只需要保存和更改一些上下文信息、CPU寄存器即可如pc指针。而进程的调度需要修改较多的内存资源如页表、地址空间等而开销更大的是修改cache缓存的数据。 cache缓存 CPU内部的高速存储器中保存着一些频繁访问的指令和数据基于局部性原理这些数据可能是未来将要被访问的也可能是当前正在访问的。这么做的目的是减少CPU与内存的IO次数以便快速响应CPU的请求而不必每次都从较慢的内存中获取数据。不同进程的cache缓存数据是不同的因此调度进程是需要切换这部分数据而同一个进程的不同线程的cache缓存相同。 CPU根据PID和LWP的对比区分当前调度是线程级还是进程级进而执行对应的调度策略。
线程的优点
线程占用的资源比进程少很多因此创建线程的开销比创建进程小线程的调度成本低于进程调度线程切换时OS的工作量小充分利用多处理器的可并行数量在等待慢速I/O操作结束的同时程序可执行其他的计算任务计算密集型应用为了能在多处理器系统上运行将计算分解到多个线程中实现I/O密集型应用为了提高性能将I/O操作重叠。线程可以同时等待不同的I/O操作。
线程的缺点
性能损失。 一个很少被外部事件阻塞的计算密集型线程往往无法与其它线程共享同一个处理器。 如果计算密集型线程的数量比可用的处理器多那么可能会有较大的性能损失这里的性能损失指的是增加了额外的同步和调度开销而可用的资源不变。 例如有10个处理器11个线程一对一的关系被破坏后多出来的线程就增加了额外的调度开销。复杂性和错误难以调试。 多线程编程涉及到共享资源、并发访问和同步等问题这增加了程序的复杂性。健壮性降低。 编写多线程需要更全面更深入的考虑在一个多线程程序里因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的换句话说多线程之间是缺乏保护的。
⭕补充 线程发生异常如野指针、除零错误等会导致线程崩溃进而引发整个进程退出。从宏观角度因为线程是进程的一个执行分支线程干的事就是进程干的事因此线程异常相当于进程异常进程就会退出。从内核角度线程出错OS发送信号给进程而不是单发给线程。 线程的独立资源和共享资源 进程是资源分配的基本单位线程是调度的基本单位。一个进程中的多个线程共享线程数据当然也有自己独立的数据。 线程的独立资源
栈寄存器中的上下文信息线程ID在Linux中表现为LWPerrno信号屏蔽字和未决信号集调度优先级
线程的共享资源
进程地址空间包括进程的数据段、代码段等文件描述符表每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)当前工作目录用户ID和组ID 2. 线程控制
Linux的pthread库 Liunx中提供给用户层进行线程控制的函数被打包在一个动态库中 —— pthread。使用线程控制接口时需要包含头文件pthread.h并在gcc/g编译时加上-l pthread选项确定链接动态库。 在/lib64目录下找到pthread库 编译时应该添加的选项
g threadTest.cc -o threadTest -l pthread # -lpthread也可以pthread_create 功能 创建一个线程 接口 int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);参数 thread线程库中定义了一个线程ID类型phtread_t这里的thread是一个输出型参数函数会向其指向的空间写入创建线程的ID attr线程的属性一般设为nullptr即可 start_routine线程执行的函数是一个返回值类型void*参数类型void*的函数指针 arg传入start_routine的参数使用前后一般需要类型转换。 返回值 RETURN VALUEOn success, pthread_create() returns 0; on error, it returns an error number, and the contents of *thread are undefined.关于线程退出的问题 同子进程退出需要父进程回收线程也需要被另外的线程回收。回收的原因如下1. 一个线程退出后对应的资源不会被释放而是留存在地址空间中。一个进程能运行的线程数是有限的如果不加以回收可能会导致内存泄漏2. 一个线程退出后其它线程可能需要获取其执行任务的结果。 pthread_join 功能 阻塞等待一个线程 接口 int pthread_join(pthread_t thread, void **retval);参数 thread线程ID retval指向的空间中存储的是线程返回的结果注意类型转换因为线程函数的返回结果是void*类型所以要用二级指针接收。如果不关心回收线程的结果则设置为nullptr。 返回值 RETURN VALUEOn success, pthread_join() returns 0; on error, it returns an error number.pthread_exit 线程函数中可以直接用return退出线程并返回结果可以被其它线程join接收 void *run(void *arg)
{int cnt 5;while (cnt--){cout I am new thread endl;sleep(1);}return nullptr; //
}也可以用pthread_exit函数。 void pthread_exit(void *retval); //和return一样返回一个void*指针Linux中线程只有joinable和unjoinable两种状态。默认情况下线程是joinable状态该状态下的线程退出后占有资源不会被释放必须等待其它线程调用pthread_join回收它释放资源或者进程退出资源全部被释放。当然可以通过调用pthread_detach分离线程将线程设置为unjoinable状态使其无需被等待回收退出即被系统自动释放资源。 pthread_detach 功能 分离线程ID为thread的线程使其无需被join等待。 接口 int pthread_detach(pthread_t thread);返回值 RETURN VALUEOn success, pthread_detach() returns 0; on error, it returns an error number.线程分离可以由别的线程分离也可以自己分离。 pthread_self 功能 获取当前线程的线程ID 接口 pthread_t pthread_self(void);⭕测试
void *run(void *arg)
{int cnt 10;while(cnt--){cout I am new thread, cnt: cnt endl;sleep(1);}pthread_exit(nullptr);
}int main()
{cout I am main thread endl;pthread_t tid;pthread_create(tid, nullptr, run, nullptr);int n pthread_join(tid, nullptr);if (n ! 0){cout join new thread fail!! endl;exit(1);}cout join new thread success!! endl;return 0;
}主线程创建新线程后调用pthread_join会阻塞等待新线程退出。运行结果如下
[ckfVM-8-3-centos lesson9_thread]$ ./mythread
I am main thread
I am new thread, cnt: 9
I am new thread, cnt: 8
I am new thread, cnt: 7
I am new thread, cnt: 6
I am new thread, cnt: 5
I am new thread, cnt: 4
I am new thread, cnt: 3
I am new thread, cnt: 2
I am new thread, cnt: 1
I am new thread, cnt: 0
join new thread success!!可以在主线程中detach线程ID为tid的新线程也可以在新线程中detach自己。
void *run(void *arg)
{//pthread_detach(pthread_self()); // 在新线程中detach自己int cnt 10;while(cnt--){cout I am new thread, cnt: cnt endl;sleep(1);}pthread_exit(nullptr);
}int main()
{cout I am main thread endl;pthread_t tid;pthread_create(tid, nullptr, run, nullptr);pthread_detach(tid); // 在主线程中detach线程ID为tid的新线程int n pthread_join(tid, nullptr);if (n ! 0){cout join new thread fail!! endl;exit(1);}cout join new thread success!! endl;return 0;
}[ckfVM-8-3-centos lesson9_thread]$ ./mythread
I am main thread
join new thread fail!! #等待失败pthread_join无法等待已分离的线程返回值非0如果在新线程中detach自己可能依然能够join成功。要想成功detach线程必须在join之前detach因为调用pthread_join函数时已经将线程视为joinable并阻塞等待了此后再detach是无效的。上面代码中如果在新线程中detach自己由于主线程和新线程调度的先后顺序不确定性很可能线程先join再detach此时的detach是无效的。 pthread_cancel 功能 撤销终止一个线程ID为thread的线程 接口 int pthread_cancel(pthread_t thread);返回值 RETURN VALUEOn success, pthread_cancel() returns 0; on error, it returns a nonzero error number.撤销一个线程后如果有另外的线程join该线程那么其收到的退出结果是PTHREAD_CANCELED。 #define PTHREAD_CANCELED ((void *) -1)⭕测试
void *run(void *arg)
{while (true){cout I am new thread endl;sleep(1);}pthread_exit(nullptr);
}int main()
{cout I am main thread endl;pthread_t tid;pthread_create(tid, nullptr, run, nullptr);sleep(3);pthread_cancel(tid);void *ret nullptr;int n pthread_join(tid, ret);if (n ! 0){cout join new thread fail!! endl;exit(1);}if (ret PTHREAD_CANCELED){cout new thread is canceled endl;}cout join new thread success!! endl;return 0;
}[ckfVM-8-3-centos lesson9_thread]$ ./mythread
I am main thread
I am new thread
I am new thread
I am new thread
new thread is canceled #新线程被撤销了
join new thread success!!用户级线程 pthread库的线程控制接口都不是直接操作Linux底层的轻量级进程而是操作用户级线程。pthread库将底层的轻量级进程封装成为用户级线程用户看到的便是线程而不是所谓的轻量级进程。动态库load到进程的共享区中因此用户级线程的空间也是load到进程的共享区中线程的大部分独立资源保存在这块空间中包括线程栈。 线程库是怎么管理用户级线程的 先描述再组织。 创建类似TCB的数据结构来描述线程并将这些数据结构组织为一张表如下。 前面使用接口获取到的线程tid其实就是该线程的用户级页表的首地址只不过将其转换成整型的格式。 int g_val 100;string toHex(pthread_t tid)
{char buf[64];snprintf(buf, sizeof(buf), 0x%x, tid);return string(buf);
}void *run(void *arg)
{cout toHex(pthread_self()) endl;pthread_exit(nullptr);
}int main()
{pthread_t t1;pthread_t t2;cout g_val: g_val endl;pthread_create(t1, nullptr, run, nullptr);pthread_create(t2, nullptr, run, nullptr);pthread_join(t1, nullptr);pthread_join(t2, nullptr);return 0;
}[ckfVM-8-3-centos lesson9_thread]$ ./mythread
g_val: 0x6020cc #全局数据区
0x4b30f700 #共享区
0x4ab0e700 #共享区全局变量默认是所有线程共享的开发者需要处理多线程竞争问题。有些情况下我们需要保证一个线程独享一份数据其它线程无法访问。这时候就要用到线程局部存储。gcc/g编译环境中可以用__thread声明一个全局变量从而每个线程都会独有一个该全局变量存储在线程局部存储区中。 __thread int g_val 0; //__thread修饰全局变量可以理解为从进程的全局变量变成线程的全局变量string toHex(pthread_t tid)
{char buf[64];snprintf(buf, sizeof(buf), 0x%x, tid);return string(buf);
}void *run(void *arg)
{cout g_val: g_val g_val: g_val endl;pthread_exit(nullptr);
}int main()
{pthread_t t1;pthread_t t2;pthread_t t3;pthread_create(t1, nullptr, run, nullptr);pthread_create(t2, nullptr, run, nullptr);pthread_create(t3, nullptr, run, nullptr);cout g_val: g_val g_val: g_val endl;pthread_join(t1, nullptr);pthread_join(t2, nullptr);pthread_join(t3, nullptr);return 0;
}[ckfVM-8-3-centos lesson9_thread]$ ./mythread #使用了线程局部存储
g_val: 1 g_val: 0x7fcb7cfcb77c
g_val: 1 g_val: 0x7fcb7bf366fc
g_val: 1 g_val: 0x7fcb7b7356fc
g_val: 1 g_val: 0x7fcb7af346fc[ckfVM-8-3-centos lesson9_thread]$ ./mythread #未使用线程局部存储
g_val: 1 g_val: 0x6021d4
g_val: 2 g_val: 0x6021d4
g_val: 3 g_val: 0x6021d4
g_val: 4 g_val: 0x6021d4每个线程都有一个独立的栈结构用于存储运行时的临时数据和压入函数栈帧。注意主线程的栈就是进程地址空间中的栈。 ENDING…