线程及其同步与互斥

线程概述

为什么要有线程?

在多进程的处理机中,各个进程轮流上处理机,实现了并发。但是,从一个进程切换到另一个进程的时候,需要花费比较多的时间,处理机需要切换上下文(保存各个寄存器的值,将一些修改的文件重新写回到磁盘上,要切换地址空间等等)。由此,出现了线程。线程是由进程引申出来的。一个进程可以产生很多线程。线程共享进程的资源(包括内核、堆等资源),因此,如果同一进程下的线程切换时,有一些资源就不必切换,因此能提高处理机效率。因此线程也叫轻量级进程(LWP)。
注意:在Linux环境下,一个进程只含有一个线程。

进程与线程的比较

根本区别:进程是操作系统资源分配的基本单位,线程是CPU调度的基本单位
地址空间:各个进程拥有自己独立的地址空间,进程间不能直接访问地址空间。同一进程下的线程共享进程的地址空间。
资源:线程除必不可少的一点儿资源确保自身能够独立运行(寄存器,栈等),几乎不拥有资源,线每个线程有自己的独立的栈,同一进程下的各个线程共享堆。
系统开销:进程切换比线程切换所花费的开销要大。
通信:线程间通信要比进程间简单。

线程间共享与不共享的资源:
  • 共享的资源
  1. 进程 ID 、父进程 ID、 进程组 ID 、会话 ID、用户 ID 和 用户组 ID
  2. 文件描述符表
  3. 信号处置
  4. 文件系统的相关信息:文件权限掩码(umask)、当前工作目录
  5. 虚拟地址空间(除栈、.text)
  • ** 不共享的资源**
  1. 线程 ID
  2. 信号掩码
  3. 线程特有数据
  4. error 变量
  5. 实时调度策略和优先级
  6. 栈,本地变量和函数的调用链接信息

相关函数

  • 创建线程 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个必要条件

  1. 破坏互斥条件。一般来说资源的类型是由资源本身决定的,我们很难讲互斥资源变为共享资源。
  2. 破坏请求和保持条件。采用静态分配的方式。在进程开始前一次性将所有所需的资源分配给他。但这样会降低系统的资源利用率。
  3. 破坏不剥夺。如果进程申请不到所需的资源,那么就释放它已经申请到的资源。同样这样也会降低系统的资源利用率。
  4. 破坏循环等待。给系统的所有资源编号,规定进程请求所需资源的顺序必须按照资源的编号依次进行。
死锁避免

银行家算法

死锁的检测和恢复
  • 可以允许系统进入死锁状态,然后检测它,并加以恢复
  • 可以忽视这个问题,认为死锁不可能在系统内发生
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值