1.线程的概念
在许多经典的操作系统教科书中,总是把进程定义为程序的执行实例,它并不执行什么, 只是维护应用程序所需的各种资源,而线程则是真正的执行实体。进程为了完成一定的工作,必须包含至少一个线程。
进程拥有资源,而线程基本上不拥有资源,只有一点在运行中必不可少的资源(程序计数器、寄存器和栈),但可以和同一进程的其他线程共享进程的资源。
进程是CPU分配资源的最小单位,而线程是CPU调度的最小单位。
如果说进程是一个资源管家,负责从主人那里要资源的话,那么线程就是干活的苦力。一个管家必须完成一项工作,就需要最少一个苦力,也就是说,一个进程最少包含一个线程,也可以包含多个线程。苦力要干活,就需要依托于管家,所以说一个线程,必须属于某一个进程。
2.linux线程的发展
最初linux实现线程采用的LinuxThreads并不能真正支持多线程,主要使用clone() 系统调用创造新进程作为可调度的实体,并且共享父进程的地址空间。总体LinuxThreads来说存在一些缺点。
后来NPTL(Native POSIX Thread Library)项目取代了LinuxThreads
3.线程特点
线程是轻量级进程(light-weight process),也有PCB,创建线程使用的底层函数和进程一样,都是clone。(也就是说,无论是创建进程的fork,还是创建线程的pthread_create,底层实现都是调用同一个内核函数 clone 。不同的是,复制对方的地址空间,那么就产生一个“进程”;共享对方的地址空间,那么就产生一个“线程”)
从内核里看进程和线程是一样的,都有各自不同的PCB。但是会有一个标志,标记出线程。
进程可以蜕变成线程
在linux下,线程最是小的执行单位;进程是最小的分配资源单位
Linux内核是不区分进程和线程的, 只在用户层面上进行区分。所以,线程所有操作函数 pthread_* 是库函数,而非系统调用。
4.线程共享资源
文件描述符表
每种信号的处理方式
当前工作目录
用户ID和组ID
内存地址空间 (.text/.data/.bss/heap/共享库)
5.线程非共享资源
线程id
处理器现场和栈指针(内核栈)
独立的栈空间(用户空间栈)
errno变量
信号屏蔽字
调度优先级
6.多线程优缺点
优点:
Ø 提高程序并发性
Ø 开销小
Ø 数据通信、共享数据方便
缺点:
Ø 库函数,不稳定
Ø 调试、编写困难、gdb不支持
Ø 对信号支持不好
优点相对突出,缺点均不是硬伤。Linux下由于实现方法导致进程、线程差别不是很大。
一般用线程,如果有问题再用进程。
7.线程操作
1.线程号
进程号系统中唯一,但是线程号只在该进程中唯一。
进程号用 pid_t 数据类型表示,是一个非负整数;线程号则用 pthread_t 数据类型来表示,Linux 使用无符号长整数表示。
有的系统在实现pthread_t 的时候,用一个结构体来表示,所以在可移植的操作系统实现不能把它做为整数处理。
pthread_self函数:
1 2 3 4 5 6 7 8 9
| #include <pthread.h>
pthread_t pthread_self(void); 功能: 获取线程号。 参数: 无 返回值: 调用线程的线程 ID 。
|
编译使用了thread相关函数的文件时,需要加上参数“-pthread
”,一定要加,否则可能会报错“undefined reference to ‘pthread_create’”。
pthread_equal函数:
1 2 3 4 5 6 7 8
| int pthread_equal(pthread_t t1, pthread_t t2); 功能: 判断线程号 t1 和 t2 是否相等。为了方便移植,尽量使用函数来比较线程 ID。 参数: t1,t2:待判断的线程号。 返回值: 相等: 非 0 不相等:0
|
2.线程创建
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| #include <stdio.h> #include <sys/types.h> #include <unistd.h> #include <string.h> #include <stdlib.h> #include <pthread.h>
void *fun(){ printf("fun thread id: %u\n",pthread_self()); return NULL; }
void *fun2(void* arg){ printf("fun2 thread id: %u\n",pthread_self()); printf("arg is %d\n", (int)(long)arg); return NULL; }
int main() { int ret = -1; pthread_t tid = -1, tid2 = -1;
ret = pthread_create(&tid,NULL,fun,NULL); if(ret != 0){ perror("pthread_create\n"); }
ret = pthread_create(&tid2,NULL,fun2,(void*)0x3); if(ret != 0){ perror("pthread_create\n"); } pthread_join(tid,NULL); pthread_join(tid2,NULL); printf("main thread id: %u\n",pthread_self());
return 0; }
|
3.线程资源回收
使用pthread_join
函数等待线程结束(此函数会阻塞),并回收线程资源
1 2 3 4 5 6 7 8 9 10 11
| #include <pthread.h>
int pthread_join(pthread_t thread, void **retval); 功能: 等待线程结束(此函数会阻塞),并回收线程资源,类似进程的 wait() 函数。如果线程已经结束,那么该函数会立即返回。 参数: thread:被等待的线程号。 retval:用来存储线程退出状态的指针的地址。 返回值: 成功:0 失败:非 0
|
上面的代码中已经使用过pthread_join
根据线程的结束方式,retval
指针中存放不同的值。
验证线程共享进程资源:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| #include <stdio.h> #include <sys/types.h> #include <unistd.h> #include <string.h> #include <stdlib.h> #include <pthread.h>
int n = 100;
void *fun2(void* arg){ int *p = (int*) arg; printf("fun2 before p=%d, n=%d\n",*p,n); ++(*p); ++n; printf("fun2 after p=%d, n=%d\n",*p,n); return NULL; }
int main() { int ret = -1; pthread_t tid = -1, tid2 = -1;
int *p = NULL; p = malloc(sizeof(int)); if(p ==NULL){ printf("malloc failed.\n"); return 1; } *p = 1;
ret = pthread_create(&tid2,NULL,fun2,(void*)p); if(ret != 0){ perror("pthread_create\n"); return 1; } pthread_join(tid2,NULL); printf("main p=%d, n=%d\n",*p,n); free(p); p = NULL;
return 0; }
|
运行结果
4.线程分离
一般情况下,线程终止后,其终止状态一直保留到其它线程调用pthread_join获取它的状态为止。但是线程也可以被置为detach状态,这样的分离后的线程一旦终止就立刻回收它占用的所有资源,而不保留终止状态。(并不是分离后线程不再依赖进程)
分离之后,不能再使用pthread_join()获取其状态。
1 2 3 4 5 6 7 8 9 10
| #include <pthread.h>
int pthread_detach(pthread_t thread); 功能: 使调用线程与当前进程分离,分离后不代表此线程不依赖与当前进程,线程分离的目的是将线程资源的回收工作交由系统自动来完成,也就是说当被分离的线程结束之后,系统会自动回收它的资源。所以,此函数不会阻塞。 参数: thread:线程号。 返回值: 成功:0 失败:非0
|
5.线程退出
线程退出不可以用exit()
,因为这样会直接退出该进程,后续代码和其他线程等都不会被执行。
- 线程从执行函数中
return
返回。
- 线程调用
pthread_exit
退出线程。
- 线程可以被同一进程中的其它线程取消。
1 2 3 4 5 6 7 8
| #include <pthread.h>
void pthread_exit(void *retval); 功能: 退出调用线程。一个进程中的多个线程是共享该进程的数据段,因此,通常线程退出后所占用的资源并不会释放。 参数: retval:存储线程退出状态的指针。 返回值:无
|
等价于return
6.线程取消
注意:线程的取消并不是实时的,而是有一定的延时。需要等待线程到达某个取消点(检查点)。
取消点:是线程检查是否被取消,并按请求进行动作的一个位置。通常是一些系统调用creat,open,pause,close,read,write….. 执行命令man 7 pthreads可以查看具备这些取消点的系统调用列表。
8.线程属性
线程属性一般使用默认参数;
对程序的性能提出更高的要求那么需要设置线程属性,比如可以通过设置线程栈的大小来降低内存的使用,增加最大线程个数。
线程属性:
1 2 3 4 5 6 7 8 9 10 11 12
| typedef struct { int etachstate; int schedpolicy; struct sched_param schedparam; int inheritsched; int scope; size_t guardsize; int stackaddr_set; void* stackaddr; size_t stacksize; } pthread_attr_t;
|
主要结构体成员:
线程分离状态
线程栈大小(默认平均分配)
线程栈警戒缓冲区大小(位于栈末尾)
线程栈最低地址
1.流程:
- 在线程创建前,pthread_attr_init对线程属性初始化(之后才能设置);
- 在线程创建前,设置线程属性;
- 线程结束后,pthread_attr_destroy销毁线程属性。
2.线程分离状态
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| #include <pthread.h>
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate); 功能:设置线程分离状态 参数: attr:已初始化的线程属性 detachstate: 分离状态 PTHREAD_CREATE_DETACHED(分离线程) PTHREAD_CREATE_JOINABLE(非分离线程) 返回值: 成功:0 失败:非0
int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate); 功能:获取线程分离状态 参数: attr:已初始化的线程属性 detachstate: 分离状态 PTHREAD_CREATE_DETACHED(分离线程) PTHREAD _CREATE_JOINABLE(非分离线程) 返回值: 成功:0 失败:非0
|
3.线程栈地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| #include <pthread.h>
int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize); 功能:设置线程的栈地址 参数: attr:指向一个线程属性的指针 stackaddr:内存首地址 stacksize:返回线程的堆栈大小 返回值: 成功:0 失败:错误号
int pthread_attr_getstack(const pthread_attr_t *attr, void **stackaddr, size_t *stacksize); 功能:获取线程的栈地址 参数: attr:指向一个线程属性的指针 stackaddr:返回获取的栈地址 stacksize:返回获取的栈大小 返回值: 成功:0 失败:错误号
|
4.线程栈大小
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| #include <pthread.h>
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize); 功能:设置线程的栈大小 参数: attr:指向一个线程属性的指针 stacksize:线程的堆栈大小 返回值: 成功:0 失败:错误号
int pthread_attr_getstacksize(const pthread_attr_t *attr, size_t *stacksize); 功能:获取线程的栈大小 参数: attr:指向一个线程属性的指针 stacksize:返回线程的堆栈大小 返回值: 成功:0 失败:错误号
|
参考程序
整体思路是在创建线程前初始化线程属性,并且对线程属性进行设置。最后,线程结束后销毁线程属性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| #define SIZE 0x100000
void *th_fun(void *arg) { while (1) { sleep(1); } }
int main() { pthread_t tid; int err, detachstate, i = 1;
pthread_attr_t attr; size_t stacksize; void *stackaddr;
pthread_attr_init(&attr); pthread_attr_getstack(&attr, &stackaddr, &stacksize); pthread_attr_getdetachstate(&attr, &detachstate);
if (detachstate == PTHREAD_CREATE_DETACHED) { printf("thread detached\n"); } else if (detachstate == PTHREAD_CREATE_JOINABLE) { printf("thread join\n"); } else { printf("thread unknown\n"); } pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
while (1) { stackaddr = malloc(SIZE); if (stackaddr == NULL) { perror("malloc"); exit(1); }
stacksize = SIZE; pthread_attr_setstack(&attr, stackaddr, stacksize); err = pthread_create(&tid, &attr, th_fun, NULL); if (err != 0) { printf("%s\n", strerror(err)); exit(1); } printf("%d\n", i++); }
pthread_attr_destroy(&attr);
return 0; }
|
9.线程注意事项
主线程退出其他线程不退出,主线程应调用pthread_exit
避免僵尸线程
a) pthread_join
b) pthread_detach
c) pthread_create指定分离属性
被join线程可能在join函数返回前就释放完自己的所有内存资源,所以不应当返回被回收线程栈中的值;
malloc和mmap申请的内存可以被其他线程释放
应避免在多线程模型中调用fork,除非马上exec,子进程中只有调用fork的线程存在,其他线程t在子进程中均pthread_exit
信号的复杂语义很难和多线程共存,应避免在多线程引入信号机制
10.同步
- 熟练掌握互斥量的使用
- 说出什么叫死锁以及解决方案
- 熟练掌握读写锁的使用
- 熟练掌握条件变量的使用
- 熟练掌握条件变量实现生产消费者模型
- 熟练掌握信号量实现生产消费者模型
11.互斥
1.概念
现代操作系统基本都是多任务操作系统,即同时有大量可调度实体在运行。在多任务操作系统中,同时运行的多个任务可能:
- 都需要访问/使用同一种资源
- 多个任务之间有依赖关系,某个任务的运行依赖于另一个任务
这两种情形是多任务编程中遇到的最基本的问题,也是多任务编程中的核心问题,同步和互斥就是用于解决这两个问题的。
互斥:是指散步在不同任务之间的若干程序片断,当某个任务运行其中一个程序片段时,其它任务就不能运行它们之中的任一程序片段,只能等到该任务运行完这个程序片段后才可以运行。最基本的场景就是:一个公共资源同一时刻只能被一个进程或线程使用,多个进程或线程不能同时使用公共资源。
同步:是指散步在不同任务之间的若干程序片断,它们的运行必须严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。最基本的场景就是:两个或两个以上的进程或线程在运行过程中协同步调,按预定的先后次序运行。比如 A 任务的运行依赖于 B 任务产生的数据。
显然,同步是一种更为复杂的互斥,而互斥是一种特殊的同步。也就是说互斥是两个任务之间不可以同时运行,他们会相互排斥,必须等待一个线程运行完毕,另一个才能运行,而同步也是不能同时运行,但他是必须要按照某种次序来运行相应的线程(也是一种互斥)!因此互斥具有唯一性和排它性,但互斥并不限制任务的运行顺序,即任务是无序的,而同步的任务之间则有顺序关系。
2.使用函数
restrict,C语言中的一种类型限定符(Type Qualifiers),用于告诉编译器,对象已经被指针所引用,不能通过除该指针外所有其他直接或间接的方式修改该对象的内容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| #include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr); 功能: 初始化一个互斥锁。 参数: mutex:互斥锁地址。类型是 pthread_mutex_t 。 attr:设置互斥量的属性,通常可采用默认属性,即可将 attr 设为 NULL。
可以使用宏 PTHREAD_MUTEX_INITIALIZER 静态初始化互斥锁,比如: pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
这种方法等价于使用 NULL 指定的 attr 参数调用 pthread_mutex_init() 来完成动态初始化,不同之处在于 PTHREAD_MUTEX_INITIALIZER 宏不进行错误检查。
返回值: 成功:0,成功申请的锁默认是打开的。 失败:非 0 错误码
|
1 2 3 4 5 6 7 8 9 10
| #include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex); 功能: 销毁指定的一个互斥锁。互斥锁在使用完毕后,必须要对互斥锁进行销毁,以释放资源。 参数: mutex:互斥锁地址。 返回值: 成功:0 失败:非 0 错误码
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| #include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex); 功能: 对互斥锁上锁,若互斥锁已经上锁,则调用者阻塞,直到互斥锁解锁后再上锁。 参数: mutex:互斥锁地址。 返回值: 成功:0 失败:非 0 错误码
int pthread_mutex_trylock(pthread_mutex_t *mutex); 调用该函数时,若互斥锁未加锁,则上锁,返回 0; 若互斥锁已加锁,则函数直接返回失败,即 EBUSY。
|
1 2 3 4 5 6 7 8 9 10
| #include <pthread.h>
int pthread_mutex_unlock(pthread_mutex_t *mutex); 功能: 对指定的互斥锁解锁。 参数: mutex:互斥锁地址。 返回值: 成功:0 失败:非0错误码
|
3.死锁
必要条件
处理死锁思路
预防方法
协议1:
所有进程开始前,必须一次性地申请所需的所有资源,这样运行期间就不会再提出资源要求,破坏了请求条件,即使有一种资源不能满足需求,也不会给它分配正在空闲的资源,这样它就没有资源,就破坏了保持条件,从而预防死锁的发生。
协议2:
允许一个进程只获得初期的资源就开始运行,然后再把运行完的资源释放出来。然后再请求新的资源。
当一个已经保持了某种不可抢占资源的进程,提出新资源请求不能被满足时,它必须释放已经保持的所有资源,以后需要时再重新申请。
对系统中的所有资源类型进行线性排序,然后规定每个进程必须按序列号递增的顺序请求资源。假如进程请求到了一些序列号较高的资源,然后有请求一个序列较低的资源时,必须先释放相同和更高序号的资源后才能申请低序号的资源。多个同类资源必须一起请求。
12.读写锁
互斥锁排他性,只能有一个线程访问资源,而多个线程读资源并不会产生问题。
读写锁允许多个线程读,只能同时有一个线程写。
读写锁的特点如下:
1)如果有其它线程读数据,则允许其它线程执行读操作,但不允许写操作。
2)如果有其它线程写数据,则其它线程都不允许读、写操作。
读写锁分为读锁和写锁,规则如下:
1)如果某线程申请了读锁,其它线程可以再申请读锁,但不能申请写锁。
2)如果某线程申请了写锁,其它线程不能申请读锁,也不能申请写锁。
(允许多个读锁,只能有一个写锁,写锁优先级高于读锁)
1.函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| #include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr); 功能: 用来初始化 rwlock 所指向的读写锁。
参数: rwlock:指向要初始化的读写锁指针。 attr:读写锁的属性指针。如果 attr 为 NULL 则会使用默认的属性初始化读写锁,否则使用指定的 attr 初始化读写锁。
可以使用宏 PTHREAD_RWLOCK_INITIALIZER 静态初始化读写锁,比如: pthread_rwlock_t my_rwlock = PTHREAD_RWLOCK_INITIALIZER;
这种方法等价于使用 NULL 指定的 attr 参数调用 pthread_rwlock_init() 来完成动态初始化,不同之处在于PTHREAD_RWLOCK_INITIALIZER 宏不进行错误检查。 返回值: 成功:0,读写锁的状态将成为已初始化和已解锁。 失败:非 0 错误码。
|
1 2 3 4 5 6 7 8 9 10
| #include <pthread.h>
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock); 功能: 用于销毁一个读写锁,并释放所有相关联的资源(所谓的所有指的是由 pthread_rwlock_init() 自动申请的资源) 。 参数: rwlock:读写锁指针。 返回值: 成功:0 失败:非 0 错误码
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| #include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); 功能: 以阻塞方式在读写锁上获取读锁(读锁定)。 如果没有写者持有该锁,并且没有写者阻塞在该锁上,则调用线程会获取读锁。 如果调用线程未获取读锁,则它将阻塞直到它获取了该锁。一个线程可以在一个读写锁上多次执行读锁定。 线程可以成功调用 pthread_rwlock_rdlock() 函数 n 次,但是之后该线程必须调用 pthread_rwlock_unlock() 函数 n 次才能解除锁定。 参数: rwlock:读写锁指针。 返回值: 成功:0 失败:非 0 错误码
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock); 用于尝试以非阻塞的方式来在读写锁上获取读锁。 如果有任何的写者持有该锁或有写者阻塞在该读写锁上,则立即失败返回。
|
12.条件变量
与互斥锁不同,条件变量是用来等待而不是用来上锁的,条件变量本身不是锁!
条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。通常条件变量和互斥锁同时使用。
条件变量的两个动作:
- 条件不满, 阻塞线程
- 当条件满足, 通知阻塞的线程开始工作
条件变量的类型: pthread_cond_t。