一、线程介绍
什么是线程:
线程是操作系统能内够进行运算、执行的最小单位,它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
总结:线程是进程的一部分,是进程内负责执行的单位,进程是由资源单位(内存资源、信号处理方案、文件表)+执行单位组成,默认情况下进程内只有一个线程,但可以有多个。
线程的发展简史:
60年代,在操作系统中能拥有资源和独立运行的基本单位是进程。
随着计算机技术的发展,进程出现了很多弊端:
一是由于进程是资源拥有者,创建、撤消与切换存在较大的时间开销,因此需要引入轻型进程;
二是由于对称多处理机出现,可以满足多个运行单位,而多个进程并行开销过大。
因此在80年代,出现了能独立运行的基本单位——线程(Threads)。
线程的调度策略:
线程是独立调度和分派的基本单位,有三种不同的调试策略:
1、线程可以为操作系统内核调度的内核线程,如Win32线程;
2、由用户进行自行调度的用户线程,如Linux、UNIX平台的POSIX Thread;
3、由内核与用户进程进行混合调度,如Windows 7的线程。
多线程适用的范围:
一个进程可以有很多线程,每条线程并行执行不同的任务。 在多核或多CPU,或支持Hyper-threading的CPU上使用多线程程序设计的好处是显而易见,即提高了程序的执行吞吐率。
在单CPU单核的计算机上,使用多线程技术,可以把进程中负责I/O处理、人机交互而常被阻塞的部分与密集计算的部分分开来执行,原因就是线程占用的资源少,被阻塞时不浪费资源。
线程的特点:
轻型实体:
线程中的实体基本上不拥有系统资源,只是有一点必不可少的、能保证独立运行的资源。线程的实体包括用于指示被执行指令序列的程序计数器、局部变量、状态参数和返回地址。
线程是动态概念,它的动态特性由线程控制块TCB(Thread Control Block)描述,包括以下信息:
1、线程状态
2、当线程不运行时,被保存的现场资源。
3、一组执行堆栈
4、存放每个线程的局部变量主存区
5、访问同一个进程中的主存和其它资源
独立调度和分派的基本单位:
在多线程OS中,线程是能独立运行的基本单位,因而也是独立调度和分派的基本单位。由于线程很“轻”,故线程的切换非常迅速且开销小(在同一进程中的)。
可并发执行:
在一个进程中的多个线程之间,可以并发执行,甚至允许在一个进程中所有线程都能并发执行;同样,不同进程中的线程也能并发执行,充分利用和发挥了CPU与外围设备并行工作的能力。
共享进程资源:
在同一进程中的各个线程,都可以访问该进程的用户空间,此外,还可以访问进程所拥有的已打开文件、定时器、信号量等,线程可以共享该进程所拥有的资源。所以线程之间互相通信不必调用内核。
二、线程与进程的区别(多进程与多线程)
资源:
进程采用虚拟空间+用户态/内核态机制,所以就导致进程与进程之间是互相独立的,各自的资源不可见。
在同一进程中的各个线程都可以共享该进程所拥有的资源。
多进程之间资源是独立的,多线程之间资源是共享的。
通信:
由于进程之间是互相独立的,需要使用各种IPC通信机制,保障多个进程协同工作。
同一进程中的各个线程共享该进程所拥有的资源,线程间可以直接读写进程数据段来进行通信,但需要线程同步和互斥手段的辅助,以保证数据的一致性。
多进程之间资源是独立的,所以需要通信,多线程之间资源是共享的,所以需要同步和互斥。
调度:
无论系统采用什么样的线程调试策略,线程上下文切换都比进程上下文切换要快得多。
身份:
进程是个资源单位,线程是个执行单位,并且线程是进程的一部分,线程需要进程安身立命,进程也需要线程当牛做马。
三、POSIX线程库
POSIX线程库介绍:
POSIX线程(POSIX Threads,常被缩写为pthread)是POSIX的线程标准,定义了创建和操纵线程的一套API。 实现POSIX 线程标准的库常被称作pthread,一般用于Unix-likePOSIX 系统,如Linux、Solaris。但是Microsoft Windows上的实现也存在,例如直接使用Windows API实现的第三方库pthread-w32。
API具体内容:
pthread定义了一套C语言的类型、函数与常量,它以pthread.h头文件和一个接口库libpthread.so,gcc和g++编译器没有默认链接该库,需要程序员使用 -l pthread 参数进行手动链接。
pthread API中大致共有100个函数调用,全都以"pthread_"开头,并可以分为四类:
1、线程管理,如创建线程,等待线程,查询线程状态等。
2、互斥锁,有创建、摧毁、锁定、解锁、设置属性等操作
3、条件变量,有创建、摧毁、等待、通知、设置与查询属性等操作
4、使用了互斥锁的线程间的同步管理。
四、创建线程
int pthread_create (pthread_t* thread, const pthread_attr_t* attr, void* (*start_routine) (void*), void* arg); thread - 线程ID,输出型参数。我们目前使用的Linux中pthread_t即unsigned long int attr - 线程属性,NULL表示缺省属性,如果没有特殊需求,一般写NULL即可 start_routine - 线程入口函数指针,参数和返回值的类型都是void* 启动线程本质上就是调用一个函数,只不过是在一个独立的线程中调用的,函数返回即线程结束 arg - 传递给线程过程函数的参数 返回值:成功返回0,失败返回错误码,但不会修改全局的错误变量,也就是无法使用perror获取错误原因。 注意: 1、restrict: C99引入的编译优化指示符,提高重复解引用同一个指针的效率。 2、应设法保证在线程过程函数执行期间,其参数所指向的目标持久有效。
#include <stdio.h> #include <unistd.h> #include <pthread.h> void* run(void* arg) { for(;;) { printf("#"); fflush(stdout); sleep(1); } } int main(int argc,const char* argv[]) { pthread_t tid; int ret = pthread_create(&tid,NULL,run,NULL); printf("%d %lu\n",ret,tid); for(;;) { printf("*"); fflush(stdout); sleep(1); } return 0; }
五、线程回收
int pthread_join (pthread_t thread, void** retval); 功能:等待thread参数所标识的线程结束,并回收相关资源,如果thread线程没有结束则阻塞 retval:获得线程正常结束时的返回值,是输出型的参数,用于获取线程入口函数的返回值。 返回值:成功返回0,失败返回错误码 从线程过程函数中返回值的方法: 1、线程过程函数将所需返回的内容放在一块内存中,返回该内存块的首地址,保证这块内存在函数返回,即线程结束,以后依然有效; 2、若retval参数非NULL,则pthread_join函数将线程入口函数所返回的指针,拷贝到该参数所指向的内存中; 3、线程入口函数所返回的指针指向text、data、bss内存段的数据,如果指向heap内存段,则还需保证在用过该内存之后释放之。
六、获取线程ID、判断线程ID
pthread_t pthread_self (void); 成功返回调用线程的ID,不会失败。 int pthread_equal (pthread_t t1, pthread_t t2); 功能:若参数t1和t2所标识的线程ID相等,则返回非零,否则返回0。 注意:某些实现的pthread_t不是unsigned long int类型,可能是结构体类型,无法通过“==”判断其相等性。
练习:在一个多线程的进程中,设计一个函数,该函数只能由主线程调用,其它线程如果调用了该函数要立即结束执行。
七、终止线程
方法1:从线程入口函数中return,主线程除外。
方法2:调用pthread_exit函数。
void pthread_exit (void* retval); retval - 和线程过程函数的返回值语义相同。
注意:在任何线程中调用exit函数都将终止整个进程。
问题:主线程结束,子线程是否会跟着一起结束?
主线程结束,并不会导致子线程跟着一起结束,它们之间没有必然联系。
但是,主线程如果执行到最后一行,会执行return 0或隐藏的return 0,而在main函数中执行return 0就相当于执行exit(0),然后当前进程就会结束,有两种方法可以避免这种情况:
方法1:
等待所有子线程结束,主线程再执行return 0;
子线程在一定时间内会结束,侧使用pthread_join。
方法2:
立即结束主线程,不要让它执行return 0;
当子线程的结束时间不确定,则使用pthread_exit。
注意:这种情况会产生新的问题,子线程的资源没有办法回收。
#include <stdio.h> #include <pthread.h> #include <unistd.h> void* run(void* arg) { for(int i=0; ;i++) { printf("子线程:%lu %d\n",pthread_self(),i); sleep(1); } } int main(void) { pthread_t tid; pthread_create(&tid,NULL,run,NULL); for(int i=0; i<3; i++) { printf("我是主线程,我要结束了,倒计时:%d\n",3-i); sleep(1); } exit }
八、线程分离
同步方式(非分离状态):创建线程之后主线程调用pthread_join函数等待其终止,并释放线程资源。
异步方式(分离状态):无需创建者等待,线程终止后自行释放资源。
int pthread_detach (pthread_t thread); 功能:使thread参数所标识的线程进入分离(DETACHED)状态。 返回值:成功返回0,失败返回错误码。
注意:如果若干个子线程需要长时间执行,不知道什么时候能结束,为了避免它父线程陷入无尽的等待,可提前给子线程设置分离状态。
九、取消线程
向发送取消请求:
int pthread_cancel (pthread_t thread); 功能:该函数只是向线程发出取消请求,并不等待线程终止。 缺省情况下,线程在收到取消请求以后,并不会立即终止,而是仍继续运行,直到其达到某个取消点。 在取消点处,线程检查其自身是否已被取消了,并做出相应动作。
设置可取消状态:
int pthread_setcancelstate (int state,int* oldstate); 成功返回0,并通过oldstate参数输出原可取消状态(若非NULL),失败返回错误码。 state取值: PTHREAD_CANCEL_ENABLE - 接受取消请求(缺省)。 PTHREAD_CANCEL_DISABLE - 忽略取消请求。
设置可取消类型:
int pthread_setcanceltype (int type, int* oldtype); 成功返回0,并通过oldtype参数输出原可取消类型 (若非NULL),失败返回错误码。 type取值: PTHREAD_CANCEL_DEFERRED - 延迟取消(缺省)。 被取消线程在接收到取消请求之后并不立即响应, 而是一直等到执行了特定的函数(取消点)之后再响应该请求。 PTHREAD_CANCEL_ASYNCHRONOUS - 异步取消。 被取消线程可以在任意时间取消,不是非得遇到取消点才能被取消。 但是操作系统并不能保证这一点。
十、线程属性
int pthread_create (pthread_t* restrict thread, const pthread_attr_t* restrict attr, void* (*start_routine) (void*), void* restrict arg); //创建线程函数的第二个参数即为线程属性,传空指针表示使用缺省属性。 typedef struct { // 分离状态 int detachstate; // PTHREAD_CREATE_DETACHED - 分离线程。 // PTHREAD_CREATE_JOINABLE(缺省) - 可汇合线程。 // 竞争范围 int scope; // PTHREAD_SCOPE_SYSTEM - 在系统范围内竞争资源(时间片)。 // PTHREAD_SCOPE_PROCESS(Linux不支持) - 在进程范围内竞争资源。 // 继承特性 int inheritsched; // PTHREAD_INHERIT_SCHED(缺省) - 调度属性自创建者线程继承。 // PTHREAD_EXPLICIT_SCHED - 调度属性由后面两个成员确定。 // 调度策略 nt schedpolicy; // SCHED_FIFO - 先进先出策略。 // 没有时间片。 // 一个FIFO线程会持续运行,直到阻塞或有高优先级线程就绪。 // 当FIFO线程阻塞时,系统将其移出就绪队列,待其恢复时再加到同优先级就绪队列的末尾。 // 当FIFO线程被高优先级线程抢占时,它在就绪队列中的位置不变。 // 因此一旦高优先级线程终止或阻塞,被抢占的FIFO线程将会立即继续运行。 // SCHED_RR - 轮转策略。 // 给每个RR线程分配一个时间片,一但RR线程的时间片耗尽,系统即将移到就绪队列的末尾。 // SCHED_OTHER(缺省) - 普通策略。 // 静态优先级为0。任何就绪的FIFO线程或RR线程,都会抢占此类线程。 // 调度参数 struct sched_param schedparam; // struct sched_param { // int sched_priority; /* 静态优先级 */ // }; // 栈尾警戒区大小(字节) 缺省一页(4096字节)。 size_t guardsize; // 栈地址 void* stackaddr; // 栈大小(字节) size_t stacksize; } pthread_attr_t; 注意:不要手动读写该结构体,而应调用pthread_attr_set/get函数设置/获取具体属性项。
设置线程属性:
初始化线程属性结构体:
pthread_attr_t attr = {}; // 不要使用这种方式 int pthread_attr_init (pthread_attr_t* attr);
设置具体线程属性项:
int pthread_attr_setdetachstate (pthread_attr_t* attr,int detachstate); int pthread_attr_setscope (pthread_attr_t* attr,int scope); int pthread_attr_setinheritsched (pthread_attr_t* attr,int inheritsched); int pthread_attr_setschedpolicy (pthread_attr_t* attr,int policy); int pthread_attr_setschedparam (pthread_attr_t* attr,const struct sched_param* param); int pthread_attr_setguardsize (pthread_attr_t* attr,size_t guardsize); int pthread_attr_setstackaddr (pthread_attr_t* attr,void* stackaddr); int pthread_attr_setstacksize (pthread_attr_t* attr,size_t stacksize); int pthread_attr_setstack (pthread_attr_t* attr,void* stackaddr, size_t stacksize);
以设置好的线程属性结构体为参数创建线程:
int pthread_create (pthread_t* restrict thread, const pthread_attr_t* testrict attr, void* (*start_routine) (void*), void* restrict arg);
销毁线程属性结构体:
int pthread_attr_destroy (pthread_attr_t* attr);
获取线程属性:
获取线程属性结构体:
int pthread_getattr_np (pthread_t thread,pthread_attr_t* attr);
获取具体线程属性项:
int pthread_attr_getdetachstate (pthread_attr_t* attr,int* detachstate); int pthread_attr_getscope (pthread_attr_t* attr,int* scope); int pthread_attr_getinheritsched (pthread_attr_t* attr,int* inheritsched); int pthread_attr_getschedpolicy (pthread_attr_t* attr,int* policy); int pthread_attr_getschedparam (pthread_attr_t* attr,struct sched_param* param); int pthread_attr_getguardsize (pthread_attr_t* attr,size_t* guardsize); int pthread_attr_getstackaddr (pthread_attr_t* attr,void** stackaddr); int pthread_attr_getstacksize (pthread_attr_t* attr,size_t* stacksize); int pthread_attr_getstack (pthread_attr_t* attr,void** stackaddr, size_t* stacksize); 以上所有函数成功返回0,失败返回错误码。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #define __USE_GNU #include <pthread.h> int printattrs (pthread_attr_t* attr) { printf("------- 线程属性 -------\n"); int detachstate; int error = pthread_attr_getdetachstate (attr, &detachstate); if (error) { fprintf (stderr, "pthread_attr_getdetachstate: %s\n",strerror (error)); return -1; } printf("分离状态: %s\n", (detachstate == PTHREAD_CREATE_DETACHED) ? "分离线程" : (detachstate == PTHREAD_CREATE_JOINABLE) ? "可汇合线程" : "未知"); int scope; if ((error = pthread_attr_getscope (attr, &scope)) != 0) { fprintf (stderr, "pthread_attr_getscope: %s\n", strerror (error)); return -1; } printf ("竞争范围: %s\n", (scope == PTHREAD_SCOPE_SYSTEM) ? "系统级竞争" : (scope == PTHREAD_SCOPE_PROCESS) ? "进程级竞争" : "未知"); int inheritsched; if ((error = pthread_attr_getinheritsched (attr, &inheritsched)) != 0) { fprintf (stderr, "pthread_attr_getinheritsched: %s\n", strerror (error)); return -1; } printf ("继承特性: %s\n", (inheritsched == PTHREAD_INHERIT_SCHED) ? "继承调用属性" : (inheritsched == PTHREAD_EXPLICIT_SCHED) ? "显式调用属性" : "未知"); int schedpolicy; if ((error = pthread_attr_getschedpolicy(attr,&schedpolicy)) != 0) { fprintf (stderr, "pthread_attr_getschedpolicy: %s\n",strerror (error)); return -1; } printf ("调度策略: %s\n", (schedpolicy == SCHED_OTHER) ? "普通" : (schedpolicy == SCHED_FIFO) ? "先进先出" : (schedpolicy == SCHED_RR) ? "轮转" : "未知"); struct sched_param schedparam; if ((error = pthread_attr_getschedparam (attr, &schedparam)) != 0) { fprintf (stderr, "pthread_attr_getschedparam: %s\n",strerror (error)); return -1; } printf ("调度优先级:%d\n", schedparam.sched_priority); size_t guardsize; if ((error = pthread_attr_getguardsize(attr, &guardsize)) != 0) { fprintf (stderr, "pthread_attr_getguardsize: %s\n",strerror (error)); return -1; } printf ("栈尾警戒区:%u字节\n", guardsize); /* void* stackaddr; if ((error = pthread_attr_getstackaddr (attr, &stackaddr)) != 0) { fprintf (stderr, "pthread_attr_getstackaddr: %s\n",strerror (error)); return -1; } printf ("栈地址: %p\n", stackaddr); size_t stacksize; if ((error = pthread_attr_getstacksize (attr, &stacksize)) != 0) { fprintf (stderr, "pthread_attr_getstacksize: %s\n",strerror (error)); return -1; } printf ("栈大小: %u字节\n", stacksize); */ void* stackaddr; size_t stacksize; if ((error = pthread_attr_getstack (attr, &stackaddr,&stacksize)) != 0) { fprintf (stderr, "pthread_attr_getstack: %s\n",strerror (error)); return -1; } printf ("栈地址: %p\n", stackaddr); printf ("栈大小: %u字节\n", stacksize); printf("------------------------\n"); return 0; } void* thread_proc (void* arg) { pthread_attr_t attr; int error = pthread_getattr_np (pthread_self (), &attr); if (error) { fprintf (stderr, "pthread_getattr_np: %s\n", strerror (error)); exit (EXIT_FAILURE); } if (printattrs (&attr) < 0) exit (EXIT_FAILURE); exit (EXIT_SUCCESS); return NULL; } int main (int argc, char* argv[]) { int error; pthread_attr_t attr, *pattr = NULL; if (argc > 1) { if (strcmp (argv[1], "-s")) { fprintf (stderr, "用法:%s [-s]\n", argv[0]); return -1; } if ((error = pthread_attr_init (&attr)) != 0) { fprintf (stderr, "pthread_attr_init: %s\n",strerror (error)); return -1; } if ((error = pthread_attr_setdetachstate (&attr,PTHREAD_CREATE_DETACHED)) != 0) { fprintf (stderr, "pthread_attr_setdetachstate: %s\n",strerror (error)); return -1; } if ((error = pthread_attr_setinheritsched (&attr,PTHREAD_EXPLICIT_SCHED)) != 0) { fprintf (stderr, "pthread_attr_setinheritsched: %s\n",strerror (error)); return -1; } if ((error = pthread_attr_setstacksize (&attr, 4096*10)) != 0) { fprintf (stderr, "pthread_attr_setstack: %s\n",strerror (error)); return -1; } pattr = &attr; } pthread_t tid; if ((error = pthread_create (&tid, pattr, thread_proc,NULL)) != 0) { fprintf (stderr, "pthread_create: %s\n", strerror (error)); return -1; } if (pattr) { if ((error = pthread_attr_destroy (pattr)) != 0) { fprintf (stderr, "pthread_attr_destroy: %s\n",strerror (error)); return -1; } } pause (); return 0; }
注意:如果man手册查不到线程的相关函数,安装完整版gnu手册:sudo apt-get install glibc-doc。
练习:实现大文件的多线程cp拷贝,对比系统cp命令,哪个速度更快,为什么?
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <pthread.h> #include <sys/stat.h> #include <sys/types.h> typedef struct Task { char* src; char* dest; size_t start; size_t end; }Task; void* run(void* arg) { Task* task = arg; // 打开源文件和目标文件 FILE* src_fp = fopen(task->src,"r"); FILE* dest_fp = fopen(task->dest,"a"); if(NULL == src_fp || NULL == dest_fp) { perror("fopen"); return NULL; } // 调整文件的位置指针 fseek(src_fp,task->start,SEEK_SET); fseek(dest_fp,task->start,SEEK_SET); // 创建缓冲区 char buf[1024]; size_t buf_size = sizeof(buf); for(int i=task->start; i<task->end; i+=buf_size) { int ret = fread(buf,1,buf_size,src_fp); if(0 >= ret) break; fwrite(buf,1,ret,dest_fp); } fclose(src_fp); fclose(dest_fp); free(task); } int main(int argc,const char* argv[]) { if(3 != argc) { puts("Use:./cp <src> <dest>"); return 0; } // 获取到文件的大小 struct stat buf; if(stat(argv[1],&buf)) { perror("stat"); return -1; } // 创建出目标文件 if(NULL == fopen(argv[2],"w")) { perror("fopen"); return -2; } // 计算需要的线程数量,以100M为单位 size_t pthread_cnt = buf.st_size/(1024*1024*100)+1; // 分配任务 pthread_t tid; for(int i=0; i<pthread_cnt; i++) { Task* task = malloc(sizeof(Task)); task->src = (char*)argv[1]; task->dest = (char*)argv[2]; task->start = i*1024*1024*100; task->end = (i+1)*1024*1024*100; // 创建子线程并分配任务 pthread_create(&tid,NULL,run,task); // 分享子线程 pthread_detach(tid); } // 结束主线程 pthread_exit(NULL); }
多线程并不能提高运行速度,反而可能会降低,所以多线程不适合解决运算密集性问题,而是适合解决等待、阻塞的问题,如果使用进程去等待,会浪费大量资源,所以使用更轻量的线程去等待,节约资源。