在写线程总结前,鸭鸭先来说说从上一章我们了解到的,进程和线程的异同点~
- 相同点:
无论是进程还是线程,都是用来实现多任务并发的技术手段.二者都可以独立调度.在多任务程序中,子进程(子线程)的调度一般与父进程(父线程)平等竞争。 - 不同点:
- 进程是操作系统资源分配的基本单位,线程是调度的基本单位.进程也可以被调度,但是线程是更小的可以调度的单位
- 进程在执行过程中拥有独立的内存单元,线程基本上不拥有系统资源,它与同属一个进程的其他线程共享进程拥有的资源
- 进程的个体间是完全独立的,而线程间是彼此依存的.多进程环境中,任何一个进程的终止,不会影响到其他进程;而多线程环境中,父线程终止,全部子线程被迫终止(没有了资源)
那么这章,在进程的基础上学习线程~
先来看看线程和进程的关系:
线程是计算机中独立运行的最小单位,运行时占用很少的系统资源.由于每个线程占用的CPU时间是由系统分配的,因此可以把线程看成操作系统分配CPU时间的基本单位.
在用户看来,多个线程是同时执行的
从操作系统调度上看,各个线程是交替执行的
系统不停地在各个线程之间切换,每个线程只有在系统分配给它的时间段内才能取得CPU的控制权,执行线程中的代码
PS.只是针对单CPU单核,在多CPU多核的主机上,多个线程是可以同时运行的
在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义
那么多线程相对于多进程的优点如下:
- 在多进程情况下,每个进程都有自己独立的地址空间,而在多线程情况下,同一进程内的线程共享进程的地址空间.因此创建一个新的进程就要花费时间来为其分配系统资源,而创建一个新的线程花费的时间则要少的多
- 在系统调度方面,由于进程地址空间独立而线程共享地址空间,线程间的切换速度要远远快过进程间的切换速度
- 在通信机制方面,进程间的数据空间相互独立,彼此通信要以专门的通信方式进行,通信时必须经过操作系统.而同一进程内的多个线程共享数据空间,一个线程的数据可以直接提供给其他线程使用,而不必经过操作系统.因此,线程间的通信更加方便和省时
PS.这三条概括起来就是节约资源和时间 - 可以提高应用程序的响应速度
- 可以提高多处理器效率
- 可以改善程序的结构
虽然线程在进程内部共享地址空间,打开的文件描述符等资源,但是它也有其私有的数据信息:
- 线程号(thread ID):唯一
- 寄存器(包括程序计数器和堆栈指针)
- 堆栈
- 信号掩码
- 优先级
- 线程私有的存储空间
如何创建线程
单线程的程序是按照一定的顺序执行的,如果在主线程里创建线程,程序此时产生分支,变成两个程序执行;其实这个多进程不一样,子进程是通过拷贝父进程的地址空间来实现的,而各线程共享程序代码,一段代码可以被多个线程执行
#include<pthread.h>
int pthread_creat(pthread_t *thread,pthread_attr_t *attr,void *(*start_routine)(void *),void *arg);
emm..鸭鸭看到这些参数,有点懵= =我们一个一个来看
- thread:指针,线程创建成功返回创建的线程ID
- attr:指向pthread_attr_t结构体的指针,指定线程的属性,NULL表示使用默认属性
- start_routine:函数指针,指向线程创建后要调用的函数,这个被线程调用的函数也称为线程函数
- arg:指针,指向传递给线程函数的参数
线程创建成功函数返回0,若不为0说明创建线程失败.
错误代码:
EAGAIN:线程数目过多,系统限制创建新的线程
EINVAL:线程属性值非法
线程创建成功后,新创建的线程开始运行第三个参数所指向的函数,原来的线程继续运行
创建线程的其他系统函数:
pthread_t pthread_self(void) //获取本线程的线程ID
int pthread_equal(pthread_t thread1,pthread_t thread2) //判断两个线程ID是否指向同一线程
int pthread_once(pthread_once_t *once_control,void (*init_routine)(void)) //用来保证init_routine线程函数在进程中只执行一次
现在我们再来详细看看pthread_attr_t结构体~~
typedef struct
{
int detachstate;
int schedpolicy;
struct sched_param schedparam;
int inheritsched;
int scope;
size_t guardsize;
int stackaddr_set;
void* stackaddr;
size_t stacksize;
}pthread_attr_t;
这个鸭鸭并没有深究...
线程终止
- return 从线程函数返回
- 调用pthread_exit()使线程退出
#include<pthread.h>
void pthread_exit(void *retval);
在主线程中,如果从main函数返回或是调用了exit函数退出主线程,则整个进程将终止,进程中的所有线程也终止
如果主线程调用pthread_exit函数,则仅仅是主线程消亡,进程不会结束,进程内的其他线程也不会终止,直到所有线程结束,进程才终止
- 线程终止有一个很重要的问题,资源释放,特别是临界资源
什么是临界资源?
在一段时间内只能被一个线程所持有
当线程要使用临界资源时需提出请求,如果该资源未被使用则申请成功,否则等待.临界资源使用完以后要释放以便其他线程可以使用
pthread_cleanup_push(),pthread_cleanup_pop()用于自动释放资源.
从pthread_cleanup_push()的调用点到pthread_cleanup_pop()之间的程序段中的终止动作都将执行pthread_cleanup_push()所指向的清理函数(如pthread_exit())
这两个函数是以宏形式提供的:
#include<pthread.h>
#define pthread_cleanup_push(routine,arg)
{
struct _pthread_cleanup_buffer buffer;
_pthread_cleanup_push(&buffer,(routine),(srg));
#define pthread_cleanup_pop _pthread_cleanup_pop(&buffer,(exeute));
}
- 程序终止还有一个要注意的问题是线程间的同步问题
线程中各个线程的运行是相互独立的,线程终止并不会相互通知,也不会影响其他线程,终止的线程所占用的资源不会随着线程的终止而归还给系统,而是仍为线程所在的进程持有.
线程也有类似进程wait()系统调用来等待其他进程结束
#include<pthread.h>
void pthread_exit(void* retvai);
int pthread_join(pthread_t th,void* thread_return);
int pthread_detach(pthread_t th);
什么是私有数据(TSD)???
一键多值,通过键值访问不同的数据
#include<pthread.h>
int pthread_key_create(pthread_ket_t *key,void(*destr_function)(void *)); //创建一个键
int pthread_setspecific(pthread_key_t key,const void * pointer)); //为一个键设置线程私有数据
void* pthread_getspecific(pthread_key_t key); //从一个键读取线程私有数据
int pthread_key_delete(pthread_key_t key); //删除一个键
- pthread_key_create:他的第一个参数key为指向键值的指针,第二个参数为一个函数指针,如果指针不为空,则在线程退出时将以key所关联的数据为参数调用destr_function(),释放分配的缓冲区.
从TSD池中分配一项,将其值赋给key供以后访问使用.key一旦被创建,所有线程都可以访问它,但各线程可根据自己的需要往key中填入不同的值,即提供了一个同名而不同值的全局变量,一键多值
一键多值靠的是一个关键数据结构数组,即TSD池
static struct pthread_key_struct pthread_keys[PTHREAD_KEYS_MAX]={(0,NULL)};
pthread_setspecific:该函数将pointer的值(不是内容)与key相关联.用pthread_setspecific为一个键指定新的线程数据时,线程必须先释放原有的线程数据用以回收空间
pthread_getspecific:通过该函数得到与key相关联的数据
pthread_key_delete:该函数用来删除一个键,删除后,键所占用的内存被释放,但是与该键相关联的线程数据所占用的内存不会被释放.因此,线程数据的释放必须在释放键之前完成
怎样处理线程同步问题?
互斥锁(通过锁机制来实现线程间的同步)
在同一时刻只允许一个线程执行一个关键部分的代码
函数(pthread.h) | 功能 |
---|---|
pthread_mutex_init | 初始化一个互斥锁 |
pthread_mutex_destroy | 注销一个互斥锁 |
pthread_mutex_lock | 加锁,如果不成功,阻塞等待 |
pthread_mutex_unlock | 解锁 |
pthread_mutex_trylock | 测试加锁,如果不成功立刻返回,错误码为EBUSY |
1.使用互斥锁前必须先进行初始化
- 静态赋值法,将宏结构常量PTHREAD_MUTEX_INITIALIZER赋给互斥锁
pthread_mutex_t = PTHREAD_MUTEX_INITIALIZER
- 通过pthread_mutex_init函数初始化互斥锁
int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutexattr_t *mutexattr);
2.初始化以后就可以给互斥锁加锁了
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
加锁时,不论哪种类型的锁,都不可能被两个不同的线程同时得到,其中一个必须等待解锁,在同一进程中的线程,如果加锁后没有解锁,则其他线程将无法再获得该锁
3.接下来就是解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
使用该函数必须满足两个条件,一是互斥锁必须处于加锁状态,二是调用本函数的线程必须是给互斥锁加锁的线程;解锁后如果有其他线程在等待互斥锁,等待队列中的第一个线程获得互斥锁
4.清除互斥锁
int pthread_mutex_destory(pthread_mutex_t *mutex);
清除锁要求处于开放状态,若锁处于锁定状态,函数返回EBUSY;成功执行返回0
条件变量(利用线程间共享的全局变量进行同步的一种机制)
两个动作:
一个等待使用资源的线程等待"条件变量被设置为真"
另一个线程在使用完资源后"设置条件为真"
函数(pthread.h) | 功能 |
---|---|
pthread_cond_init | 初始化条件变量 |
pthread_cond_wait | 基于条件变量阻塞,无条件等待 |
pthread_cond_timedwait | 阻塞直到指定时间发生,计时等待 |
pthread_cond_signal | 解除特定线程的阻塞,存在多个等待线程时按入队顺序激活其中一个 |
pthread_cond_broadcast | 解除所有线程的阻塞 |
pthread_cond_destory | 消除条件变量 |
1.条件变量的初始化
- 静态赋值法,将宏结构常量PTHREAD_COND_INITIALIZER赋给互斥锁
pthread_mutex_t = PTHREAD_MUTEX_INITIALIZER
- 通过pthread_cond_init函数初始化
int pthread_cond_init(pthread_cond_t *cond,pthread_condattr_t *cond_attr);
cond_attr通常是NULL
2.等待条件成立
int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);
int pthread_cond_timewait(pthread_cond_t *cond,pthread_mutex_t *mutex,const struct timespec *abstime);
pthread_cond_wait函数释放由mutex指向的互斥锁,同时使当前线程关于cond指向的条件变量阻塞,直到条件被信号唤醒
条件表达式在互斥锁的保护下求值,如果条件表达式为假,那么线程基于条件变量阻塞
当一个线程改变条件变量的值时,条件变量获得一个信号,使得等待条件变量的线程退出阻塞状态
pthread_cond_timewait不同的地方在于将阻塞直到条件变量获得信号或者经过由abstime指定的时间,也就是说,在给定时刻前没有满足,则返回ETIMEOUT,结束等待
3.激活等待条件成立的线程
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
4.清除条件变量
int pthread_cond_destory(pthread_cond_t *cond);
只有在没有线程等待该条件变量的时候才能清除这个条件变量,否则返回EBUSY
异步信号(利用线程间共享的全局变量进行同步的一种机制)
Linux操作系统中,进程在内核中实现,线程在内核外实现
Linux线程本质上是轻量级的进程
线程同进程一样也可以接受和处理信号,信号也是一种线程间同步的手段
信号与任何线程都是异步的,也就是说信号到达线程的时间是不定的
如果有多个线程可以接收异步信号,则只有一个被选中
int pthread_kill(pthread_t threadid,int signo);
用来向特定的线程发送信号signo
int pthread_sigmask(int how,const sigset_t *newmask,sigset_t *oldmask);
用来设置线程的信号屏蔽码,但对不允许屏蔽的Cancel信号和不允许响应的Restart信号进行了保护
int sigwait(const sigset_t *set,int *sig);
用来阻塞线程,等待set中指定的信号之一到达,并将达到的信号存入*sig中
出错处理
记住两个函数,灵活运用,可以快速发现问题所在
根据错误码获取一个描述错误信息的字符串:
#include<string.h>
char* strerror(int errnum);
根据errno打印对应的错误提示信息:
#include<stdio.h>
void perror(const char *message);
错误码详见另一篇博客~