全文约 5790 字,预计阅读时长: 17分钟
多线程概念
- 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”
- 一切进程至少都有一个执行线程
- 线程在进程内部运行,本质是在进程地址空间内运行
- 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化。
- 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流。
如果创建“进程”,不独立创建地址空间,用户及页表,甚至不进行I/O将程序的数据和代码加载到内存;只创建 task_struct;然后让新的 PCB,指向和老的PCB指向同样的进程地址空间 mm_struct,当前进程通过合理的资源分配,让每个 task_struct 都能使用进程的一部分资源,如代码也拆成多份,让每个线程去执行不同的代码;此时我们的每个PCB被CPU调度的时候,执行的 “ 粒度 ”比原始进程执行的 “ 粒度 ”要小一些。
进程的地址空间:站在资源的较低,其实式进程的资源窗口。进程,站在OS角度看:承担分配系统资源的基本单位。一个进程被创建好以后,后续可能内部存在多个执行流(线程)。如何看待曾经学的、用的进程呢?本质是:承担系统资源的基本实体,不过内部只有一个执行流。
Linux下的线程 VS Windows 下的线程
windows具有真正线程的概念。系统内可能存在大量的进程、线程,进程线程比:1:n,操作系统需要管理线程;支持真线程的系统一定要做到描述线程:TCB,TCB又有一堆属性,其中可能包括又隶属于哪一个进程的信息等;操作系统既要进行进程管理,又要进行线程管理,设计层面是比较复杂的。windows上一定会有相关线程操作的系统调用接口。
Linux下其实没有真正意义上的线程的概念的。由于TCB和PCB有很强的相似性,进程和线程都是执行流;因此把概念同意,用进程模拟实现线程,只需要一套机制进行管理。站在CPU的角度,原本切换进程需要大量的准备工作,而线程还是在刚才进程的内部,就省去了一堆麻烦事儿。现在可能执行的“进程流”,只是更加轻量化的进程。所以在Linux上不可能直接在OS层面提供线程的系统调用接口,最多是轻量级进程的调度接口。线程原生库提供的线程操作,其实是对轻量级进程接口的封装,再配上一些缓冲区保存用户的临时数据等别的功能。另外语言上的接口,用的也都是系统提供的线程接口。
多线程的优缺点
- 线程优点:
- 创建成本小、切换成本小、占用资源少。
- 线程缺点:
- 性能损失。一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
- 健壮性降低。编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
- 缺乏访问控制。进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
- 编程难度提高。编写与调试一个多线程程序比单线程程序困难得多。
- 以上总结:一定会存在大量的临界资源。势必可能需要使用各种互斥和同步机制。
- 线程异常:
- 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃。
- 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出。
- 线程用途:
- 合理的使用多线程,能提高CPU密集型程序的执行效率。 尽量让线程数和CPU的核数一样多。线程太多,则线程的切换会成为主要矛盾。
- 合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)。
进程 VS 线程
- 进程是资源分配的基本单位,线程是调度的基本单位。
- 线程共享进程数据,但也独有自己的一部分数据:
- 线程ID、一组寄存器、栈、errno、信号屏蔽字、调度优先级
- 上下文数据、独立的栈结构:线程是可以切换的;线程是可以独立运行的。
- 进程的多个线程共享同一地址空间,,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用;如果定义一个全局变量,在各线程中都可以访问到。除此之外,各线程还共享以下进程资源和环境:
- 文件描述符表
- 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
- 当前工作目录、用户id和组id
- 总结:一个进程的大部分数据资源,都是线程共享的。
线程控制
- POSIX线程库:
- 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以 “pthread_” 打头的
- 要使用这些函数库,要通过引入头文
<pthread.h>
- 链接这些线程函数库时要使用编译器命令的“
-lpthread
”选项
线程创建
- 创建一个新的线程:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);
- 参数:
thread
:返回线程ID attr
:设置线程的属性,attr为NULL表示使用默认属性。start_routine
:是个函数地址,线程启动后要执行的函数。arg
:传给线程启动函数的参数。- 返回值:成功返回0;失败返回错误码。
- 参数:
- main 函数 是 主线程,创建的线程叫 新线程。
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <pthread.h>
void *rout(void *arg) {
int i;
for( ; ; )
{
printf("I'am thread 1\n");
sleep(1);
}
}
int main( void )
{
pthread_t tid;
int ret;
if ( (ret=pthread_create(&tid, NULL, rout, NULL)) != 0 )
{
fprintf(stderr, "pthread_create : %s\n", strerror(ret));
exit(EXIT_FAILURE);
}
int i;
for(; ; )
{
printf("I'am main thread\n");
sleep(1);
}
}
----//void* 是系统层面设计的一个通用接口。
- Makefile文件:
t1:t1.cc
g++ -o $@ $^ -std=c++11 -lphtread
- 查看线程命令:
$ ps -aL | head -1 && ps -aL | grep t1
- PID :证明多个线程是属于同一个进程的。
- LWP:Light-weight process:执行流标识唯一性,CPU调度层面看的就是这个 线程ID LWP。
pthread_ create
函数,会产生一个线程ID,库层面管理线程的标识符,存放在第一个参数指向的地址中。第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。- 线程库NPTL(现代Linux上模式运行的线程库是NPTL(Native POSIX Thread Library)。)提供了
pthread_ self
函数,可以获得线程自身的ID:pthread_t pthread_self(void);
- pthread_t 到底是什么类型呢?取决于实现。对于Linux目前实现的NPTL实现而言,pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址。
- pthread_t 到底是什么类型呢?取决于实现。对于Linux目前实现的NPTL实现而言,pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址。
mmap区域
mmap是一种内存映射的方法,这一功能可以用在文件的处理上,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。在编程时可以使某个磁盘文件的内容看起来像是内存中的一个数组。如果文件由记录组成,而这些记录又能够用结构体来描述的话,可以通过访问结构数组来更新文件的内容。
实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。mmap 内存映射详解
线程终止
- 如果需要只终止某个线程而不终止整个进程,可以有三种方法:
- 从线程函数
return
。这种方法对主线程不适用,从main函数return相当于调用exit。 - 线程可以调用
pthread_ exit
终止自己。 - 一个线程可以调用
pthread_ cancel
终止同一进程中的另一个线程。
- 从线程函数
pthread_exit
函数:void pthread_exit(void *value_ptr);
- 参数:
value_ptr:value_ptr
不要指向一个局部变量。 - 返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)。
- 参数:
pthread_exit
或者return
返回的指针所指向的内存单元必须是全局的或者是用malloc
分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。
---//return
void *thread1( void *arg )
{
printf("thread 1 returning ... \n");
int *p = (int*)malloc(sizeof(int));
*p = 1;
return (void*)p;
}
void *thread3( void *arg )
{
while ( 1 )
{
printf("thread 3 is running ...\n");
sleep(1);
}
return NULL;
}
----//`pthread_exit`
void *thread2( void *arg )
{
printf("thread 2 exiting ...\n");
int *p = (int*)malloc(sizeof(int));
*p = 2;
pthread_exit((void*)p);
}
pthread_cancel
函数:int pthread_cancel(pthread_t thread);
::取消一个执行中的线程thread:
:线程ID- 返回值:成功返回0;失败返回错误码
- 只有当一个线程跑起来的时候,才有可能被cancel。
- 自己取消自己,你得给操作系统点反应时间。。
....
pthread_t tid;
pthread_create(&tid, NULL, thread3, NULL);
sleep(3);
pthread_cancel(tid);
.....
线程等待
- 为什么需要线程等待:
- 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内,防止资源泄漏。
- 获得线程退出时的相关退出码。
- 保证主线程最后退出。让新线程正常结束。
- 如果没等待,会造成类似僵尸进程的状态。
int pthread_join(pthread_t thread, void **value_ptr);
:主线程阻式等待线程结束- 参数:
thread
:线程ID, value_ptr
:它指向一个指针,后者指向线程的返回值,可以返回任意结构类型的线程退出信息。- 返回值:成功返回0;失败返回错误码。
- 参数:
....
void *ret;
pthread_t tid;
pthread_create(&tid, NULL, thread3, NULL);
sleep(3);
pthread_cancel(tid);
pthread_join(tid, &ret);
....
- 调用该函数的线程将挂起等待,直到 id 为 thread 的线程终止。thread 线程以不同的方法终止,通过 pthread_join 得到的终止状态是不同的,总结如下:
- 如果 thread 线程通过
return
返回,value_ ptr 所指向的单元里存放的是 thread线程 函数的返回值。 - thread线程 被别的线程调用
pthread_ cancel
异常终掉,value_ ptr 所指向的单元里存放的是常数PTHREAD_ CANCELED
,宏:-1。 - 如果 thread线程 是自己调用
pthread_exit
终止的,value_ptr 所指向的单元存放的是传给 pthread_exit 的参数。 - 如果对 thread线程 的终止状态不感兴趣,可以传 NULL 给 value_ ptr 参数。
- 如果 thread 线程通过
分离线程
- 分离的本质:是让主线程不用再 join 新线程,从而让新线程退出的时候,自动回收资源。
- 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
- 如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
- 可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:
- 主线程分离:
int pthread_detach(pthread_t thread);
- 自我分离:
pthread_detach(pthread_self());
- 主线程分离:
- joinable和分离是冲突的,一个线程不能既是joinable又是分离的。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
void *thread_run( void * arg )
{
pthread_detach(pthread_self());
printf("%s\n", (char*)arg);
return NULL;
}
int main( void )
{
pthread_t tid;
if ( pthread_create(&tid, NULL, thread_run, "thread1 run...") != 0 )
{
printf("create thread error\n");
return 1;
}
int ret = 0;
sleep(1);//很重要,要让线程先分离,再等待
if ( pthread_join(tid, NULL ) == 0 )
{
printf("pthread wait success\n");
ret = 0;
} else
{
printf("pthread wait failed\n");
ret = 1;
}
return ret;
}
NPTL
Linux没有真正意义上的线程,但是会提供轻量级进程的接口vfrok;为了更好的适配,封装了一个用户层的原生线程库,线程相关的属性数据在用户空间的共享区的线程库内,由库保存维护。
线程库NPTL(现代Linux上模式运行的线程库是NPTL(Native POSIX Thread Library),用户层线程ID,其实是共享区里线程库中的某一个起始位置。主线程,不使用库中的栈结构,直接使用地址空间中的栈。
寄语
- 主线程创建新线程,等待新线程,取消新线程,回收新线程。