目录
1. Linux线程概念
什么是线程
- 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”
- 一切进程至少都有一个执行线程
- 线程在进程内部运行,本质是在进程地址空间内运行
- 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化
- 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流
线程的优点
- 创建一个新线程的代价要比创建一个新进程小得多
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
- 线程占用的资源要比进程少很多
- 能充分利用多处理器的可并行数量
- 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
- I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
线程的缺点
性能损失
- 一个很少被外部事件阻塞的计算密集型线程往往无法与其它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的 同步和调度开销,而可用的资源不变。
健壮性降低
- 编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
缺乏访问控制
- 进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
编程难度提高
- 编写与调试一个多线程程序比单线程程序困难得多
线程异常
- 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
- 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出
线程用途
- 合理的使用多线程,能提高CPU密集型程序的执行效率
- 合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)
2. Linux进程VS线程
进程和线程
- 进程是资源分配的基本单位
- 线程是调度的基本单位
- 线程共享进程数据,但也拥有自己的一部分数据
- 线程ID
- 一组寄存器
- 栈
- errno
- 信号屏蔽字
- 调度优先级
进程的多个线程共享同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
- 文件描述符表
- 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
- 当前工作目录
- 用户id和组id
进程和线程的关系如下图:
关于进程线程的问题
- 如何看待之前学习的单进程?——具有一个线程执行流的进程
3. Linux线程控制
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;失败返回错误码
错误检查:
- 传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
- pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回
- pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误, 建议通过返回值判定,因为读取返回值要比读取线程内的errno变量的开销更小
#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() { pthread_t tid; int ret = pthread_create(&tid, NULL, rout, NULL); if ( ret != 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_ create函数会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和前面说的线程ID 不是一回事。
- 前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要 一个数值来唯一表示该线程。
- pthread_ create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID, 属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。
- 线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID:
#include <stdio.h> #include <pthread.h> #include <unistd.h> void* rout(void* arg) { int i; for ( ; ; ) { printf("I'm thread 1 virtualID: %lu\n", pthread_self()); sleep(1); } // 建议添加返回值(虽然不会执行到这里) return NULL; } int main() { pthread_t tid; int ret = pthread_create(&tid, NULL, rout, NULL); if (ret != 0) { perror("pthread_create failed"); return 1; } for ( ; ; ) { printf("I'm main thread\n"); sleep(1); } // 实际上不会执行到这里,需要更好的线程同步 pthread_join(tid, NULL); return 0; }
pthread_t 到底是什么类型呢?取决于实现。对于Linux目前实现的NPTL实现而言,pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址。
线程终止
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
- 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
- 线程可以调用pthread_ exit终止自己。
- 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。
pthread_exit函数
功能:线程终止 原型 void pthread_exit(void *value_ptr); 参数 value_ptr:value_ptr不要指向一个局部变量。 返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)
需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。
pthread_cancel函数
功能:取消一个执行中的线程 原型 int pthread_cancel(pthread_t thread); 参数 thread:线程ID 返回值:成功返回0;失败返回错误码
线程等待 为什么需要线程等待?
- 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
- 创建新的线程不会复用刚才退出线程的地址空间。
pthread_join函数
功能:等待线程结束 原型 int pthread_join(pthread_t thread, void **value_ptr); 参数 thread:线程ID value_ptr:它指向一个指针,指向线程的返回值 返回值:成功返回0;失败返回错误码
调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的 终止状态是不同的,总结如下:
- 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
- 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数 PTHREAD_ CANCELED。
- 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。
- 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <pthread.h> 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 return pthread_create(&tid, NULL, thread1, NULL); pthread_join(tid, &ret); printf("thread return, thread id %lX, return code:%d\n", (unsigned long)tid, *(int*)ret); free(ret); // thread 2 exit pthread_create(&tid, NULL, thread2, NULL); pthread_join(tid, &ret); printf("thread return, thread id %lX, return code:%d\n", (unsigned long)tid, *(int*)ret); free(ret); // thread 3 cancel by other pthread_create(&tid, NULL, thread3, NULL); sleep(3); pthread_cancel(tid); pthread_join(tid, &ret); if (ret == PTHREAD_CANCELED) printf("thread return, thread id %lX, return code:PTHREAD_CANCELED\n", (unsigned long)tid); else printf("thread return, thread id %lX, return code:NULL\n", (unsigned long)tid); }
运行截图:
我们可以通过上面那个时间线的截图来帮助我们分析这段代码,这段代码首先有四个线程,三个分支线程一个主线程。我们让第一个分支线程执行:在堆上开辟一块空间,然后赋值并将其返回,第二个分支线程进行:同样的开空间赋值,多了一步终止线程pthread_exit函数,第三个分支是无限循环执行三秒,由外部调用pthread_cancle函数终止。
主线程就管理他们,并将他们的线程id和错误码打印出来我们可以观察到如上结果。
4. 分离线程
- 默认情况下,新创建的线程是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, (void*)"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; }
由这段测试代码我们就可以得知分离后的线程,我们就不需要等待了。