操作系统:线程互斥|线程同步|锁的概念

本文围绕C++线程互斥与同步展开。先介绍线程互斥,包括互斥量使用、锁本质及死锁相关知识,如死锁产生条件与避免方法;接着阐述线程同步,引入条件变量解决线程饥饿和锁资源浪费问题,还介绍了条件变量接口及使用效果。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

前言

1.线程互斥

1.1.互斥量|锁的使用

1.2.锁的本质

1.3.死锁

1.3.1.什么是死锁

1.3.2.死锁产生的4个必要条件

1.3.3.如何避免死锁

2.线程同步

2.1.知识引入

2.2.条件变量

2.2.1.为什么需要条件变量

2.2.2.条件变量接口


前言

进行这一章节的学习之前,我们需要回顾一下操作系统:进程间通信 | System V IPC-优快云博客这篇博客的3.2.信号量部分

  • 临界资源:多线程执行流共享的资源就叫做临界资源
  • 临界区:每个线程内部,访问临界资源的代码,就叫做临界区
  • 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
  • 原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成

以上4个知识非常重要!!! 

1.线程互斥

线程互斥用于确保在任意时刻只有一个线程可以访问某个特定的资源或代码段。这种机制可以防止多个线程同时修改同一数据,从而避免数据不一致或数据损坏的问题。

接着我们通过一个例子,来体会一下多线程下数据不一致或数据损坏的问题

int ticket = 1000;
void *ThreadTicket(void *args)
{
    pthread_detach(pthread_self());
    // 给每个线程进行死循环抢票
    while (1)
    {
        const char *name = (const char *)args;
        if (ticket > 0)
        {
            usleep(1000);
            printf("%s get a ticket: %d\n", name, ticket--);
        }
        else
            break;
    }
}
int main()
{
    pthread_t tid1;
    pthread_create(&tid1, nullptr, ThreadTicket, (void*)"Thread 1");
    pthread_t tid2;
    pthread_create(&tid2, nullptr, ThreadTicket, (void*)"Thread 2");
    pthread_t tid3;
    pthread_create(&tid3, nullptr, ThreadTicket, (void*)"Thread 3");

    while (1)
    {
        if (ticket < 0)
        {
            cout << "主线程结束抢票" << endl;
            sleep(5);
            break;
        }
    }
}

这段代码我们希望实现4个线程同时抢票,所以我们定义了一块共享资源ticket,并且4个线程同时对ticket进行减一……

然而我们实际运行程序时会发现,ticket输出不合理为负数,也就是数据出现了损害!那么为什么会出现这种情况呢?

  • 简单来说:CPU调度最后一次时,可能该时间片结束,需要等待下次调度来完成减减,然而CPU又调度了其他线程,那么这里就会出现两次或多次减减……
  • 根本原因:首先if语句判断为真的同时,线程可以并发切换到其他线程。其次我们通过unsleep模拟抢票登记信息,在这段时间内也会有很多线程被CPU调度。最后ticket减减并不是原子性的操作,在中间过程可能会出现线程切换!

 如何理解ticket减减不是原子的呢?首先我们知道原子的,即为完成或者失败!在CPU调度过程中,需要将共享变量ticket从内存加载到寄存器中,接着更新寄存器里面的值,执行-1操作,最后将新值从寄存器写回共享变量ticket的内存地址。也就是我们语言层面的一条语句,在底层需要3个动作实现,而每个动作是原子的,结合起来ticket减减不是原子的(6种情况)。

因为我们无法实现原子性,所以我们需要控制线程需要有互斥行为,当代码进入临界区执行时,不允许其他线程进入该临界区。一般情况下,我们对临界区进行加锁来实现互斥!

1.1.互斥量|锁的使用

  • 大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。
  • 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。

在上面的讲解中,我们得出:多个线程并发的操作共享变量,会带来一些问题。因此我们需要实现某一线程访问并修改共享资源时,需要对其他线程进行限制,加锁保护当前线程,实现合理修改。

那么我们如何使用互斥量,即如何在多线程并发场景进行加锁么?

// 定义全局锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

首先我们需要定义一个锁,可以是局部的和全局的,这里我们先介绍全局锁,定义完锁之后,对锁的基本操作有:上锁和解锁

// 上锁
pthread_mutex_lock(&mutex);
// 解锁
pthread_mutex_unlock(&mutex);

 当我们学会了上锁和解锁,我们就需要探究在哪里加锁和解锁!我们知道互斥量是为了避免“多个线程并发操作临界资源”而存在的,所以我们加锁解锁是需要在临界区中的!以我们抢票的例子:

int ticket = 1000;
// 定义全局锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 全局使用锁
void *ThreadTicketGlobal(void *args)
{
    pthread_detach(pthread_self());
    // 给每个线程进行死循环抢票
    while (1)
    {
        // 进行加锁
        pthread_mutex_lock(&mutex);

        const char *name = (const char *)args;

        if (ticket > 0)
        {
            usleep(1000);
            printf("%s get a ticket: %d\n", name, --ticket);
            pthread_mutex_unlock(&mutex);
        }
        else
        {
            // 在break前需要解锁,因为会直接跳出该模块
            pthread_mutex_unlock(&mutex);    
            break;
        }
    }
}

所以我们加锁的位置是在if判断之前,因为if区域是我们的临界区,不允许多线程同时访问࿰

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值