牛客C++笔记——linux多线程开发

本文详细介绍了Linux环境下多线程开发的基础概念和技术要点,包括线程与进程的区别、线程的操作函数、线程同步机制等内容,并通过具体示例阐述了互斥锁、条件变量及信号量的应用。

linux多线程开发

线程概述

在这里插入图片描述

  1. 一个进程可包含多个线程,多个线程之间共享全局内存区
  2. 进程是CPU分配资源的最小单位,线程是操作系统调度执行的最小单位
  3. 使用ps -LF pid就可以查询进程下面的线程

线程和进程的区别

在这里插入图片描述

  1. 线程之间,它们都是共享虚拟地址空间的,只是各自又各自的代码区域,各自又各自的栈空间区域,
    在这里插入图片描述

线程操作函数

在这里插入图片描述

  1. pthread_self()获取当前线程的ID

pthread_create()——创建新线程

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

pthread_exit()——终止线程

在这里插入图片描述

  1. 程序的return 0 或者exit(0) 都是退出了进程,进程都推了,那里面的线程也被销毁了,
  2. 使用pthread_exit() 只是退出了线程,其他线程不受影响
    在这里插入图片描述

pthread_equal()——比较线程ID是否相等

在这里插入图片描述

pthread_jion()——和一个终止的线程进行连接

  1. 用于回收线程的资源
  2. 任何一个函数都可以调用这个去回收别人
  3. 是阻塞的,调用一次只能回收一个
    在这里插入图片描述
  4. 由于每个线程都区分着堆空间,就是局部变量,所以线程返回这里不能是局部变量,下面这样就是错的
    在这里插入图片描述
    得用全局变量返回
    在这里插入图片描述
  5. 主线程中
    在这里插入图片描述

pthread_detach()——分离一个线程

  1. 分离的线程直接就释放资源了,不需要其他线程来回收
  2. 可以直接在主线程中去释放子线程
    在这里插入图片描述

pthread_cancel()——取消一个线程

  1. 就是让线程终止,要终止的话需要一定条件,需要看
  2. 调用之后不是立刻就终止,执行到取消点的时候,才会终止
    在这里插入图片描述

线程属性操作函数

在这里插入图片描述

  1. pthread_attr_ 后面其实有很多函数,可以通过man pthread_attr_ 加tab去查看,这里展示俩
    在这里插入图片描述
    在这里插入图片描述

线程同步

在这里插入图片描述

  1. 临界区可以这样理解,比如1-5行代码,三个线程abc都会执行,我们需要让1-5行代码为原子操作,就是说a在执行的时候,是一口气执行完的,不能说a执行到第2行时候,c把cpu抢过去执行
  2. 像下面的就是不安全的,没有同步
/*
    使用多线程实现买票的案例。
    有3个窗口,一共是100张票。
*/
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
// 全局变量,所有的线程都共享这一份资源。
int tickets = 100;
void * sellticket(void * arg) {
    // 卖票
    while(tickets > 0) {
        usleep(6000);
        printf("%ld 正在卖第 %d 张门票\n", pthread_self(), tickets);
        tickets--;
    }
    return NULL;
}
int main() {
    // 创建3个子线程
    pthread_t tid1, tid2, tid3;
    pthread_create(&tid1, NULL, sellticket, NULL);
    pthread_create(&tid2, NULL, sellticket, NULL);
    pthread_create(&tid3, NULL, sellticket, NULL);

    // 回收子线程的资源,阻塞
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    pthread_join(tid3, NULL);

    // 设置线程分离。
    // pthread_detach(tid1);
    // pthread_detach(tid2);
    // pthread_detach(tid3);
    pthread_exit(NULL); // 退出主线程
    return 0;
}

互斥锁

在这里插入图片描述

  1. 这里用一个例子来理解,就是共用厕所,一个人在上厕所的时候,他就把门锁起来,其他人进不来,只能等着,他用完了,再解锁,其他人再来用
  2. a锁了厕所时,门锁只能由他来开
  3. 在等待锁释放的时候,会睡眠在那里
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    对上面存在问题的代码的改进:
  4. 由于多个线程都需要用到这个锁,所以他需要时全局的
    在这里插入图片描述
  5. 可以在主线程中初始化互斥锁
    在这里插入图片描述
  6. 进程结束前需要释放掉这个锁
    在这里插入图片描述
  7. 给临界区用锁保护起来
    在这里插入图片描述

死锁

在这里插入图片描述

读写锁

在这里插入图片描述

  1. 这样就可以几个线程同时一起读一个全局,效率高于互斥锁
  2. 应用于读取比写数据多的情景下

读写锁操作函数

  1. 用法和互斥锁一样的
    在这里插入图片描述
    在这里插入图片描述

C++11里面引入的管理锁的新类

这里就用到了RALL机制,让锁具有生命周期,锁他的作用域结束的时候,自动释放锁,避免出现如果程序异常,导致没有 unlock;

  1. lock_guard
std::mutex mut;
{
  std::lock_guard<std::mutex> lockGuard(mut);  // lock in lock_guard 构造函数
  sharedVariable++;
}  // unlock in lock_guard 析构函数

上面同样的功能,如果不使用RALL的话

