目录
1. Linux线程概念
什么是线程
线程的优点
线程的缺点
线程异常
线程用途
2. Linux进程VS线程
进程和线程
进程的多个线程共享
关于进程线程的问题
3. Linux线程控制
POSIX线程库
创建线程
线程ID及进程地址空间布局
线程终止
线程等待
4. 分离线程
后记:●由于作者水平有限,文章难免存在谬误之处,敬请读者斧正,俚语成篇,恳望指教!
——By 作者:新晓·故知
1. 了解线程概念,理解线程与进程区别与联系。 2. 学会线程控制,线程创建,线程终止,线程等待。 3. 了解线程分离与线程安全概念。 4. 学会线程同步。 5. 学会使用互斥量,条件变量,posix信号量,以及读写锁。 6. 理解基于读写锁的读者写者问题。
什么是线程
- 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”
- 一切进程至少都有一个执行线程
- 线程在进程内部运行,本质是在进程地址空间内运行
- 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化
- 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流。
线程: (1)在进程内部运行的执行流 (2)线程比进程粒度更细,调度成本更低 (3)线程是CPU调度的基本单位 我们在之前学习过,fork()之后,父、子进程是共享代码的。可以通过if else 进行判断让父子进程执行不同的代码块。不同的执行流,可以做到进行对特定资源的划分。 线程:进程 —— N:1 线程也有描述的数据结构(TCB),在Linux中,进程和线程在概念层面没有区分,只有一个称为执行流。
线程的优点
- 创建一个新线程的代价要比创建一个新进程小得多
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
- 线程占用的资源要比进程少很多
- 能充分利用多处理器的可并行数量
- 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现 I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
线程的缺点
性能损失 一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额 外的同步和调度开销,而可用的资源不变。 健壮性降低 编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。 缺乏访问控制 进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。 编程难度提高 编写与调试一个多线程程序比单线程程序困难得多线程异常
- 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
- 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出
线程用途
- 合理的使用多线程,能提高CPU密集型程序的执行效率
- 合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)
进程和线程
- 进程是资源分配的基本单位
- 线程是调度的基本单位
- 线程共享进程数据,但也拥有自己的一部分数据:
线程ID
一组寄存器 栈 errno 信号屏蔽字 调度优先级进程的多个线程共享
同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:进程和线程的关系如下图:
- 文件描述符表
- 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
- 当前工作目录
- 用户id和组id
![]()
关于进程线程的问题
如何看待之前学习的单进程?具有一个线程执行流的进程我们之前理解进程:进程=内核数据结构+进程对应的代码和数据
现在理解进程:从内核视角,进程是承担分配系统资源的基本实体,是向系统申请资源的基本单位!
内部只有一个执行流的进程——单执行流进程
背部有多个执行流的进程——多执行流进程
多个线程谁先运行,由OS决定。
#include
#include #include #include using namespace std;void *callback1(void *args) {string name = (char *)args;while (true){cout << name << ": " << ::getpid() << endl;sleep(1);} } void *callback2(void *args) {string name = (char *)args;while (true){cout << name << ": " << ::getpid() << endl;sleep(1);} }int main() {pthread_t tid1;pthread_t tid2;pthread_create(&tid1, nullptr, callback1, (void *)"thread 1");pthread_create(&tid2, nullptr, callback2, (void *)"thread 2");while (true){cout << "我是主线程... :" << ::getpid() << endl;sleep(1);}pthread_join(tid1, nullptr);pthread_join(tid2, nullptr);return 0; } C++也支持线程,其封装的是系统原生的pthread。
Linux下的线程是进程模拟的,是轻量级进程。
![]()
#include
#include #include using namespace std;void *startRoutine(void *args) {while (true){cout << "线程正在运行..." << endl;sleep(1);} } int main() {pthread_t tid;int n = pthread_create(&tid, nullptr, startRoutine, (void *)"thread 1");cout << "new thread id: " << tid << endl; //线程idwhile (true){cout << " main thread 正在运行..." << endl;sleep(1);}return 0; }
#include
#include #include #include using namespace std;static void printTid(const char* name,const pthread_t &tid) {printf("%s 正在运行,thread id: 0x%x\n",name,tid); } void *startRoutine(void *args) {const char* name = static_cast (args);while (true){//cout << "线程正在运行..." << endl;printTid(name,pthread_self());sleep(1);} } int main() {pthread_t tid;int n = pthread_create(&tid, nullptr, startRoutine, (void *)"thread 1");//cout << "new thread id: " << tid << endl; //线程idwhile (true){//cout << " main thread 正在运行..." << endl;printTid("main thread" ,pthread_self());sleep(1);}return 0; }
#include
#include #include #include using namespace std;static void printTid(const char *name, const pthread_t &tid) {printf("%s 正在运行,thread id: 0x%x\n", name, tid); } void *startRoutine(void *args) {const char *name = static_cast (args);int cnt = 5;while (true){printTid(name, pthread_self());sleep(1);if (!(cnt--))break;}cout << "线程退出!" << endl;return nullptr; } int main() {pthread_t tid;int n = pthread_create(&tid, nullptr, startRoutine, (void *)"thread 1");sleep(5);pthread_join(tid, nullptr);cout<<"main thread join success!"< shell脚本:
while :; do ps -aL | head -1 && ps -aL | grep mythread; sleep 1;done
while :; do ps ajx | grep mythread; sleep 1; done
线程退出的时候,一般需要进行join,否则就会造成类似于进程那样的内存泄漏问题!
#include
#include #include #include using namespace std;static void printTid(const char *name, const pthread_t &tid) {printf("%s 正在运行,thread id: 0x%x\n", name, tid); } void *startRoutine(void *args) {const char *name = static_cast (args);int cnt = 5;while (true){printTid(name, pthread_self());sleep(1);if (!(cnt--))break;}cout << "线程退出!" << endl;// 线程退出方式:// 1.returnreturn (void *)10; } int main() {pthread_t tid;int n = pthread_create(&tid, nullptr, startRoutine, (void *)"thread 1");(void)n;// sleep(1);void *ret = nullptr; // void **retval是一个输出型参数pthread_join(tid, &ret);cout << "main thread join success! *ret: " << (long long)ret << endl;sleep(1);while (true){printTid("main thread", pthread_self());sleep(1);}return 0; }
-1 其实是内核中“取消”定义的宏。
pthread_t其实是一个地址。
![]()
![]()
主线程(进程)退出,则所有线程退出!
1. 立即分离,延后分离 -- 线程活着 -- 意味着,我们不在关心这个线程的死活。( 线程退出的第四种方式,延后退出)
2. 新线程分离,但是主线程先退出(进程退出) --- 一般我们分离线程,对应的main thread一般不要退出(常驻内存的进程)
3. Linux线程控制
POSIX线程库
- 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的
- 要使用这些函数库,要通过引入头文
- 链接这些线程函数库时要使用编译器命令的“-lpthread”选项
创建线程
错误检查:功能:创建一个新的线程 原型int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void * (*start_routine)(void*), void *arg); 参数thread:返回线程IDattr:设置线程的属性,attr为NULL表示使用默认属性start_routine:是个函数地址,线程启动后要执行的函数arg:传给线程启动函数的参数 返回值:成功返回0;失败返回错误码
- 传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
- pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回
- pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值业判定,因为读取返回值要比读取线程内的errno变量的开销更小
#include
#include #include #include #include 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);} } 线程ID及进程地址空间布局
pthread_t pthread_self(void); pthread_t 到底是什么类型呢?取决于实现。对于Linux目前实现的NPTL实现而言,pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址。
- pthread_ create函数会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和前面说的线程ID不是一回事。
- 前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。
- pthread_ create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。
- 线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID:
![]()
线程终止
如果需要只终止某个线程而不终止整个进程,可以有三种方法: 1. 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。 2. 线程可以调用pthread_ exit终止自己。 3. 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。 pthread_exit函数需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。 pthread_cancel函数功能:线程终止 原型void pthread_exit(void *value_ptr); 参数value_ptr:value_ptr不要指向一个局部变量。 返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)
功能:取消一个执行中的线程 原型 int pthread_cancel(pthread_t thread); 参数 thread:线程ID 返回值:成功返回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的参数。 4. 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。功能:等待线程结束 原型 int pthread_join(pthread_t thread, void **value_ptr); 参数 thread:线程ID value_ptr:它指向一个指针,后者指向线程的返回值 返回值:成功返回0;失败返回错误码
![]()
#include
#include #include #include #include void *thread1( void *arg ) {printf("thread 1 returning ... \n");int *p = (int*)malloc(sizeof(int));*p = 1;return (void*)p;} void *thread2( void *arg ) {printf("thread 2 exiting ...\n");int *p = (int*)malloc(sizeof(int));*p = 2;pthread_exit((void*)p); } void *thread3( void *arg ) {while ( 1 ){ // printf("thread 3 is running ...\n");sleep(1);}return NULL; } int main( void ) {pthread_t tid;void *ret;// thread 1 returnpthread_create(&tid, NULL, thread1, NULL);pthread_join(tid, &ret);printf("thread return, thread id %X, return code:%d\n", tid, *(int*)ret);free(ret);// thread 2 exitpthread_create(&tid, NULL, thread2, NULL);pthread_join(tid, &ret);printf("thread return, thread id %X, return code:%d\n", tid, *(int*)ret);free(ret);// thread 3 cancel by otherpthread_create(&tid, NULL, thread3, NULL);sleep(3);pthread_cancel(tid);pthread_join(tid, &ret);if ( ret == PTHREAD_CANCELED )printf("thread return, thread id %X, return code:PTHREAD_CANCELED\n", tid);elseprintf("thread return, thread id %X, return code:NULL\n", tid); } 运行结果:[root@localhost linux]# ./a.outthread 1 returning ... thread return, thread id 5AA79700, return code:1thread 2 exiting ...thread return, thread id 5AA79700, return code:2thread 3 is running ...thread 3 is running ...thread 3 is running ...thread return, thread id 5AA79700, return code:PTHREAD_CANCELED 4. 分离线程
默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。 如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。 int pthread_detach(pthread_t thread); 可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离; pthread_detach(pthread_self());joinable和分离是冲突的,一个线程不能既是joinable又是分离的
#include
#include #include #include #include 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; }