#导入Word文档图片# Linux下线程编程
Posted DS小龙哥
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了#导入Word文档图片# Linux下线程编程相关的知识,希望对你有一定的参考价值。
第一章 实现Linux下线程基本运用
1.1线程简介
线程,有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现出间断性。线程也有就绪、阻塞和运行三种基本状态。就绪状态是指线程具备运行的所有条件,逻辑上可以运行,在等待处理机;运行状态是指线程占有处理机正在运行;阻塞状态是指线程在等待一个事件(如某个信号量),逻辑上不可执行。每一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。
线程是程序中一个单一的顺序控制流程。进程内一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位指运行中的程序的调度单位。在单个程序中同时运行多个线程完成不同的工作,称为多线程。
线程与进程的区别
1.2.1 前言
进程与线程的区别,早已经成为了经典问题。自线程概念诞生起,关于这个问题的讨论就没有停止过。无论是初级程序员,还是资深专家,都应该考虑过这个问题,只是层次角度不同。一般程序员而言,搞清楚二者的概念,在工作实际中去运用成为了焦点。而资深工程师则在考虑系统层面如何实现两种技术及其各自的性能和实现代价。以至于到今天,Linux内核还在持续更新完善(关于进程和线程的实现模块也是内核完善的任务之一)。
1.2.2 二者的相同点
无论是进程还是线程,对于程序员而言,都是用来实现多任务并发的技术手段。二者都可以独立调度,因此在多任务环境下,功能上并无差异。并且二者都具有各自的实体,是系统独立管理的对象个体。所以在系统层面,都可以通过技术手段实现二者的控制。而且二者所具有的状态都非常相似。而且,在多任务程序中,子进程(子线程)的调度一般与父进程(父线程)平等竞争。
其实在Linux内核2.4版以前,线程的实现和管理方式就是完全按照进程方式实现的。在2.6版内核以后才有了单独的线程实现。
1.2.3 实现方式的差异
进程是资源分配的基本单位,线程是调度的基本单位。
进程的个体间是完全独立的,而线程间是彼此依存的。多进程环境中,任何一个进程的终止,不会影响到其他进程。而多线程环境中,父线程终止,全部子线程被迫终止(没有了资源)。而任何一个子线程终止一般不会影响其他线程,除非子线程执行了exit()系统调用。任何一个子线程执行exit(),全部线程同时灭亡。
- 多任务程序设计模式的区别
由于进程间是独立的,所以在设计多进程程序时,需要做到资源独立管理时就有了天然优势,而线程就显得麻烦多了。
比如:多任务的TCP程序的服务端,父进程执行accept()一个客户端连接请求之后会返回一个新建立的连接的描述符DES,此时如果fork()一个子进程,将DES带入到子进程空间去处理该连接的请求,父进程继续accept等待别的客户端连接请求,这样设计非常简练,而且父进程可以用同一变量(val)保存accept()的返回值,因为子进程会复制val到自己空间,父进程再覆盖此前的值不影响子进程工作。但是如果换成多线程,父线程就不能复用一个变量val多次执行accept()了。因为子线程没有复制val的存储空间,而是使用父线程的,如果子线程在读取val时父线程接受了另一个客户端请求覆盖了该值,则子线程无法继续处理上一次的连接任务了。改进的办法是子线程立马复制val的值在自己的栈区,但父线程必须保证子线程复制动作完成之后再执行新的accept()。但这执行起来并不简单,因为子线程与父线程的调度是独立的,父线程无法知道子线程何时复制完毕。这又得发生线程间通信,子线程复制完成后主动通知父线程。这样一来父线程的处理动作必然不能连贯,比起多进程环境,父线程显得效率有所下降。
关于资源不独立,看似是个缺点,但在有的情况下就成了优点。多进程环境间完全独立,要实现通信的话就得采用进程间的通信方式,它们通常都是耗时间的。而线程则不用任何手段数据就是共享的。当然多个子线程在同时执行写入操作时需要实现互斥。
1.2.4 实体间(进程间,线程间,进线程间)通信方式的不同
进程间的通信方式有这样几种:
A.共享内存 B.消息队列 C.信号量 D.有名管道 E.无名管道 F.信号
G.文件 H.socket
线程间的通信方式上述进程间的方式都可沿用,且还有自己独特的几种:
A.互斥量 B.自旋锁 C.条件变量 D.读写锁 E.线程信号 G.全局变量
进程间采用的通信方式要么需要切换内核上下文,要么要与外设访问(有名管道,文件)。所以速度会比较慢。而线程采用自己特有的通信方式的话,基本都在自己的进程空间内完成,不存在切换,所以通信速度会较快。也就是说,进程间与线程间分别采用的通信方式,除了种类的区别外,还有速度上的区别。
说明: 当运行多线程的进程捕获到信号时,只会阻塞主线程,其他子线程不会影响会继续执行。
1.2.5 控制方式的异同
进程与线程的身份标示ID管理方式不一样,进程的ID为pid_t类型,实际为一个int型的变量(也就是说是有限的)。
在全系统中,进程ID是唯一标识,对于进程的管理都是通过PID来实现的。每创建一个进程,内核去中就会创建一个结构体来存储该进程的全部信息:
每一个存储进程信息的节点也都保存着自己的PID。需要管理该进程时就通过这个ID来实现(比如发送信号)。当子进程结束要回收时(子进程调用exit()退出或代码执行完),需要通过wait()系统调用来进行,未回收的消亡进程会成为僵尸进程,其进程实体已经不复存在,但会虚占PID资源,因此回收是有必要的。
线程的ID是一个long型变量:
它的范围大得多,管理方式也不一样。线程ID一般在本进程空间内作用就可以了,当然系统在管理线程时也需要记录其信息。
对于线程而言,若要主动终止需要调用pthread_exit() ,主线程需要调用pthread_join()来回收(前提是该线程没有设置 “分离属性”)。像线发送线程信号也是通过线程ID实现的。
1.2.6 资源管理方式的异同
进程本身是资源分配的基本单位,因而它的资源都是独立的,如果有多进程间的共享资源,就要用到进程间的通信方式了,比如共享内存。共享数据就放在共享内存去,大家都可以访问,为保证数据写入的安全,加上信号量一同使用。一般而言,共享内存都是和信号量一起使用。消息队列则不同,由于消息的收发是原子操作,因而自动实现了互斥,单独使用就是安全的。
线程间要使用共享资源不需要用共享内存,直接使用全局变量即可,或者malloc()动态申请内存。显得方便直接。而且互斥使用的是同一进程空间内的互斥量,所以效率上也有优势。
1.3 线程接口函数
1.3.1 创建线程
pthread_create是Unix操作系统(Unix、Linux等)的创建线程的函数。
- 编译时需要指定链接库:-lpthread
- 函数原型
#include <pthread.h> int pthread_create ( pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg ); |
- 参数说明第一个参数为指向线程标识符的指针。
第二个参数用来设置线程属性。默认可填NULL。
第三个参数是线程运行函数的起始地址。
最后一个参数是运行函数的参数。不需要参数可填NULL。 - Linux下查看函数帮助:# man pthread_create
- 返回值:若线程创建成功,则返回0。若线程创建失败,则返回出错编号。
线程创建成功后, attr参数用于指定各种不同的线程属性。新创建的线程从start_rtn函数的地址开始运行,该函数只有一个万能指针参数arg,如果需要向线程工作函数传递的参数不止一个,那么需要把这些参数放到一个结构中,然后把这个结构的地址作为arg的参数传入。
示例:
#include <stdio.h> #include <pthread.h> //线程函数1 void *pthread_func1(void *arg) while(1) printf("线程函数1正在运行.....\\n"); sleep(2); //线程函数2 void *pthread_func2(void *arg) while(1) printf("线程函数2正在运行.....\\n"); sleep(2); int main(int argc,char **argv) pthread_t thread_id1; pthread_t thread_id2; /*1. 创建线程1*/ if(pthread_create(&thread_id1,NULL,pthread_func1,NULL)) printf("线程1创建失败!\\n"); return -1; /*2. 创建线程2*/ if(pthread_create(&thread_id2,NULL,pthread_func2,NULL)) printf("线程2创建失败!\\n"); return -1; /*3. 等待线程结束,释放线程的资源*/ pthread_join(thread_id1,NULL); pthread_join(thread_id2,NULL); return 0; //gcc pthread_demo_code.c -lpthread |
1.3.3 退出线程
线程通过调用pthread_exit函数终止执行,就如同进程在结束时调用exit函数一样。这个函数的作用是,终止调用它的线程并返回一个指向某个对象的指针。
这个函数的作用是,终止调用它的线程并返回一个指向某个对象的指针,该返回值可以通过pthread_join函数的第二个参数得到。
- 函数原型
#include <pthread.h> void pthread_exit(void *retval); |
- 参数解析线程的需要返回的地址。
注意:线程结束必须释放线程堆栈,就是说线程函数必须调用pthread_exit()结束,否则直到主进程函数退出才释放
1.3.3 等待线程结束
pthread_join()函数,以阻塞的方式等待thread指定的线程结束。当函数返回时,被等待线程的资源被收回。如果线程已经结束,那么该函数会立即返回。并且thread指定的线程必须是joinable(结合属性)属性。
- 函数原型
#include <pthread.h> int pthread_join(pthread_t thread, void **retval); |
- 参数第一个参数: 线程标识符,即线程ID,标识唯一线程。
最后一个参数: 用户定义的指针,用来存储被等待线程返回的地址。 - 返回值 0代表成功。 失败,返回的则是错误号。
- 接收线程返回值示例:
//退出线程 pthread_exit ("线程已正常退出"); //接收线程的返回值 void *pth_join_ret1; pthread_join( thread1, &pth_join_ret1); |
1.3.4 线程分离属性
创建一个线程默认的状态是joinable(结合属性),如果一个线程结束运行但没有调用pthread_join,则它的状态类似于进程中的Zombie Process(僵死进程),即还有一部分资源没有被回收(退出状态码),所以创建线程者应该pthread_join来等待线程运行结束,并可得到线程的退出代码,回收其资源(类似于进程的wait,waitpid)。但是调用pthread_join(pthread_id)函数后,如果该线程没有运行结束,调用者会被阻塞,在有些情况下我们并不希望如此。
pthread_detach函数可以将该线程的状态设置为detached(分离状态),则该线程运行结束后会自动释放所有资源。
- 函数原型
#include <pthread.h> int pthread_detach(pthread_t thread); |
- 参数线程标识符
- 返回值0表示成功。错误返回错误码。
EINVAL线程并不是一个可接合线程。
ESRCH没有线程ID可以被发现。
1.3.5 获取当前线程的标识符
pthread_self函数功能是获得线程自身的ID。
- 函数原型
#include <pthread.h> pthread_t pthread_self(void); |
- 返回值当前线程的标识符。
pthread_t的类型为unsigned long int,所以在打印的时候要使用%lu方式,否则显示结果出问题。
1.3.6 自动清理线程资源
线程可以安排它退出时需要调用的函数,这样的函数称为线程清理处理程序。用于程序异常退出的时候做一些善后的资源清理。
在POSIX线程API中提供了一个pthread_cleanup_push()/pthread_cleanup_pop()函数用于自动释放资源。从pthread_cleanup_push()的调用点到pthread_cleanup_pop()之间的程序段中的终止动作(包括调用 pthread_exit()和异常终止)都将执行pthread_cleanup_push()所指定的清理函数。
注意:pthread_cleanup_push函数与pthread_cleanup_pop函数需要成对调用。
- 函数原型
void pthread_cleanup_push(void (*routine)(void *),void *arg); //注册清理函数 void pthread_cleanup_pop(int execute); //释放清理函数 |
- 参数void (*routine)(void *) :处理程序的函数入口。
void *arg :传递给处理函数的形参。
int execute:执行的状态值。 0表示不调用清理函数。1表示调用清理函数。 - 导致清理函数调用的条件:
- 调用pthread_exit()函数
- pthread_cleanup_pop的形参为1。
注意:return不会导致清理函数调用。
1.3.7 自动清理线程示例代码
#include <stdio.h> #include <pthread.h> #include <stdlib.h> //线程清理函数 void routine_func(void *arg) printf("线程资源清理成功\\n"); //线程工作函数 void *start_routine(void *dev) pthread_cleanup_push(routine_func,NULL); //终止线程 // pthread_exit(NULL); pthread_cleanup_pop(1); //1会导致清理函数被调用。0不会调用。 int main(int argc,char *argv[]) pthread_t thread_id; //存放线程的标识符 /*1. 创建线程*/ if(pthread_create(&thread_id,NULL,start_routine,NULL)!=0) printf("线程创建失败!\\n");
/*2.设置线程的分离属性*/ if(pthread_detach(thread_id)!=0) printf("分离属性设置失败!\\n"); while(1) return 0; |
1.3.8 线程取消函数
pthread_cancel函数为线程取消函数,用来取消同一进程中的其他线程。
头文件: #include <pthread.h>
函数原型:pthread_cancel(pthread_t tid);
1.4 线程栈空间设置
1.4.1 通过ulimit命令设置栈空间大小
pthread_create 创建线程时,若不指定分配堆栈大小,系统会分配默认值,查看默认值方法如下:
[root@tiny4412 ]#ulimit -s 10240 |
上面的10240单位是KB,也就是默认的线程栈空间大小为10M
也可以通过ulimit -a命令查看,其中的stack size也表示栈空间大小。
[root@tiny4412 ]#ulimit -a -f: file size (blocks) unlimited -t: cpu time (seconds) unlimited -d: data seg size (kb) unlimited -s: stack size (kb) 10240 -c: core file size (blocks) 0 -m: resident set size (kb) unlimited -l: locked memory (kb) 64 -p: processes 7512 -n: file descriptors 1024 -v: address space (kb) unlimited -w: locks unlimited -e: scheduling priority 0 -r: real-time priority 0 |
- 设置栈空间大小: ulimit -s <栈空间大小>
[root@tiny4412 ]#ulimit -s 8192 //设置栈空间大小 [root@tiny4412 ]#ulimit -s //查看栈空间大小 8192 //大小为8M |
注意: 栈空间设置只能在超级管理员用户权限下设置。
每个线程的栈空间都是独立的,如果栈空间溢出程序会出现段错误。如果一个进程有10个线程,那么分配的栈空间大小就是10*<每个线程栈大小>
例如:
int main(int argc,char **argv) char buff[1024*1024*10]; //在栈空间定义数组,如果超出了栈空间总大小程序会奔溃。 printf("hello world!\\n"); return 0; |
1.4.2 通过线程函数设置栈空间大小
堆栈最小值定义为 PTHREAD_STACK_MIN(单位字节),包含#include <limits.h>后可以通过打印其值查看。
示例:
#include <pthread.h> #include <limits.h> int main(void) printf("STACK_MIN:%d\\n",PTHREAD_STACK_MIN); //16384->16KB return 0; |
对于默认值可以通过pthread_attr_getstacksize (&attr, &stack_size); 打印stack_size来查看。
示例:
#include <pthread.h> #include <limits.h> int main(void) pthread_attr_t attr; int ret,stack_size; ret=pthread_attr_init(&attr); /*初始化线程属性*/ if(ret!=0)return -1; ret=pthread_attr_getstacksize(&attr,&stack_size); if(ret!=0)return -1; printf("stacksize=%d\\n",stack_size/1024/1024);//stack_size是字节单位 10485760 –10M return 0; |
尤其在嵌入式中内存不是很大,若采用默认值的话,会导致出现问题,若内存不足,则 pthread_create 会返回12,定义如下:
#define EAGAIN 11 #define ENOMEM 12 /* Out of memory */ |
上面了解了堆栈大小,下面就来了解如何使用 pthread_attr_setstacksize 重新设置堆栈大小。先看下它的原型:
#include <pthread.h> int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize); |
attr 是线程属性变量;stacksize 则是设置的堆栈大小。 返回值0,-1分别表示成功与失败。
示例:
#include <pthread.h> #include <limits.h> #include <stdio.h> int main(void) pthread_attr_t attr; int ret,stack_size=1024*1024*20;//20M /*1.初始化线程属性*/ ret=pthread_attr_init(&attr); if(ret!=0)return -1; /*2.设置栈空间大小*/ ret=pthread_attr_setstacksize(&attr,stack_size); if(ret!=0)return -1; /*3. 查看设置之后的栈空间大小*/ ret=pthread_attr_getstacksize(&attr,&stack_size); if(ret!=0)return -1; printf("stacksize=%dM\\n",stack_size/1024/1024);//stack_size是字节单位 return 0; |