之前我一直都提到的是进程,现在多了一个线程的概念,从字面意思来看,线程应该比进程小。嘿嘿。
其实操作系统刚开始的时候,提出进程概念后,操作系统一直都是以进程作为独立运行的基本单位,然后有人感觉了,这不对呀,进程之间的中断转换太浪费了,并且用户态到核心态的切换也有点麻烦,所以在80年代中期,人们又提出了毕竟更小的独立运行的基本单位咯--线程,用来提高系统内存程序的并发执行程度。所以线程就这么出现了。
那,什么是线程呢?
其实简单来说,线程就是进程中的执行分流,从操作系统内部来说其实就是一个指令序列,他执行了地址上的跳转,但是却不影响其他流程的执行。进程它所针对的是独占我们所有的资源,但是线程确实在单个进程之下进行资源的共享,但是共享虽然共享,但是人嘛,不管怎么过度曝光,都有着自己的隐私,所以我们看一下线程所需要拥有的属性:
由于同一进程的多个线程共享同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果
定义一 个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
1. 文件描述符表
2. 每种信号的处理方式(SIG_IGN、SIG_DFL或者自定义的信号处理函数)
3. 当前工作目录
4. 用户id和组id
但有些资源是每个线程各有一份的:
1. 线程id
2. 上下文,包括各种寄存器的值、程序计数器和栈指针
3. 栈空间
4. errno变量
5. 信号屏蔽字
6. 调度优先级
以上是关于线程的概念,以后我会提到用户态线程与核心态线程,这里暂且不提,我们来看一下如何在Linux下使用线程。
下面来讲解一下线程控制:
首先要注意一点的是,之前提到的进程都是system V版本的系统,所以他的库调用都是
#include<sys/XXX>
但是关于线程,我们遵循的是POSIX版本的系统,一定要注意这一点。
然后看一下线程的创建函数:注意在编译包含使用线程的程序是,需要在gcc命令后面加上-lpthread
#include<pthread.h> pthread_create(pthread_t *thread,const pthread_attr_t *attr, void*(*start_routine)(void *),void* arg);
返回值:成功返回0,失败返回错误号。以前学过的系统函数都是成功返回0,失败返回-1,而错误
号保存在全局变量errno中,而pthread库的函数都是通过返回值返回错误号,虽然每个线程也都
有 一个errno,但这是为了兼容其它函数接口而提供的,pthread库本身并不使用它,通过返回值
返回错误码更加清晰。
在一个线程中调用pthread_create()创建新的线程后,当前线程从pthread_create()返回继续往下执行,而新的线程所执行的代码由我们传给pthread_create的函数指针start_routine决定start_routine函数接收一个参数,是通过pthread_create的arg参数传递给它的,该参数的类型为void *,这个指针按什么类型解释由调用者自己定义。start_routine的返回值类型也是void *,这个指针的含义同样由调用者自己定义。start_routine返回时,这个线程就退出了,其它线程可以调用pthread_join得到start_routine的返回值,类似于父进程调用wait(2)得到子进程的退出状态,稍后详细介绍pthread_join。
pthread_create成功返回后,新创建的线程的id被填写到thread参数所指向的内存单元。我们知道进程id的类型是pid_t,每个进程的id在整个系统中是唯一的,调用getpid(2)可以获得当前进程的id,是一个正整数值。线程id的类型是thread_t,它只在当前进程中保证是唯一的,在不同的系统中thread_t这个类型有不同的实现,它可能是一个整数值,也可能是一个结构体,也可能是一个地址,所以不能简单地当成整数用printf打印,调用pthread_self(3)可以获得当前线程的id。
attr参数表示线程属性.
下面看一下程序代码:
#include <stdio.h> #include <stdlib.h> #include <pthread.h> pthread_t tid; void* thread_run(void *_val) { printf("%s,pid is :%d,tid is :%u\n",(char*)_val,(int )getpid(),(unsigned long long)pthread_self()); } int main() { int ret = pthread_create(&tid,NULL,thread_run,"other thread run"); if(ret != 0) { printf("creater error:%s\n",strerror(ret)); exit(ret); } printf("main thread run:pid is :%d,tid is :%u\n",(int )getpid(),(unsigned long long)pthread_self()); sleep(1); return 0; }
如果任意一个线程调用了exit或_exit,则整个进程的所有线程都终止,
运行结果:
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
1. 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
2. 一个线程可以调用pthread_cancel终止同一进程中的另一个线程。
3. 线程可以调用pthread_exit终止自己。
用pthread_cancel终止一个线程分同步和异步两种情况,比较复杂,不打算详细介绍,同学们先自己研究。
然后我们看一下它的其他几个要求函数:
#include<pthread.h> void pthread_exit(void *retval); int pthread_join(pthread_t thread,void **retval);
1. 如果thread线程通过return返回,value_ptr所指向的单元里存放的是thread线程函数的返
回值。
2. 如果thread线程被别的线程调用pthread_cancel异常终掉,value_ptr所指向的单元里存放
的是常数PTHREAD_CANCELED。
3. 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ptr参数。
操作代码:
#include <stdio.h> #include <stdlib.h> #include <pthread.h> void* thread1(void *_val) { printf("thread 1 returning ...\n"); return (void*)1; } void* thread2(void *_val) { printf("thread 2 exiting ...\n"); pthread_exit((void*)2); } void* thread3(void *_val) { while(1) { printf("pthread 3is running,wait for be cancal...\n"); sleep(1); } return NULL; } int main() { pthread_t tid; void *tret; //thread 1 return pthread_create (&tid,NULL,thread1,NULL); pthread_join(tid,&tret); printf("thread return ,thread id is :%u,return code is : %d\n",(unsigned long long )tid,tret); //thread 2 return void *tret2; pthread_create (&tid,NULL,thread2,NULL); pthread_join(tid,&tret2); printf("thread return ,thread id is :%u,exit code is : %d\n",(unsigned long long )tid,tret2); //thread 1 return void *tret3; pthread_create (&tid,NULL,thread3,NULL); sleep(3); pthread_cancel(tid); pthread_join(tid,&tret3); printf("thread return ,thread id is :%u,cancel code is : %d\n",(unsigned long long )tid,tret3); return 0; }
运行结果:
返回值有问题,我也不知道为什么。。。
其中需要注意点,当创建一个线程,并且在join等待一个线程时,需要当前的逻辑流执行完以后才能执行下面的代码,也就是为什么3个线程的ID是相同的了。
有关分离线程
在任何一个时间点上,线程是可结合的(joinable)或者是分离的(detached)。一个可结合的线程能够被其他线程收回其资源和杀死。在被其他线程回收之前,它的存储器资源(例如栈)是不释放的。相反,一个分离的线程是不能被其他线程回收或杀死的,它的存储器 资源在它终止时由系统自动释放。默认情况下,线程被创建成可结合的。为了避免存储器泄漏,每个可结合线程都应该要么被显示地回收,即调用pthread_join;要么通过调用pthread_detach函数被分离。
如果一个可结合线程结束运行但没有被join,则它的状态类似于进程中的Zombie Process,即还有一部分资源没有被回收,所以创建线程者应该调用pthread_join来等待线程运行结束,并可得到线程的退出代码,回收其资源。由于调用pthread_join后,如果该线程没有运行结束,调用者会被阻塞,在有些情况下我们并不希望如此。例如,在Web服务器中当主线程为每个新来的连接请求创建一个子线程进行处理的时候,主线程并不希望因为调用pthread_join而阻塞(因为还要继续处理之后到来的连接请求),这时可以在子线程中加入代码
pthread_detach(pthread_self())
或者父线程调用
pthread_detach(thread_id)(非阻塞,可立即返回)
这将该子线程的状态设置为分离的(detached),如此一来,该线程运行结束后会自动释放所有资源。
代码如下:
#include <stdio.h> #include <stdlib.h> #include <pthread.h> void *thread_run(void *_val) { pthread_detach(pthread_self()); printf("%s\n",(char *)_val); return NULL; } int main() { pthread_t tid; int tret = pthread_create(&tid,NULL,thread_run,"thread1 run..."); if(tret != 0) { printf("create thread error,info is :%s\n",strerror(tret)); return tret; } int ret = 0; sleep(1); if(0 == pthread_join(tid,NULL)) { printf("wait 1"); ret = 0; } else{ printf("wait failed"); ret = 1; } return ret; }
运行结果:
好了,总结一下:
其实关于线程,我们在理解他的出现由来和基本概念之后我们知道为什么需要他的存在,例如我们加载一个网页,图片,视频,还有文字是分别加载的,如果用单进程进行加载的话他们是交错出现,但是如果使用多线程的话他们可以同时进行加载,并且也比进程快,所以线程的出现解决了模块问题的并发或者单列处理。
然后就是关于Linux系统下的线程编程了,其实只要理解线程的创建,等待,分离,终止,就可以算作是我们能够操纵进程了,记住他的几个相关操作函数,就OK了,其实线程就是进程的肢体而已。
以上。