线程概述
为什么要有线程?
在多进程的处理机中,各个进程轮流上处理机,实现了并发。但是,从一个进程切换到另一个进程的时候,需要花费比较多的时间,处理机需要切换上下文(保存各个寄存器的值,将一些修改的文件重新写回到磁盘上,要切换地址空间等等)。由此,出现了线程。线程是由进程引申出来的。一个进程可以产生很多线程。线程共享进程的资源(包括内核、堆等资源),因此,如果同一进程下的线程切换时,有一些资源就不必切换,因此能提高处理机效率。因此线程也叫轻量级进程(LWP)。
注意:在Linux环境下,一个进程只含有一个线程。
进程与线程的比较
根本区别:进程是操作系统资源分配的基本单位,线程是CPU调度的基本单位
地址空间:各个进程拥有自己独立的地址空间,进程间不能直接访问地址空间。同一进程下的线程共享进程的地址空间。
资源:线程除必不可少的一点儿资源确保自身能够独立运行(寄存器,栈等),几乎不拥有资源,线每个线程有自己的独立的栈,同一进程下的各个线程共享堆。
系统开销:进程切换比线程切换所花费的开销要大。
通信:线程间通信要比进程间简单。
线程间共享与不共享的资源:
- 共享的资源
- 进程 ID 、父进程 ID、 进程组 ID 、会话 ID、用户 ID 和 用户组 ID
- 文件描述符表
- 信号处置
- 文件系统的相关信息:文件权限掩码(umask)、当前工作目录
- 虚拟地址空间(除栈、.text)
- ** 不共享的资源**
- 线程 ID
- 信号掩码
- 线程特有数据
- error 变量
- 实时调度策略和优先级
- 栈,本地变量和函数的调用链接信息
相关函数
- 创建线程 pthread_create()
- 终止线程 pthread_exit()
- 获取当前线程的id pthread_self()
- 比较两个线程ID是否相等 pthread_equal()
- 连接终止线程 pthread_join()
- 分离线程 pthread_detach()
- 取消线程 pthread_cancel
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
- 功能:创建一个子线程
- 参数:
- thread:传出参数,线程创建成功后,子线程的线程ID被写到该变量中。
- attr : 设置线程的属性,一般使用默认值,NULL
- start_routine : 函数指针,这个函数是子线程需要处理的逻辑代码
- arg : 给第三个参数使用,传参
- 返回值:
成功:0
失败:返回错误号。这个错误号和之前errno不太一样。
获取错误号的信息: char * strerror(int errnum);
void pthread_exit(void *retval);
功能:终止一个线程,在哪个线程中调用,就表示终止哪个线程
参数:
retval:需要传递一个指针,作为一个返回值,可以在pthread_join()中获取到。
pthread_t pthread_self(void);
功能:获取当前的线程的线程ID
int pthread_equal(pthread_t t1, pthread_t t2);
功能:比较两个线程ID是否相等
不同的操作系统,pthread_t类型的实现不一样,有的是无符号的长整型,有的是使用结构体去实现的。
int pthread_join(pthread_t thread, void **retval);
- 功能:和一个已经终止的线程进行连接
回收子线程的资源
这个函数是阻塞函数,调用一次只能回收一个子线程
一般在主线程中使用
- 参数:
- thread:需要回收的子线程的ID
- retval: 接收子线程退出时的返回值
- 返回值:
0 : 成功
非0 : 失败,返回的错误号
int pthread_detach(pthread_t thread);
- 功能:分离一个线程。被分离的线程在终止的时候,会自动释放资源返回给系统。
1.不能多次分离,会产生不可预料的行为。
2.不能去连接一个已经分离的线程,会报错。
- 参数:需要分离的线程的ID
- 返回值:
成功:0
失败:返回错误号
int pthread_cancel(pthread_t thread);
- 功能:取消线程(让线程终止)
取消某个线程,可以终止某个线程的运行,
但是并不是立马终止,而是当子线程执行到一个取消点,线程才会终止。
取消点:系统规定好的一些系统调用,我们可以粗略的理解为从用户区到内核区的切换,这个位置称之为取消点。
线程属性
一般地,Linux下的线程有:绑定属性、分离属性、调度属性、堆栈大小属性和满占警戒区大小属性等。通过修改线程属性可以提高线程的效率。
相关函数
- 初始化线程属性 pthread_attr_init()
- 释放线程属性的资源 pthread_attr_destroy()
- 获取线程分离的状态属性 pthread_attr_getdetachstate()
- 设置线程分离的状态属性 pthread_attr_setdetachstate()
线程同步与互斥
同步:由于系统处于多线程状态,每个线程都是并发执行的,因此每个线程的执行顺序是随机的,这就要求在线程随机执行的条件下,要按照我们要求的顺序,最终要能得出一个正确结果(或者是我们期待的结果),这就是线程的同步问题。
如上图,有两个进程, 如果按照123456的顺序来执行的话,那么最终a=1, b = 1, c = 2。如果按照142536的顺序来执行的话,那么a=0,b=0,c= 0。所以两个进程要按一定的顺序推进,才能保证结果唯一。这就是进程的同步。
互斥:在多线程的环境下,每个线程都有可能上处理机,两个不同的线程有可能会访问同一个资源。比如说打印机。如果只有一台打印机,而有两个人都想使用这台打印机打印资料,就必须有一个人要等待。因此,对于独占资源(也叫临界资源),我们必须保证其是互斥的访问(在每个时刻当且仅当只有一个进程在访问)。这就是进程的互斥。
生产者与消费者问题
生产者:生产者进程负责生产商品,并将他放到仓库里。
消费者:消费者进程负责消费商品。
我们分别创建三个生产者进程生产商品,三个消费者进程消费商品。
//创建3个生产者线程,3个消费者进程
pthread_t producer[3];
pthread_t consumer[3];
for(int i = 0; i < 3; ++i){
pthread_create(&producer[i],NULL,Produce,NULL);
pthread_create(&consumer[i],NULL,Consume,NULL);
}
生产者进程负责生产商品,并将生产好的商品挂到链表上(采用头插法)
//生产者进程
void *Produce(void* args){
//不断生产产品
while(1){
struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));//生产商品
newNode->num = rand() % 100; //商品编号
newNode->next = head;
head = newNode;
printf("Prodecer tid: %ld produce a thing %d\n", pthread_self(), newNode->num);
usleep(100); //延迟达到效果
}
return NULL;
}
消费者进程负责消费商品,每次从链表头拿取一个节点。
//消费者进程
void *Consume(void *args){
while(1){
if(head != NULL){
//链表不为空时我们再拿(即有商品时才可以拿)
struct Node* node = head;
head = head->next;
printf("Comsumer tid: %ld comsume a thing %d\n", pthread_self(), node->num);
free(node);
node = NULL;
usleep(100);
}
}
return NULL;
}
问题一:存在内存多次释放的问题
原因:无论是消费者还是生产者,对于仓库(链表)的访问都应该是互斥的。当任何一个进程访问链表时,其他进程都不能访问。内存多次释放是由于在消费者代码中,有可能一个进程获取了链表的头节点,但还没来得及删除头节点,另一个进程也获取了该头节点,因此,到删除的时候就会产生二次释放的问题。
解决方法:对于链表的访问,我们采用 互斥锁(互斥量) 进行互斥访问。
互斥量相关函数
互斥量的类型 pthread_mutex_t
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
- 初始化互斥量
- 参数 :
- mutex : 需要初始化的互斥量变量
- attr : 互斥量相关的属性,NULL
- restrict : C语言的修饰符,被修饰的指针,不能由另外的一个指针进行操作。
pthread_mutex_t *restrict mutex = xxx;
pthread_mutex_t * mutex1 = mutex;
int pthread_mutex_destroy(pthread_mutex_t *mutex);
- 释放互斥量的资源
int pthread_mutex_lock(pthread_mutex_t *mutex);
- 加锁,阻塞的,如果有一个线程加锁了,那么其他的线程只能阻塞等待(可以理解为while(1)循环等待直到解锁)
int pthread_mutex_trylock(pthread_mutex_t *mutex);
- 尝试加锁,如果加锁失败,不会阻塞,会直接返回。
int pthread_mutex_unlock(pthread_mutex_t *mutex);
- 解锁
因此生产者和消费者的代码就应该如下(省略了初始化)
//生产者进程
void *Produce(void* args){
//不断生产产品
while(1){
struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
newNode->num = rand() % 100;
//对链表上锁
pthread_mutex_lock(&listMutex);
newNode->next = head;
head = newNode;
//对链表解锁
pthread_mutex_unlock(&listMutex);
printf("Prodecer tid: %ld produce a thing %d\n", pthread_self(), newNode->num);
usleep(100); //延迟达到效果
}
return NULL;
}
//消费者进程
void *Consume(void *args){
while(1){
if(head != NULL){
//对链表上锁
pthread_mutex_lock(&listMutex);
struct Node* node = head;
head = head->next;
//对链表解锁
pthread_mutex_unlock(&listMutex);
printf("Comsumer tid: %ld comsume a thing %d\n", pthread_self(), node->num);
free(node);
node = NULL;
usleep(100);
}
}
return NULL;
}
问题二:
实际上仓库的容量是有限的,即生产者无法无限制地往仓库里放商品,而消费者当仓库里没有商品时也就不能往外拿商品,即消费者必须在生产者生产之后才能消费商品,这表明了两个线程之间的执行顺序,属于线程的同步问题。
解决方法:信号量或者条件变量可以解决线程同步问题
信号量记录了可以使用的资源数量,例如当一个信号量sem(代表打印机)为5时,表明有5台打印机可以使用,当有线程使用一台时,信号量就会减一,当信号量小于等于0时,表示无资源可用,那么线程就应该阻塞等待,直到有资源可用。
首先,条件变量不是锁。条件变量是用来等待资源的。条件变量通常是和互斥锁一起使用的。当条件不满足时,条件变量告诉线程要停在这,不允许往下走了;当条件满足时,条件变量就会告诉那些想要这个资源的线程(一个或多个),现在有这个资源了,你们可以拿走这个资源继续访问了。但由于资源是互斥的,因此一个线程要拿走资源,就必须加锁。这就是为什么条件变量是和互斥锁一起用的。
信号量相关函数
信号量的类型 sem_t
int sem_init(sem_t *sem, int pshared, unsigned int value);
- 初始化信号量
- 参数:
- sem : 信号量变量的地址
- pshared : 0 用在线程间 ,非0 用在进程间
- value : 信号量中的值
int sem_destroy(sem_t *sem);
- 释放资源
int sem_wait(sem_t *sem);
- 对信号量加锁,调用一次对信号量的值-1,如果值为0,就阻塞
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
int sem_post(sem_t *sem);
- 对信号量解锁,调用一次对信号量的值+1
int sem_getvalue(sem_t *sem, int *sval);
条件变量相关函数
条件变量的类型 pthread_cond_t
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
-条件变量初始化
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
- 等待,调用了该函数,线程会阻塞。
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
- 等待多长时间,调用了这个函数,线程会阻塞,直到指定的时间结束。
int pthread_cond_signal(pthread_cond_t *cond);
- 唤醒一个或者多个等待的线程
int pthread_cond_broadcast(pthread_cond_t *cond);
- 唤醒所有的等待的线程
对于生产者来说,需要的资源是仓库中的空余位置(记为empty),生产者每生产一件商品,就消耗一个空位置(empty–),增加一个商品数量(full++)
对于消费者来说,需要的资源是仓库中的商品数量(记为full),消费者每消费一件商品,就消耗一件商品(full–),增加一个空位(empty++)。
因此,生产者与消费者代码如下。
//生产者线程
void *Produce(void* args){
//不断生产产品
while(1){
sem_wait(&empty); //申请empty(空位)
struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
newNode->num = rand() % 100;
//对链表上锁
pthread_mutex_lock(&listMutex);
newNode->next = head;
head = newNode;
//对链表解锁
pthread_mutex_unlock(&listMutex);
sem_post(&full);//发送信号有一个位置满了
printf("Prodecer tid: %ld produce a thing %d\n", pthread_self(), newNode->num);
usleep(100); //延迟达到效果
}
return NULL;
}
//消费者线程
void *Consume(void *args){
while(1){
sem_wait(&full); //申请满的位置
//对链表上锁
pthread_mutex_lock(&listMutex);
struct Node* node = head;
head = head->next;
//对链表解锁
pthread_mutex_unlock(&listMutex);
printf("Comsumer tid: %ld comsume a thing %d\n", pthread_self(), node->num);
free(node);
node = NULL;
sem_post(&empty); //发送信号空位+1
usleep(100);
}
return NULL;
}
死锁
什么是死锁
在多道程序环境中,多个进程可以竞争有限数量的资源。当一个进程申请资源时,如果这时没有可用资源,那么这个进程进入等待状态。有时,如果所申请的资源被其他等待进程占有,那么该等待进程有可能再也无法改变状态。这种情况称为死锁。
死锁产生的必要条件
- 互斥条件:一个资源每次只能被一个进程使用
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系
解决方法
死锁预防
破坏死锁的4个必要条件
- 破坏互斥条件。一般来说资源的类型是由资源本身决定的,我们很难讲互斥资源变为共享资源。
- 破坏请求和保持条件。采用静态分配的方式。在进程开始前一次性将所有所需的资源分配给他。但这样会降低系统的资源利用率。
- 破坏不剥夺。如果进程申请不到所需的资源,那么就释放它已经申请到的资源。同样这样也会降低系统的资源利用率。
- 破坏循环等待。给系统的所有资源编号,规定进程请求所需资源的顺序必须按照资源的编号依次进行。
死锁避免
银行家算法
死锁的检测和恢复
- 可以允许系统进入死锁状态,然后检测它,并加以恢复
- 可以忽视这个问题,认为死锁不可能在系统内发生