mutex mut;
mut.lock();
sharedVariable++;
mut.unlock();
  1. unique_lock
    类 unique_lock 也是通用互斥包装器,也有lock_guard一样的RAII机制,但它有更多功能,允许延迟锁定、锁定的有时限尝试、递归锁定、所有权转移和与条件变量一同使用。它可移动,但不可复制。它有类似std::mutex一样的接口,更加灵活方便.
std::mutex mut;
// exsample_1{
  std::unique_lock<std::mutex> unilock(mut, std::defer_lock);  // 只创建 unique_lock 对象
  unilock.lock();  // 这里需要手动lock
  sharedVariable++;
}  // 如果里面没有调unlock,这里就unlock

// exsampl_2{
	std::unique_lock<std::mutex> unilock(mut, std::adopt_lock);  // 只创建 unique_lock 对象,已经调用了lock
	sharedVariable++;
	unilock.unlock();  // 这里可以直接解锁,也可以析构解锁
}

  1. 二者都是自释放锁;lock_guard 在时间和空间上都比unique_lock要快;lock_guard 功能单一,只能用作自释放锁;unique_lock具备lock_guard的所有能力,同时提供更多的能力,比如锁的成员函数都会被封装后导出,同时不会引入double lock和 double unlock;

  2. 那么为什么有时候需要unlock()?
    因为lock()锁住的代码段越少,执行越快,整个程序运行效率越高。
    锁头锁住的代码的多少称为锁的粒度,粒度一般用粗细来描述。
    锁住的代码少,这个粒度叫细,执行效率高。
    锁住的代码多,粒度叫粗,执行效率就低。
    要学会尽量选择合适粒度的代码进行保护,力度太细,可能漏掉共享数据的保护,粒度太粗,影响效率。
    选择合适的粒度,是高级程序员的能力和实力的体现。

  3. 如果确定使用unique_lock了,就不要再直接使用 mutex 的 lock 和 unlock ,可以直接使用 unique_lock 的 lock 和 unlock,混合使用会导致程序异常,原因是unique_lock 内部会维护一个标识用来记录自己管理的 锁 当前处于何种状态,如果直接使用 mutex的成员函数,unique_lock无法更新自己的状态,从而导致 double lock 和 double unlock(因为unique_lock一定会在析构的时候unlock),这两种情况都会导致崩溃。

条件变量

在这里插入图片描述

  1. 条件变量就是提供一种在线程之间协调工作的机制,不能用来解决线程数据安全的问题,要和锁配合使用
  2. 比如一个线程使用了pthread_cond_wait(),那他就会阻塞在这,要等到有其他线程通过pthrea_cond_signal()给他发了一个信号之后,才会唤醒他继续往下执行。
  3. 代码示例:
/*
条件变量的类型 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);
    - 唤醒所有的等待的线程
*/
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>

// 创建一个互斥量
pthread_mutex_t mutex;
// 创建条件变量
pthread_cond_t cond;

//创建一个链表用来做生产和消费的容器
struct Node{
    int num;
    struct Node *next;
};

// 头结点
struct Node * head = NULL;

void * producer(void * arg) {
    // 生产者不断的创建新的节点,添加到链表中
    while(1) {
        pthread_mutex_lock(&mutex);
        struct Node * newNode = (struct Node *)malloc(sizeof(struct Node));
        newNode->next = head;
        head = newNode;
        newNode->num = rand() % 1000;
        printf("add node, num : %d, tid : %ld\n", newNode->num, pthread_self());
        // 只要生产了一个,就通知消费者消费
        pthread_cond_signal(&cond);
        pthread_mutex_unlock(&mutex);
        usleep(100);
    }
    return NULL;
}

void * customer(void * arg) {
//消费者,删除一个节点
    while(1) {
        pthread_mutex_lock(&mutex);
        // 保存头结点的指针
        struct Node * tmp = head;
        // 判断是否有数据
        if(head != NULL) {
            // 有数据
            head = head->next;
            printf("del node, num : %d, tid : %ld\n", tmp->num, pthread_self());
            free(tmp);
            pthread_mutex_unlock(&mutex);
            usleep(100);
        } else {
            // 没有数据,需要等待
            // 当这个函数调用阻塞的时候,会对互斥锁进行解锁
            pthread_cond_wait(&cond, &mutex);
            //当收到信号后不阻塞了,继续向下执行,会重新加锁,所以下面需要再解锁一下
            pthread_mutex_unlock(&mutex);
        }
    }
    return  NULL;
}

int main() {
    pthread_mutex_init(&mutex, NULL);//初始化互斥锁
    pthread_cond_init(&cond, NULL);//初始化条件变量

    // 创建5个生产者线程,和5个消费者线程
    pthread_t ptids[5], ctids[5];

    for(int i = 0; i < 5; i++) {
        pthread_create(&ptids[i], NULL, producer, NULL);
        pthread_create(&ctids[i], NULL, customer, NULL);
    }

    for(int i = 0; i < 5; i++) {
        pthread_detach(ptids[i]);
        pthread_detach(ctids[i]);
    }

    while(1) {
        sleep(10);
    }

    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);

    pthread_exit(NULL);
    return 0;
}

信号量

  1. 也称信号灯,像信号灯那么理解,通过灯的亮和灭来交互
    在这里插入图片描述
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值