1. 线程的基本概念
在学习了进程之后,大家都知道进程就是一个正在运行的程序的实例。那什么是线程呐?线程是一个进程内部的一个控制序列。所有的进程都至少有一个执行线程。
线程概念引入的原因
linux操作系统既然已经建立了进程的概念,那为什还要使用线程呐?在Linux系统下,创建一个进程必须分配给它独立的地址空间,建立数据表来维护它的代码段、堆栈段和数据段,这样很耗费资源。而同一进程中的线程们,他们使用相同的地址空间,共享大部分数据,创建线程的资源花费远小于创建一个进程。而且线程之间的切换远远比进程之间切换省事。比如后边要学习的服务器,多线程可以大大提高请求的处理效率。
进程与线程的联系
进程和线程是实现多任务并非的技术手段,做多任务的环境下,功能上来说并没有太大的差异。进程是资源分配的基本单位,而线程是调度的基本单元。操作系统中每一个执行的进程,都有它自己的地址空间,而同一进程中可以有多个线程,也就是多个执行流在同时执行。这里的同时,如果是单核处理器,则此时并不是真正意义上的同时,由于处理器运行速度很快,给每个执行流分配了时间片,在单核处理器中微观上还是顺序执行,而在多核处理器中,就是真正意义上的并行。由于同一进程的多个线程共享同一地址空间,因此线程之间有互相共享的资源,也有彼此独占的资源。
进程与线程的区别
进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在一般情况下不会对其他进程产生影响,而线程只是进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程崩溃掉就等于整个进程崩溃掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对某些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。
2.线程相关API
1.线程创建函数
头文件:
#include<pthread.h>
函数声明:
int pthread_create(pthread_t *thread,const pthread_attr_t *attr,void *(*start_routine)(void *),void *arg);
返回值:
成功则返回 0, 否则返回出错误码。
参数:
thread 保存线程描述符
attr 设置线程属性
start_routine 线程运行函数的起始地址(启动例程)
arg 运行函数的参数
说明:
编译时要加上 -lpthread 参数来调用静态链接库
2.线程退出函数
头文件:
#include<pthread.h>
函数声明:
void pthread_exit(void *ret_val);
参数:
ret_val 调用线程的返回值
说明:
用来终止调用线程并通过 ret_val参数向线程的回收者传递其推出信息。
3.等待线程结束
头文件:
#include<pthread.h>
函数声明:
int pthread_join(pthread_t thread,void **retval);
返回值:
成功则返回 0, 否则返回出错误码
参数:
thread 线程标识符
retval 用来存储被等待线程的返回值
说明:
以阻塞的方式等待 thread 指定的线程结束并回收资源。一个进程中的所有线程都可以调用pthread_join函数来回收其他线程,即等待其他
线程结束。当线程调用了pthread_join函数则将一直阻塞,直到指定的线程调用pthread_exit从启动例程中返回或者被取消。
example:
void *f(void *arg) {
printf("thread1 running\n");
int a = 111;
pthread_exit((void *)&a);
}
int main(int argc,char *argv[])
{
pthread_t pid1;
pthread_create(&pid1,NULL,f,NULL);
int t;
int *p = &t;
pthread_join(pid1,(void **)&p);
printf(" 线程结束的返回值为 :%d\n",t);
return 0;
}
线程在自己的栈区上分配了一个变量然后把指向这个变量的指针传给pthread_exit(), 当调用 pthread_join() 的线程试图获取该值时 , 随着线程的退出,所有局部变量被释放,这个值已经无效。
4.返回线程标识符
头文件:
#include<pthread.h>
函数声明:
pthread_t pthread_self(void);
返回值:
线程标识符
说明:
pthread_self() 返回的是相对于进程中各个线程之间的标识号,对于这个进程内是唯一的,而不同进程中,每个线程的 pthread_self() 可能
返回是一样的。要想得到内核中的真实线程 ID ,需调用 gettid(), 用户态的库并没有直接提供 gettid() 函数,需要执行类似的系统调用
syscall(__NR_gettid)
5.取消线程
头文件:
#include<pthread.h>
函数声明:
int pthread_cancel(pthread_t thread);
返回值:
成功则返回 0, 否则返回出错误码。
参数:
thread 线程标识符
说明:
异常终止线程,即取消线程。
3.线程同步问题
多个并发的线程协同完成一项任务时,由于数据交换需要而在线程执行次序上的约束关系,叫同步关系。并发线程为竞争同一个资源而间接发生的相互制约关系,叫互斥关系。
example:
多次运行上面的程序,结果和你开始想的一样吗?
为了处理线程同步问题,linux操作系统提供了互斥锁和条件变量。
互斥锁:
特点:
原子性:把一个互斥锁定义为一个原子操作,这意味着操作系统保证了如果一个线程锁定了互斥锁,则没有其他线程可以在同一时间成功锁定这个互斥量。
唯一性:如果一个线程锁定一个互斥量,在它接除锁定之前,没有其他线程可以锁定这个互斥量。
非繁忙等待:如果一个线程已经锁定了一个互斥锁,第二个线程又试图去锁定这个互斥锁,则第二个线程将被挂起(不占用 CPU 资源),直到第一个线程解锁,第二个线程则被唤醒并继续执行,同时锁定这个互斥量。
API:
1.初始化互斥锁
头文件
#include <pthread.h>
函数原型
int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutex_attr *attr);
返回值
成功返回 0 ,否则非零
参数
mutex: 待初始化的锁
attr: 新建互斥锁的属性
2.销毁互斥锁
头文件
#include<pthread.h>
函数原型
int pthread_mutex_destory(pthread_mutex_t *mutex);
返回值
成功返回 0 ,否则非 0
参数
mutex: 待销毁锁
说明:
释放锁所占用的资源,且要求锁处于开放状态,销毁处于锁定状态的锁时返回 EBUSY 错误
3.互斥锁操作函数
头文件
#include<pthread.h>
函数原型
// 以原子操做加锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
// 以原子操作解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
// 尝试加锁
int pthread_mutex_trylock(pthread_mutex_t *mutex);
返回值
成功返回 0 ,否则返回一个错误提示码
参数
mutex: 待操作的锁
原子操作:所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会切换到另一个线程。
example:
pthread_mutex_t mutex;
void foo() {
pthread_mutex_lock(&mutex);
// do something
pthread_mutex_unlock(&mutex);
}
void bar() {
pthread_mutex_lock(&mutex);
// do something
foo();
pthread_mutex_unlock(&mutex);
}
int main()
{
pthread_mutex_init(&mutex);
bar();
return 0;
}
foo 函数和 bar 函数都获取了同一个锁,而 bar 函数又会调用 foo函数。如果 mutex 锁是个非递归锁,则这个程序会立即死锁。
条件变量:
互斥锁为防止多个线程同时访问同一个共享量,而条件变量允许一个线程就某个共享变量(或其他共享资源)的状态变化通知其他线程,并让其他线程等待(阻塞于)这一通知。条件变量总是结合互斥锁一起使用,条件变量就共享变量的状态改变发出通知,而互斥锁则提供对该共享变量访问的互斥。
API:
1.初始化条件变量
头文件
#include<pthread.h>
函数原型
int pthread_cond_init(pthread_cond_t *cv,const pthread_condattr_t *cattr);
返回值
成功返回 0, 否则返回错误码
参数
cv: 需初始化的条件变量
cattr: 设置的条件变量属性
2.销毁条件变量
头文件
#include<pthread.h>
函数原型
int pthread_cond_destory(pthread_cond_t *cond);
返回值
成功返回 0, 否则返回错误码
参数
cond: 需销毁的条件变量
说明:
只有在没有线程在该条件变量上等待时,才可以注销条件变量,否则会返回 EBUSY
3.等待条件变量
头文件
#include<pthread.h>
函数原型
int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);
返回值
成功返回 0, 否则返回错误码
参数
cond: 等待的条件变量
mutex: 所使用的互斥锁
说明:
线程阻塞在等待条件变量的状态中
4.唤醒条件变量
头文件
#include<pthread.h>
函数原型
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
返回值
成功返回 0, 否则返回错误码
参数
cond: 要唤醒的条件变量
example:
pthread_cond_t cond;
void *f(void *arg) {
pthread_cond_signal(&cond);
pthread_exit(NULL);
}
int main()
{
pthread_t tid1;
pthread_mutex_init(&mutex,NULL);
pthread_cond_init(&cond,NULL);
pthread_create(&tid1,NULL,f,(void *)&cond);
pthread_cond_wait(&cond,&mutex);
return 0;
}
在调用 pthread_cond_wait 之前,若没有互斥锁的保护,则线程有可能达等待信号发生阶段前收到信号,结果自然是信号成功的被忽略掉,程序阻塞在等待信号中。
pthread_mutex_t mutex;
pthread_cond_t cond;
void *f(void *arg) {
pthread_mutex_lock(&mutex);
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
pthread_exit(NULL);
}
int main()
{
pthread_t tid1;
pthread_mutex_init(&mutex,NULL);
pthread_cond_init(&cond,NULL);
pthread_mutex_lock(&mutex);
pthread_create(&tid1,NULL,f,(void *)&cond);
pthread_cond_wait(&cond,&mutex);
return 0;
}
在 pthread_cond_wait(&cond,&mutex) 会执行以下操作
对 mutex 解锁
//do something
等待条件变量 cond 被唤醒
//do something
对 mutex 加锁
如果使用另外的线程调用 pthread_cond_signal() 后未解锁,则程序将阻塞在 pthread_cond_wait 中等待锁的释放。
pthread_mutex_t mutex;
pthread_cond_t cond;
void *f(void *arg) {
pthread_mutex_lock(&mutex);
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
pthread_exit(NULL);
}
int main()
{
pthread_t tid1;
pthread_mutex_init(&mutex,NULL);
pthread_cond_init(&cond,NULL);
pthread_mutex_lock(&mutex);
pthread_create(&tid1,NULL,f,(void *)&cond);
pthread_cond_wait(&cond,&mutex);
pthread_mutex_unlock(&mutex);
return 0;
}
( 1 )在调用 pthread_cond_wait() 前必须加锁,防止因为多线程竞争而忽略唤醒信号
( 2 )调用 pthread_cond_signal 之后必须要立即释放互斥锁,因为 pthread_cond_wait 的最后一步是要将指定的互斥量重新锁住,如果pthread_cond_signal 之后没有释放互斥锁 pthread_cond_wait 仍然要阻塞
4.线程池
为什么要使用线程池?
多线程在使用过程中会带来调度上的开销,当线程执行时间远大于线程创建的时间时,开销可以忽略不计。如果一个应用需要频繁的创建和销毁线程,而任务执行的时间又非常短,这样线程创建和销毁的带来的开销就不容忽视,这时候就应该使用线程池。