【Linux】26:线程互斥和同步

目录

一、线程互斥

[1.1 进程线程间的互斥相关背景概念](#1.1 进程线程间的互斥相关背景概念)

[1.2 数据不一致的现象](#1.2 数据不一致的现象)

[1.3 如何解决这个问题](#1.3 如何解决这个问题)

[1.4 理解数据为什么不一致](#1.4 理解数据为什么不一致)

[1.5 认识锁的接口](#1.5 认识锁的接口)

[1.6 理解锁](#1.6 理解锁)

[1.7 互斥量的封装](#1.7 互斥量的封装)

[1.7.1 方式1](#1.7.1 方式1)

[1.7.2 方式2RAII风格](#1.7.2 方式2RAII风格)

二、线程同步

[2.1 条件变量](#2.1 条件变量)

[2.2 同步概念和竞态条件](#2.2 同步概念和竞态条件)

[2.3 条件变量的接口](#2.3 条件变量的接口)

[2.3.1 测试代码1](#2.3.1 测试代码1)

[2.4 生产者消费者模型](#2.4 生产者消费者模型)

[2.4.1 为什么使用生产者消费者模型](#2.4.1 为什么使用生产者消费者模型)

[2.4.2 生产者消费者模型的优点](#2.4.2 生产者消费者模型的优点)

[2.5 基于BlockingQueue的生产者消费者模型](#2.5 基于BlockingQueue的生产者消费者模型)

[2.5.1 BlockingQueue](#2.5.1 BlockingQueue)

[2.5.2 C++queue模拟阻塞队列的生产者消费者模型(单生产者单消费者)](#2.5.2 C++queue模拟阻塞队列的生产者消费者模型(单生产者单消费者))

[2.5.2 C++queue模拟阻塞队列的生产者消费者模型(多生产者多消费者)](#2.5.2 C++queue模拟阻塞队列的生产者消费者模型(多生产者多消费者))

[2.6 条件变量的封装](#2.6 条件变量的封装)

三、POSIX信号量

[3.1 POSIX信号量的相关概念](#3.1 POSIX信号量的相关概念)

[3.2 基于环行队列的生产者消费者模型](#3.2 基于环行队列的生产者消费者模型)

[3.3 POSIX信号量的相关接口](#3.3 POSIX信号量的相关接口)

[3.4 基于环形队列的生产者消费者模型(单生产者单消费者)](#3.4 基于环形队列的生产者消费者模型(单生产者单消费者))

[3.4.1 main.cc](#3.4.1 main.cc)

[3.4.2 RingQueue.hpp](#3.4.2 RingQueue.hpp)

[3.4.3 Sem.hpp](#3.4.3 Sem.hpp)

[3.5 基于环形队列的生产者消费者模型(多生产者多消费者)](#3.5 基于环形队列的生产者消费者模型(多生产者多消费者))

[3.5.1 main.cc](#3.5.1 main.cc)

[3.5.2 Sem.hpp](#3.5.2 Sem.hpp)

[3.5.3 RingQueue.hpp](#3.5.3 RingQueue.hpp)

[3.5.4 Mutex.hpp](#3.5.4 Mutex.hpp)

四、总结


我们了解线程之后,可以发现一个进程内部的线程是共享地址空间的!!!所以线程共享大部分资源。如果我们定义全局变量,申请一块内存,所有的线程都可以看见,那么在多线程访问共享资源时,它极有可能造成各种数据不一致问题,为了解决数据不一致问题,我们就需要学习线程的同步和互斥!!

一、线程互斥

1.1 进程线程间的互斥相关背景概念

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

1.2 数据不一致的现象

我们来编写一段代码,让4个线程来模拟抢票的过程,代码如下所示:

cpp 复制代码
#include <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>
int ticket = 100;
void *routine(void *args)
{
    std::string name = static_cast<const char *>(args);
    while (1)
    {
        if (ticket > 0)
        {
            usleep(1000);
            printf("%s sells ticket:%d\n", name.c_str(), ticket);
            ticket--;
        }
        else
        {
            break;
        }
    }
    return nullptr;
}
int main()
{
    pthread_t tid1, tid2, tid3, tid4;
    pthread_create(&tid1, nullptr, routine, (void *)"thread-1");
    pthread_create(&tid2, nullptr, routine, (void *)"thread-2");
    pthread_create(&tid3, nullptr, routine, (void *)"thread-3");
    pthread_create(&tid4, nullptr, routine, (void *)"thread-4");

    pthread_join(tid1, nullptr);
    pthread_join(tid2, nullptr);
    pthread_join(tid3, nullptr);
    pthread_join(tid4, nullptr);
    return 0;
}

运行如下所示:

我们运行之后发现票的数量居然是负数了,不是只有票的数量大于0才可以自减嘛?为什么能变成负数呢?这是因为所有的线程访问票的时候不是互斥的。

为什么会出现这个现象呢?

当票数为1的时候,线程1来判断票数大于0,进入了if语句;如果此时发生了线程切换,线程2来判断票数大于0,进入了if语句;如果此时发生了线程切换,线程3来判断票数大于0,进入了if语句;如果此时发生了线程切换,线程4来判断票数大于0,进入了if语句;此时4个线程在票数为1的情况下,都进入了if语句。然后当4个线程切换回来执行的时候,都可以对票数减一。就出现了负数的情况。

1.3 如何解决这个问题

出现这个问题的原因,就是4个线程访问票的时候,不是互斥进行的。ticket是一个共享资源,每一个线程都可以访问。访问ticket共享资源的代码叫做临界区,我们可以在进入临界区之前加上锁,加上锁之后,只有持有锁的进程才可以访问ticket变量。其他线程不允许访问。退出临界区之后释放锁。

修改代码如下:

cpp 复制代码
#include <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>
int ticket = 100;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void *routine(void *args)
{
    std::string name = static_cast<const char *>(args);
    while (1)
    {
        pthread_mutex_lock(&lock);
        if (ticket > 0)
        {
            usleep(1000);
            printf("%s sells ticket:%d\n", name.c_str(), ticket);
            ticket--;
            pthread_mutex_unlock(&lock);
        }
        else
        {
            pthread_mutex_unlock(&lock);
            break;
        }
    }
    return nullptr;
}
int main()
{
    pthread_t tid1, tid2, tid3, tid4;
    pthread_create(&tid1, nullptr, routine, (void *)"thread-1");
    pthread_create(&tid2, nullptr, routine, (void *)"thread-2");
    pthread_create(&tid3, nullptr, routine, (void *)"thread-3");
    pthread_create(&tid4, nullptr, routine, (void *)"thread-4");

    pthread_join(tid1, nullptr);
    pthread_join(tid2, nullptr);
    pthread_join(tid3, nullptr);
    pthread_join(tid4, nullptr);
    return 0;
}

运行如下所示:

1.4 理解数据为什么不一致

为什么数据会不一致:

  1. if语句判断条件为真以后,代码可以并发的切换到其他线程
  2. usleep这个模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段
  3. --ticket操作本身就不是一个原子操作

--操作并不是原子操作,而是对应三条汇编指令:

  • load:将共享变量ticket从内存加载到寄存器中
  • update:更新寄存器里面的值,执行-1操作
  • store:将新值,从寄存器写回共享变量ticket的内存地址

要解决以上问题,需要做到三点:

  • 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
  • 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
  • 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。

要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量。

1.5 认识锁的接口

锁的类型为:pthread_mutex_t,也是pthread库提供的锁,和线程一样,linux不存在真正意义上的线程,线程的概念是pthread库提供的,当然,pthread不仅仅提供线程,它还要提供控制线程,保护共享资源,临界资源的锁机制。称之为互斥锁,或者互斥量。

初始化锁的函数如下所示:

定义互斥锁有两种方式:

方式1:使用全局的锁,然后使用PTHREAD_MUTEX_INITIALIZER来初始化。这样定义的锁不需要被释放,程序运行结束,会自动释放。

方式2:如果想要采用局部的方式定义锁,就需要使用pthread_mutex_init来初始化锁。当你不想要这个锁了,需要调用pthread_mutex_destroy来销毁锁。

当我们将锁给定义出来之后,不管是全局的,还是局部的。所有线程访问公共资源之前,都必须申请锁!!退出临界区之后要释放锁。函数如下所示:

1:所有人(线程)都必须相互竞争锁,因此所有线程都需要看到锁,因此锁本身就是临界资源,锁是为了保护共享资源的安全,那么谁来保护锁的安全呢?因此申请锁的过程必须是原子的。

2:申请锁成功的线程,可以向后继续运行,访问临界区。如果申请锁失败了呢?就会阻塞挂起该线程,当锁被其他线程释放之后,就唤起因为该锁而阻塞的进程。然后这些线程重新竞争该锁。

3:锁提供的能力本质:执行临界区代码由并行转换为串行。

定义锁的方式二:测试代码如下:

cpp 复制代码
#include <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>
int ticket = 100;

class ThreadData
{
public:
    ThreadData(const std::string &n, pthread_mutex_t &lock)
        : name(n), lockp(&lock)
    {
    }
    ~ThreadData() {}
    std::string name;
    pthread_mutex_t *lockp;
};
void *routine(void *args)
{
    ThreadData *td = static_cast<ThreadData *>(args);
    while (1)
    {
        pthread_mutex_lock(td->lockp);
        if (ticket > 0)
        {
            usleep(1000);
            printf("%s sells ticket:%d\n", td->name.c_str(), ticket);
            ticket--;
            pthread_mutex_unlock(td->lockp);
        }
        else
        {
            pthread_mutex_unlock(td->lockp);
            break;
        }
    }
    return nullptr;
}
int main()
{
    pthread_mutex_t lock;               // 局部定义锁
    pthread_mutex_init(&lock, nullptr); // 初始化锁

    pthread_t tid1, tid2, tid3, tid4;
    ThreadData *td1 = new ThreadData("thread-1", lock);
    ThreadData *td2 = new ThreadData("thread-2", lock);
    ThreadData *td3 = new ThreadData("thread-3", lock);
    ThreadData *td4 = new ThreadData("thread-4", lock);
    pthread_create(&tid1, nullptr, routine, (void *)td1);
    pthread_create(&tid2, nullptr, routine, (void *)td2);
    pthread_create(&tid3, nullptr, routine, (void *)td3);
    pthread_create(&tid4, nullptr, routine, (void *)td4);

    pthread_join(tid1, nullptr);
    pthread_join(tid2, nullptr);
    pthread_join(tid3, nullptr);
    pthread_join(tid4, nullptr);
    pthread_mutex_destroy(&lock); // 销毁锁
    return 0;
}

问题一:如果我们写代码的时候,让一个线程不用申请锁会怎么样?这叫做自己给自己的代码写bug,一旦需要访问共享资源,所有的线程都需要申请锁!!!

问题二:加锁之后,在临界区内部,允许线程切换吗?切换了会怎么样?

加锁之后,在临界区内部,允许线程切换,并且不会出现线程数据不一致问题,当线程1拿到锁之后,被切换了,线程2想要访问共享资源ticket,就先需要申请锁,此时锁已经被线程1申请到了,所以线程2就会阻塞!!只有当线程1执行完临界区的代码之后,才会释放锁,其他线程才有机会展开对锁的竞争,抢到锁的线程才能进入临界区。

1.6 理解锁

1:硬件级实现锁:为什么我的代码不是原子的,这是因为我的代码是大量的代码块,当时钟中断来了,可以随时被切换,最简单粗暴的做法,关闭时钟中断,只要把时钟中断关闭了,就没有人能打扰我了,实现了原子性。

2:软件级实现锁:使用swap或exchange指令

为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令**,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,**即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。现在我们把lock和unlock的伪代码改一下:

1.7 互斥量的封装

1.7.1 方式1

mutex.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <pthread.h>

namespace MutexModule
{
    class Mutex
    {
    public:
        Mutex()
        {
            pthread_mutex_init(&_mutex, nullptr);
        }
        void Lock()
        {
            pthread_mutex_lock(&_mutex);
        }
        void UnLock()
        {
            pthread_mutex_unlock(&_mutex);
        }
        ~Mutex()
        {
            pthread_mutex_destroy(&_mutex);
        }

    private:
        pthread_mutex_t _mutex;
    };
}

test.cc

cpp 复制代码
#include "mutex.hpp"
#include <unistd.h>

using namespace MutexModule;
class ThreadData
{
public:
    ThreadData(const std::string name, Mutex &mutex)
        : _name(name), _mutex(&mutex)
    {
    }
    ~ThreadData() {}

    std::string _name;
    Mutex *_mutex;
};
int ticket = 100;
void *routine(void *args)
{
    ThreadData *td = static_cast<ThreadData *>(args);
    while (1)
    {
        td->_mutex->Lock();
        if (ticket > 0)
        {
            usleep(10000);
            printf("%s sells %d\n", td->_name.c_str(), ticket);
            ticket--;
            td->_mutex->UnLock();
        }
        else
        {
            td->_mutex->UnLock();
            break;
        }
    }
    return nullptr;
}
int main()
{
    Mutex mutex;
    ThreadData *td1 = new ThreadData("thread-1", mutex);
    ThreadData *td2 = new ThreadData("thread-2", mutex);
    ThreadData *td3 = new ThreadData("thread-3", mutex);
    ThreadData *td4 = new ThreadData("thread-4", mutex);
    pthread_t tid1, tid2, tid3, tid4;

    pthread_create(&tid1, nullptr, routine, td1);
    pthread_create(&tid2, nullptr, routine, td2);
    pthread_create(&tid3, nullptr, routine, td3);
    pthread_create(&tid4, nullptr, routine, td4);

    pthread_join(tid1, nullptr);
    pthread_join(tid2, nullptr);
    pthread_join(tid3, nullptr);
    pthread_join(tid4, nullptr);

    return 0;
}

1.7.2 方式2RAII风格

mutex.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <pthread.h>

namespace MutexModule
{
    class Mutex
    {
    public:
        Mutex()
        {
            pthread_mutex_init(&_mutex, nullptr);
        }
        void Lock()
        {
            pthread_mutex_lock(&_mutex);
        }
        void UnLock()
        {
            pthread_mutex_unlock(&_mutex);
        }
        ~Mutex()
        {
            pthread_mutex_destroy(&_mutex);
        }

    private:
        pthread_mutex_t _mutex;
    };
    class LockGuard
    {
    public:
        LockGuard(Mutex &mutex) : _mutex(mutex)
        {
            _mutex.Lock();
        }
        ~LockGuard()
        {
            _mutex.UnLock();
        }

    private:
        Mutex &_mutex;
    };
}

test.cc

cpp 复制代码
#include "mutex.hpp"
#include <unistd.h>

using namespace MutexModule;
class ThreadData
{
public:
    ThreadData(const std::string name, Mutex &mutex)
        : _name(name), _mutex(&mutex)
    {
    }
    ~ThreadData() {}

    std::string _name;
    Mutex *_mutex;
};
int ticket = 100;
void *routine(void *args)
{
    ThreadData *td = static_cast<ThreadData *>(args);
    while (1)
    {
        LockGuard guard(*td->_mutex);
        if (ticket > 0)
        {
            usleep(10000);
            printf("%s sells %d\n", td->_name.c_str(), ticket);
            ticket--;
        }
        else
        {
            break;
        }
    }
    return nullptr;
}
int main()
{
    Mutex mutex;
    ThreadData *td1 = new ThreadData("thread-1", mutex);
    ThreadData *td2 = new ThreadData("thread-2", mutex);
    ThreadData *td3 = new ThreadData("thread-3", mutex);
    ThreadData *td4 = new ThreadData("thread-4", mutex);
    pthread_t tid1, tid2, tid3, tid4;

    pthread_create(&tid1, nullptr, routine, td1);
    pthread_create(&tid2, nullptr, routine, td2);
    pthread_create(&tid3, nullptr, routine, td3);
    pthread_create(&tid4, nullptr, routine, td4);

    pthread_join(tid1, nullptr);
    pthread_join(tid2, nullptr);
    pthread_join(tid3, nullptr);
    pthread_join(tid4, nullptr);

    return 0;
}

二、线程同步

2.1 条件变量

  • 当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
  • 例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。

2.2 同步概念和竞态条件

  • 同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步
  • 竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。在线程场景下,这种问题也不难理解

2.3 条件变量的接口

条件变量的类型为:pthread_cond_t。

初始化条件变量:初始化条件变量和锁的初始化一样,同样有两者做法

方式1:使用全局的条件变量,然后使用PTHREAD_COND_INITIALIZER来初始化。这样定义的条件变量不需要被释放,程序运行结束,会自动释放。

方式2:如果想要采用局部的方式定义条件变量,就需要使用pthread_cond_init来初始化锁。当你不想要这个条件变量了,需要调用pthread_cond_destroy来销毁条件变量。

等待条件变量的接口:

第一个参数:在指定的条件变量下等待。

第二个参数:是一把互斥锁,下面再讲解。

唤醒在指定条件变量下的等待线程接口:

pthread_cond_signal是唤醒在该条件变量下等待的一个线程。

pthread_cond_broadcast是唤醒在该条件变量下等待的所有线程。

2.3.1 测试代码1

cpp 复制代码
#include <iostream>
#include <pthread.h>
#include <vector>
#include <string>
#include <unistd.h>

#define NUM 5
int cnt = 1000;
pthread_mutex_t glock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t gcond = PTHREAD_COND_INITIALIZER;

void *threadrun(void *args)
{
    std::string name = static_cast<const char *>(args);

    while (true)
    {
        pthread_mutex_lock(&glock);
        pthread_cond_wait(&gcond, &glock);
        std::cout << name << "计算:" << cnt << std::endl;
        cnt++;
        pthread_mutex_unlock(&glock);
    }
    return nullptr;
}

int main()
{
    std::vector<pthread_t> threads;
    for (int i = 0; i < NUM; i++)
    {
        pthread_t tid;
        char *name = new char[64];
        snprintf(name, 64, "thread-%d", i);
        pthread_create(&tid, nullptr, threadrun, name);
        threads.push_back(tid);
    }
    // 让主线程每隔一段时间唤醒一个线程
    while (true)
    {
        std::cout << "唤醒某一个线程" << std::endl;
        pthread_cond_signal(&gcond);
        sleep(1);
    }
    for (auto &tid : threads)
    {
        pthread_join(tid, nullptr);
    }
    return 0;
}

等待是需要阻塞等的,什么条件才会等呢?在买票的情况下,票数为0,就需要阻塞等。等待之前,我们就要对资源的数量进行判定,判定本身就是访问临界资源,判断等待的情况下,一定是在临界区内部的。判定结果,也一定在临界区内部,所以,条件不满足需要休眠,也一定是在临界区内休眠的。

在判定结果不满足时,是需要等待的,你在等待的时候,不要忘记,你是带着锁的啊。你去休眠了,锁还被你拿走了,那所有的线程不都访问不了临界区了吗?

所以pthread_cond_wait在等待的时候,需要传入一把锁,就是上面返回临界资源的锁,当判定结果不满足时,就去阻塞等待,等待的时候,把这把锁释放出来,这样其他的线程就可以访问临界区了。

当有一个线程调用pthread_cond_signal的时候,就会把你唤醒,然后继续去竞争锁!

2.4 生产者消费者模型

  • 仓库本质是就是临界资源

三种关系

  • 生产者之间:互斥关系
  • 消费者之间:互斥关系
  • 生产者和消费者之间:互斥和同步关系

两种角色

  • 生产者和消费者

一个交易场所

  • 以特定结构构成的内存空间

2.4.1 为什么使用生产者消费者模型

生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。

2.4.2 生产者消费者模型的优点

  • 解耦
  • 支持并发,提供效率
  • 支持忙闲不均

2.5 基于BlockingQueue的生产者消费者模型

2.5.1 BlockingQueue

在多线程编程中阻塞队列(BlockingQueue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)

2.5.2 C++queue模拟阻塞队列的生产者消费者模型(单生产者单消费者)

BlockQueue.hpp代码如下:

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <queue>
#include <pthread.h>

const int defaultcap = 5;

template <class T>
class BlockQueue
{
    bool IsFull()
    {
        return _q.size() >= _cap;
    }

    bool IsEmpty()
    {
        return _q.size() == 0;
    }

public:
    BlockQueue(int cap = defaultcap)
        : _cap(cap), _csleep_num(0), _psleep_num(0)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_full_cond, nullptr);
        pthread_cond_init(&_empty_cond, nullptr);
    }
    void Equeue(const T &in)
    {
        pthread_mutex_lock(&_mutex);
        while (IsFull())
        {
            // 阻塞队列满了,需要生产者线程阻塞
            // 重点1:pthread_cond_wait调用成功,阻塞挂起当前线程之前,要先自动释放锁
            // 重点2:当线程被唤醒时,默认就在临界区内被唤醒,但是我没有锁了,要从
            // pthread_cond_wait成功返回之后,需要当前线程重新申请_mutex锁。
            // 重点3:如果我被唤醒了,但是申请锁失败了怎么办?当前线程就会在锁上阻塞等待!
            _psleep_num++;
            // 问题1:pthread_cond_wait是函数吗?有没有可能失败?
            // 失败了就没有阻塞该线程,而立即返回了,此时队列还是满的,push就会出错
            // 问题2:pthread_cond_wait可能会因为条件其实并不满足,pthread_cond_wait被伪唤醒了
            // 解决方法:不能使用if,而使用while
            pthread_cond_wait(&_full_cond, &_mutex);
            _psleep_num--;
        }
        _q.push(in);
        // 此时一定有数据
        if (_csleep_num > 0)
        {
            pthread_cond_signal(&_empty_cond);
            std::cout << "唤醒消费者" << std::endl;
        }
        pthread_mutex_unlock(&_mutex);
    }
    T Pop()
    {
        pthread_mutex_lock(&_mutex);
        while (IsEmpty())
        {
            // 如果队列是空的,需要消费者阻塞
            _csleep_num++;
            pthread_cond_wait(&_empty_cond, &_mutex);
            _csleep_num--;
        }
        T data = _q.front();
        _q.pop();
        // 此时一定有空间
        if (_psleep_num > 0)
        {
            pthread_cond_signal(&_full_cond);
            std::cout << "唤醒生产者" << std::endl;
        }
        pthread_mutex_unlock(&_mutex);
        return data;
    }
    ~BlockQueue()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_full_cond);
        pthread_cond_destroy(&_empty_cond);
    }

private:
    std::queue<T> _q;
    int _cap;

    pthread_mutex_t _mutex;
    pthread_cond_t _full_cond;
    pthread_cond_t _empty_cond;

    int _csleep_num;
    int _psleep_num;
};

Main.cc代码如下:

cpp 复制代码
#include "BlockQueue.hpp"
#include <iostream>
#include <unistd.h>
#include <pthread.h>

void *consumer(void *args)
{
    BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(args);
    // 消费者不断的消耗
    while (true)
    {
        int data = bq->Pop();
        std::cout << "消费了一个数据:" << data << std::endl;
    }
    return nullptr;
}
void *productor(void *args)
{
    int data = 1;
    BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(args);
    // 生产者不断的生产
    while (true)
    {
        sleep(1);
        std::cout << "生产了一个数据" << data << std::endl;
        bq->Equeue(data);
        data++;
    }
    return nullptr;
}

int main()
{
    // 创建阻塞队列
    BlockQueue<int> *bq = new BlockQueue<int>();

    // 构建生产者消费者
    pthread_t c, p;
    pthread_create(&c, nullptr, consumer, bq);
    pthread_create(&p, nullptr, productor, bq);

    // join
    pthread_join(c, nullptr);
    pthread_join(p, nullptr);
    return 0;
}

运行如下所示:

扩展认识:阻塞队列只能放整数吗?可以放任务吗?

任务形式1:

任务模块Task.hpp

cpp 复制代码
#pragma once

class Task
{
public:
    Task() {}
    Task(int x, int y)
        : _x(x), _y(y)
    {
    }
    void Execute()
    {
        _result = _x + _y;
    }
    int X() { return _x; }
    int Y() { return _y; }
    int Result() { return _result; }

private:
    int _x;
    int _y;
    int _result;
};

Main.cc

cpp 复制代码
#include "BlockQueue.hpp"
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include "Task.hpp"

void *consumer(void *args)
{
    BlockQueue<Task> *bq = static_cast<BlockQueue<Task> *>(args);
    // 消费者不断的消耗
    while (true)
    {
        Task task = bq->Pop();
        std::cout << "消费者拿到了一个任务" << std::endl;
        task.Execute();
        std::cout << "消费者执行一个任务" << task.X() << "+" << task.Y() << "=" << task.Result() << std::endl;
    }
    return nullptr;
}
void *productor(void *args)
{
    BlockQueue<Task> *bq = static_cast<BlockQueue<Task> *>(args);
    int x = 1;
    int y = 1;
    // 生产者不断的生产
    while (true)
    {
        sleep(1);
        Task task(x, y);
        std::cout << "生产者发布了一个任务" << task.X() << "+" << task.Y() << std::endl;
        bq->Equeue(task);
        x++;
        y++;
    }
    return nullptr;
}

int main()
{
    // 创建阻塞队列
    BlockQueue<Task> *bq = new BlockQueue<Task>();

    // 构建生产者消费者
    pthread_t c, p;
    pthread_create(&c, nullptr, consumer, bq);
    pthread_create(&p, nullptr, productor, bq);

    // join
    pthread_join(c, nullptr);
    pthread_join(p, nullptr);
    return 0;
}

运行如下所示:


任务形式2:

cpp 复制代码
#pragma once
#include <functional>
#include <iostream>
#include <unist.h>

// 任务形式2
// 我们定义了一个任务类型,返回值为void,参数为空
using task_t = std::function<void()>;
void Download()
{
    std::cout << "我是一个下载任务..." << std::endl;
    sleep(3);
}
cpp 复制代码
#include "BlockQueue.hpp"
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include "Task.hpp"

// 方式3:
void *consumer(void *args)
{
    BlockQueue<task_t> *bq = static_cast<BlockQueue<task_t> *>(args);
    // 消费者不断的消耗
    while (true)
    {
        task_t task = bq->Pop();
        std::cout << "消费者拿到了一个任务" << std::endl;
        std::cout << "消费者开始执行一个任务" << std::endl;
        task();
    }
    return nullptr;
}
void *productor(void *args)
{
    BlockQueue<task_t> *bq = static_cast<BlockQueue<task_t> *>(args);
    // 生产者不断的生产
    while (true)
    {
        sleep(1);
        std::cout << "生产者发布了一个任务" << std::endl;
        bq->Equeue(Download);
    }
    return nullptr;
}

int main()
{
    // 创建阻塞队列
    BlockQueue<task_t> *bq = new BlockQueue<task_t>();

    // 构建生产者消费者
    pthread_t c, p;
    pthread_create(&c, nullptr, consumer, bq);
    pthread_create(&p, nullptr, productor, bq);

    // join
    pthread_join(c, nullptr);
    pthread_join(p, nullptr);
    return 0;
}

在上面生产者消费者模型的优点,我们说过效率比较高,为什么高呢?生产过程和消费过程是串行的,为什么效率还高呢?

不是生产和消费串行;是【对队列的访问】串行,生产、消费的业务逻辑是并行的!

  1. 生产数据、消费数据的业务逻辑可以并行执行;仅访问共享队列的临界区串行,临界区代码短小,持有锁时间短。
  2. 使用条件变量阻塞等待,不是忙轮询,等待时线程休眠,不浪费 CPU。
  3. 队列作为缓冲区,解耦生产与消费,可以平滑两者速度差异,提升整体吞吐。

2.5.2 C++queue模拟阻塞队列的生产者消费者模型(多生产者多消费者)

阻塞队列完全不需要该,多生产者多消费者增加了两个关系,生产者之间的互斥关系,生产者之间的互斥关系。但是我们给阻塞队列加锁了啊,生产者和生产者,消费者和消费者本来就是互斥关系。

Main.cc函数如下:

cpp 复制代码
#include "BlockQueue.hpp"
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include "Task.hpp"


// 多生产者,多消费者:
void *consumer(void *args)
{
    BlockQueue<task_t> *bq = static_cast<BlockQueue<task_t> *>(args);
    // 消费者不断的消耗
    while (true)
    {
        task_t task = bq->Pop();
        std::cout << "消费者拿到了一个任务" << std::endl;
        std::cout << "消费者开始执行一个任务" << std::endl;
        task();
    }
    return nullptr;
}
void *productor(void *args)
{
    BlockQueue<task_t> *bq = static_cast<BlockQueue<task_t> *>(args);
    // 生产者不断的生产
    while (true)
    {
        sleep(1);
        std::cout << "生产者发布了一个任务" << std::endl;
        bq->Equeue(Download);
    }
    return nullptr;
}

int main()
{
    // 创建阻塞队列
    BlockQueue<task_t> *bq = new BlockQueue<task_t>();

    // 构建生产者消费者
    pthread_t c[2], p[3];
    pthread_create(c, nullptr, consumer, bq);
    pthread_create(c + 1, nullptr, consumer, bq);
    pthread_create(p, nullptr, productor, bq);
    pthread_create(p + 1, nullptr, productor, bq);
    pthread_create(p + 2, nullptr, productor, bq);

    // join
    pthread_join(c[0], nullptr);
    pthread_join(c[1], nullptr);
    pthread_join(p[0], nullptr);
    pthread_join(p[1], nullptr);
    pthread_join(p[2], nullptr);
    return 0;
}

2.6 条件变量的封装

cpp 复制代码
#pragma once

#include <iostream>
#include <pthread.h>
#include "Mutex.hpp"
using namespace MutexModule;

namespace CondModule
{
    class Cond
    {
    public:
        Cond()
        {
            pthread_cond_init(&_cond, nullptr);
        }
        void Wait(Mutex &mutex)
        {
            pthread_cond_wait(&_cond, mutex.Get());
        }
        void Singal()
        {
            pthread_cond_signal(&_cond);
        }
        void BroadCast()
        {
            pthread_cond_broadcast(&_cond);
        }
        ~Cond()
        {
            pthread_cond_destroy(&_cond);
        }

    private:
        pthread_cond_t _cond;
    };
}

三、POSIX信号量

3.1 POSIX信号量的相关概念

POSIX 信号量是POSIX 标准定义的一种进程 / 线程间同步原语,本质是一个计数器,用来控制对共享资源的访问,解决竞态条件。

申请信号量本身是对资源的预定机制。

多线程使用资源,有两种场景:

1:将目标资源整体使用。(使用锁或者二元信号量)

2:将目标资源安装不同的块,分批使用。(信号量)

使用信号量,所有线程都需要看到信号量,申请信号量就是对资源数量做--操作,释放信号量就是对资源做++操作。信号量本身也是临界资源,所以信号量的操作也是原子的。

3.2 基于环行队列的生产者消费者模型

环形队列采⽤数组模拟,⽤模运算来模拟环状特性

环形结构起始状态和结束状态都是一样的,不好判断为空或者为满,所以可以通过加计数器或者标记位来判断满或者空。另外也可以预留一个空的位置,作为满的状态

但是我们现在有信号量这个计数器,就很简单的进行多线程间的同步过程。

关于环形队列,有几个约定如下:

1:环形队列的数据为空时,必须要去生产者生产,消费者阻塞

2:环形队列的数据为满时,必须要求消费者消费,生产者阻塞

3:生产者已经把环形队列的数据生产满了,此时不能再生产数据了,必须阻塞生产者

4:当环形队列的数据为空时,消费者不允许消费,必须阻塞。
思考:

1:只要生产者,消费者不访问同一个位置,那么生产者和消费者就可以同时运行!!

2:什么时候,生产者和消费者要访问同一个位置呢?

情况一:环形队列为空时

情况二:环形队列为满时。

那么环形队列不为空,不为满,就可以同时运行!!!

3:环形队列为空时:必须阻塞消费者,并且互斥访问同一个位置且只能让生产者先运行,

4:环形队列为满时:必须阻塞生产者,并且互斥访问同一个位置且只能让消费者先运行
结论1:环形队列不为空并且不为满,生产者消费者可以同时运行。

结论2:环形队列为空或为满时,生产者消费者需要同步互斥运行。

这个结论都是基于那4个约定进行的,那么怎么保证这四个约定成立呢?信号量就可以保证!!

信号量是表示资源数量的。

那么对于生产者,关心的资源是什么?是环形队列空的位置有多少个。使用信号量sem_blank=N

那么对于消费者,关心的资源是什么?是环形队列的数据有多少个。使用信号量sem_data=0

3.3 POSIX信号量的相关接口

1:初始化信号量:

参数一:信号量

参数二:0表示线程间共享,非0表示进程间共享

参数三:信号量初始值

返回值:0调用成功,非0调用失败

2:销毁信号量:

3:等待信号量:

4:释放信号量:

3.4 基于环形队列的生产者消费者模型(单生产者单消费者)

3.4.1 main.cc

cpp 复制代码
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include "RingQueue.hpp"
void *consumer(void *args)
{
    RingQueue<int> *rq = static_cast<RingQueue<int> *>(args);
    while (1)
    {
        sleep(2);
        int data = 0;
        rq->Pop(&data);
        std::cout << "消费了一个数据:" << data << std::endl;
    }
    return nullptr;
}
void *productor(void *args)
{
    RingQueue<int> *rq = static_cast<RingQueue<int> *>(args);
    int data = 10;
    while (1)
    {
        std::cout << "生产了一个数据" << data << std::endl;
        rq->Equeue(data);
        data++;
        // sleep(1);
    }
    return nullptr;
}
int main()
{
    RingQueue<int> *rq = new RingQueue<int>();
    pthread_t p, c;
    pthread_create(&p, nullptr, productor, (void *)rq);
    pthread_create(&c, nullptr, consumer, (void *)rq);

    pthread_join(p, nullptr);
    pthread_join(c, nullptr);
}

3.4.2 RingQueue.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <vector>
#include "Sem.hpp"
static const int gcap = 5;
using namespace SemModule;
template <class T>
class RingQueue
{
public:
    RingQueue(int cap = gcap)
        : _rq(cap),
          _cap(cap),
          _blank_sem(cap),
          _p_index(0),
          _data_sem(0),
          _c_index(0)
    {
    }
    void Equeue(const T &in)
    {
        // 生产者
        // 1:申请信号量
        _blank_sem.P();
        // 2:生产
        _rq[_p_index] = in;
        // 3:更新下标
        _p_index++;
        // 4:维护环形特性
        _p_index %= _cap;
        // 5:通知消费者
        _data_sem.V();
    }
    void Pop(T *out)
    {
        // 消费者
        // 1:申请信号量
        _data_sem.P();
        // 2:消费
        *out = _rq[_c_index];
        // 3:更新下标
        _c_index++;
        // 4:维护环形特性
        _c_index %= _cap;
        // 5:通知生产者
        _blank_sem.V();
    }
    ~RingQueue() {}

private:
    std::vector<T> _rq;
    int _cap;

    // 生产者
    Sem _blank_sem;
    int _p_index;
    // 消费者
    Sem _data_sem;
    int _c_index;
};

3.4.3 Sem.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <pthread.h>
#include <semaphore.h>

namespace SemModule
{
    const int defaultvalue = 1;
    class Sem
    {
    public:
        Sem(unsigned int sem_value = defaultvalue)
        {
            sem_init(&_sem, 0, sem_value);
        }
        void P()
        {
            int n = sem_wait(&_sem);
        }
        void V()
        {
            int n = sem_post(&_sem);
        }
        ~Sem()
        {
            sem_destroy(&_sem);
        }

    private:
        sem_t _sem;
    };
}

3.5 基于环形队列的生产者消费者模型(多生产者多消费者)

3.5.1 main.cc

cpp 复制代码
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include "RingQueue.hpp"
void *consumer(void *args)
{
    RingQueue<int> *rq = static_cast<RingQueue<int> *>(args);
    while (1)
    {
        sleep(2);
        int data = 0;
        rq->Pop(&data);
        std::cout << "消费了一个数据:" << data << std::endl;
    }
    return nullptr;
}
void *productor(void *args)
{
    RingQueue<int> *rq = static_cast<RingQueue<int> *>(args);
    int data = 10;
    while (1)
    {
        std::cout << "生产了一个数据" << data << std::endl;
        rq->Equeue(data);
        data++;
        // sleep(1);
    }
    return nullptr;
}
int main()
{
    RingQueue<int> *rq = new RingQueue<int>();
    pthread_t p[3], c[2];
    pthread_create(&p[0], nullptr, productor, (void *)rq);
    pthread_create(&p[1], nullptr, productor, (void *)rq);
    pthread_create(&p[2], nullptr, productor, (void *)rq);
    pthread_create(&c[0], nullptr, consumer, (void *)rq);
    pthread_create(&c[1], nullptr, consumer, (void *)rq);

    pthread_join(p[0], nullptr);
    pthread_join(p[1], nullptr);
    pthread_join(p[2], nullptr);
    pthread_join(c[0], nullptr);
    pthread_join(c[1], nullptr);
}

3.5.2 Sem.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <pthread.h>
#include <semaphore.h>

namespace SemModule
{
    const int defaultvalue = 1;
    class Sem
    {
    public:
        Sem(unsigned int sem_value = defaultvalue)
        {
            sem_init(&_sem, 0, sem_value);
        }
        void P()
        {
            int n = sem_wait(&_sem);
        }
        void V()
        {
            int n = sem_post(&_sem);
        }
        ~Sem()
        {
            sem_destroy(&_sem);
        }

    private:
        sem_t _sem;
    };
}

3.5.3 RingQueue.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <vector>
#include "Sem.hpp"
#include "Mutex.hpp"
static const int gcap = 5;
using namespace SemModule;
using namespace MutexModule;

template <class T>
class RingQueue
{
public:
    RingQueue(int cap = gcap)
        : _rq(cap),
          _cap(cap),
          _blank_sem(cap),
          _p_index(0),
          _data_sem(0),
          _c_index(0)
    {
    }
    // void Equeue(const T &in)
    // {
    //     // 生产者
    //     _pmutex.Lock();
    //     // 1:申请信号量
    //     _blank_sem.P();
    //     // 2:生产
    //     _rq[_p_index] = in;
    //     // 3:更新下标
    //     _p_index++;
    //     // 4:维护环形特性
    //     _p_index %= _cap;
    //     // 5:通知消费者
    //     _data_sem.V();
    //     _pmutex.UnLock();
    // }
    // void Pop(T *out)
    // {
    //     // 消费者
    //     _cmutex.Lock();
    //     // 1:申请信号量
    //     _data_sem.P();
    //     // 2:消费
    //     *out = _rq[_c_index];
    //     // 3:更新下标
    //     _c_index++;
    //     // 4:维护环形特性
    //     _c_index %= _cap;
    //     // 5:通知生产者
    //     _blank_sem.V();
    //     _cmutex.UnLock();
    // }
    void Equeue(const T &in)
    {
        // 生产者
        // 1:申请信号量
        _blank_sem.P();
        {
            LockGuard lockguard(_pmutex);
            // 2:生产
            _rq[_p_index] = in;
            // 3:更新下标
            _p_index++;
            // 4:维护环形特性
            _p_index %= _cap;
        }
        // 5:通知消费者
        _data_sem.V();
    }
    void Pop(T *out)
    {
        // 消费者
        // 1:申请信号量
        _data_sem.P();
        {
            LockGuard lockguard(_cmutex);
            // 2:消费
            *out = _rq[_c_index];
            // 3:更新下标
            _c_index++;
            // 4:维护环形特性
            _c_index %= _cap;
        }

        // 5:通知生产者
        _blank_sem.V();
    }
    ~RingQueue() {}

private:
    std::vector<T> _rq;
    int _cap;

    // 生产者
    Sem _blank_sem;
    int _p_index;
    // 消费者
    Sem _data_sem;
    int _c_index;

    // 维护多生产,多消费
    Mutex _cmutex;
    Mutex _pmutex;
};

3.5.4 Mutex.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <pthread.h>

namespace MutexModule
{
    class Mutex
    {
    public:
        Mutex()
        {
            pthread_mutex_init(&_mutex, nullptr);
        }
        void Lock()
        {
            pthread_mutex_lock(&_mutex);
        }
        void UnLock()
        {
            pthread_mutex_unlock(&_mutex);
        }
        pthread_mutex_t *Get()
        {
            return &_mutex;
        }
        ~Mutex()
        {
            pthread_mutex_destroy(&_mutex);
        }

    private:
        pthread_mutex_t _mutex;
    };
    class LockGuard
    {
    public:
        LockGuard(Mutex &mutex) : _mutex(mutex)
        {
            _mutex.Lock();
        }
        ~LockGuard()
        {
            _mutex.UnLock();
        }

    private:
        Mutex &_mutex;
    };
}

在单生产者单消费者的环形队列中,不需要维护生产者之间的互斥关系,也不需要维护消费者之间的互斥关系,只维护了生产者和消费者的同步互斥问题。

但是在多生产者多消费者的环形队列中,不仅需要维护生产者和消费者之间的关系,还需要维护生产者之间的互斥关系,消费者之间的互斥关系。

  • 观察两段代码,我们需要辨析:生产者消费者模型中,应该先加互斥锁,还是先申请信号量。
  • 方案 1:先申请互斥锁,再申请信号量。 这种思路认为:多生产者场景下,先通过锁竞争选出一个生产者获得环形队列的访问权;消费者同理,先竞争锁。拿到锁的线程,再去完成位置的同步判断。
  • 方案 2:先申请信号量,再申请互斥锁。 各个生产者先通过信号量完成资源预定。只有成功预定到资源后,才有资格去竞争互斥锁。
  • 可以用电影院买票的例子类比: 方案 1 相当于先排队抢占柜台,再尝试买票;方案 2 是先预定票源,拿到购票资格后,再排队办理手续。
  • 对比二者效率:方案 2 效率更高。 线程先通过信号量瓜分资源,后续仅在操作临界区时串行竞争锁,锁的持有时间短。
  • 反观方案 1:线程先要竞争互斥锁,就算抢到锁,后续仍可能因资源不足阻塞在信号量上。

对比一下阻塞队列和环形队列生产数据的场景:

为什么环形队列在输入数据的时候,不需要判定环形队列是否为满呢?

这是因为信号量是用来描述临界资源数量的。信号量申请失败就是条件不满足!!!

所以信号量的另一个本质:信号量把对临界资源是否存在,以原子性的形式,呈现在访问临界资源之前就判断了。

四、总结

  1. 线程互斥(互斥量 mutex) 进程内线程共享地址空间,全局变量、堆内存属于临界资源 ,访问临界资源的代码段叫临界区。ticket--这类操作不是原子操作,会被线程调度打断,多线程并发访问会出现数据不一致(抢票负数问题)。使用pthread_mutex_t互斥锁:进入临界区lock,退出unlock,保证同一时刻仅一个线程进入临界区;锁支持全局静态初始化PTHREAD_MUTEX_INITIALIZER,也可局部动态init/destroy;还可封装成 RAII 风格 LockGuard,利用对象生命周期自动加解锁,防止忘记释放锁。锁底层依靠 CPU 硬件 swap/exchange 原子指令实现。
  2. 线程同步(条件变量 cond) 互斥只保证安全,同步是在安全基础上,控制线程执行顺序,解决等待问题 ,使用pthread_cond_t条件变量。pthread_cond_wait会自动释放传入的互斥锁、阻塞线程;被唤醒后重新竞争锁。必须用 while 循环判断条件 ,不能用 if,处理伪唤醒。pthread_cond_signal唤醒一个等待线程,pthread_cond_broadcast唤醒全部等待线程。典型应用:阻塞队列 BlockingQueue 生产者消费者模型 。
    • 关系:生产者之间互斥、消费者之间互斥,生产者与消费者之间互斥 + 同步;
    • 原理:队列满生产者阻塞,队列空消费者阻塞,用条件变量等待,不忙轮询,节省 CPU;
    • 优点:解耦生产消费、平滑速度差异、并发执行业务,仅操作队列临界区串行。
  3. POSIX 信号量(semaphore) 信号量本质是原子计数器 ,用于资源计数同步。sem_init初始化(第二个参数 0 代表线程间共享);sem_wait(P操作)申请资源,计数器减 1,资源不足则阻塞;sem_post(V操作)释放资源,计数器加 1;sem_destroy销毁。常用于环形队列生产者消费者模型 :设置两个信号量,空白位置sem_blank(生产者关心),数据sem_data(消费者关心)。
    • 单生产单消费:只需要信号量做同步,不需要生产者间、消费者间互斥锁;
    • 多生产多消费:除信号量同步,额外增加互斥锁,保证多个生产者 / 多个消费者之间互斥访问队列下标。
  4. 信号量与互斥锁顺序辨析 生产者消费者环形队列:先 P 申请信号量,再竞争互斥锁(方案 2)是正确写法。信号量预先预定资源,只有拿到资源资格后,才去竞争锁操作临界区,锁持有时间短,并发效率更高。 如果先拿锁、再申请信号量(方案 1):线程抢到锁后,还可能因资源不足阻塞在信号量,锁被长时间占用,其他线程全部阻塞,效率低。类比电影院:方案 2 先预定票源,再排队办理;方案 1 先占柜台,再看有没有票。
相关推荐
邪修king3 小时前
Re:Linux 系统篇(三十五):进程间通信Chapter2:从匿名管道到进程池:Linux 进程间通信实战(深度理解「先描述,再组织」)
java·linux·运维
拆房老料4 小时前
【第四部分:Agentic 智能体设计模式】16. 一个 Prompt 搞不定复杂任务?试试 Prompt Chaining
java·前端·人工智能·设计模式·prompt
张张123y5 小时前
后端第十四课:Agent 记忆系统(让 Agent 记住上下文和用户,跨会话不健忘)
java·大数据·人工智能·python·agent·记忆
阿里嘎多学长9 小时前
2026-10-20 GitHub 热点项目精选
开发语言·程序员·github·代码托管
曹牧9 小时前
C#:空合并运算符
java·前端·数据库
布吉岛的石头9 小时前
Java 程序员第 50 阶段2:预训练的目标函数:自回归语言建模本质
java·开发语言·人工智能·深度学习·回归·transformer
golang学习记10 小时前
IDEA 2026年的玩法已经完全不同
java·ide·intellij-idea
小羊没烦恼!10 小时前
铁道部新客票系统设计(二)
开发语言·前端·数据库·python·oracle·c#·php
紧跟先前的步伐10 小时前
Go AI Agent 实战 05|Temperature 与生成参数:让模型输出可控
java·人工智能·golang