基础知识

RAII

RAII 即 "Resource Acquisition Is Initialization",是一种重要的C++编程思想和资源管理技术。它巧妙地利用了C++对象生命周期的自动管理特性,以保证在程序中资源被可靠地管理和释放。下面对 RAII 做一个详细的分析:

核心思想

  • 资源与对象的生命周期绑定RAII 的核心在于将资源(如内存、文件句柄、网络连接等)的控制与C++对象生命周期结合起来。这意味着,只要对象存在,资源就一直保持可用状态;当对象超出作用域之后,其析构函数自动释放资源。

机制和好处

  1. 构造函数分配资源:当我们创建一个对象时,其构造函数负责分配和初始化所需的资源。

  2. 析构函数释放资源:当对象的生命周期结束(即超出作用域)时,C++会自动调用析构函数来释放资源。

  3. 异常安全性:通过RAII,资源的使用是异常安全的。即使在异常抛出时,析构函数也会被自动调用,从而确保资源被正确释放,避免资源泄漏。

  4. 简化代码:RAII减少了显式资源管理的复杂性,开发者不需要手动释放资源,这种自动管理能使代码更加清晰、可维护。

RAII常见应用

  1. 智能指针

  • std::unique_ptr: 独占所有权的智能指针,在使用完毕后自动销毁托管的对象。

  • std::shared_ptr: 共享所有权的智能指针,使用引用计数,当最后一个std::shared_ptr被销毁时,托管的对象也会被释放。

  1. 标准库容器(如std::vector, std::map等):

  • 这些容器内部也使用RAII来管理其元素内存,减少内存泄漏的可能。

  1. 文件和互斥锁管理

  • std::fstream在打开的文件结束其作用域时自动关闭。

  • std::lock_guardstd::unique_lock自动管理锁的获取和释放。

实例

智能指针是RAII最经典的例子,比如:

#include <iostream>
#include <memory> // for std::unique_ptr and std::make_unique

void example() {
    // 使用 std::make_unique 创建一个管理整型的 unique_ptr
    std::unique_ptr<int> ptr = std::make_unique<int>(42); // 在此分配资源
    
    // 使用 ptr 指向的对象
    std::cout << "Value: " << *ptr << std::endl; // 输出指针所指向的值,即 42
    
    // 修改 ptr 指向的对象
    *ptr = 100; 
    std::cout << "Updated Value: " << *ptr << std::endl; // 输出更新后的值,即 100
    
    // 在此不需要显式删除,超出范围(函数结束时),内存自动释放
}

int main() {
    example();  // 调用 example 函数
    return 0;
}

在这个例子中,std::unique_ptr通过RAII的方式确保即使函数结束或发生异常,内存都会被安全释放,避免了内存泄漏。

总结

RAII 是一种高级但非常实用的设计模式,通过绑定资源管理到对象生命周期,简化了C++程序资源管理的复杂性,提高了代码的安全性和可维护性。它是现代C++程序设计的重要基石。

互斥锁

互斥锁(Mutex)是多线程编程中常用的同步机制,用于保护共享资源的访问,确保同一时间只有一个线程能够访问该资源。以下是对互斥锁的详细分析,包括工作原理、常用函数以及代码实例。

工作原理

在多线程环境中,如果多个线程试图同时访问共享资源,可能会导致数据竞争和不一致性。互斥锁的设计目的是确保同一时间内只有一个线程能够访问关键代码段。它的基本工作流程如下:

  1. 加锁: 当线程需要访问共享资源时,会调用 pthread_mutex_lock 函数尝试获取锁。如果锁已被其他线程持有,则当前线程会被阻塞,直到锁可用。

  2. 关键代码段: 一旦成功获取锁,线程便进入保护的关键代码段,安全地访问共享资源。

  3. 解锁: 当线程完成对共享资源的操作后,会调用 pthread_mutex_unlock 函数释放锁,这将允许其他被阻塞的线程获取该锁。

  4. 错误处理: 互斥锁的操作可能会失败,因此每个操作后都应检查返回值,并根据返回的错误码进行适当处理。

常用函数

以下是互斥锁相关的常用函数:

  1. pthread_mutex_init: 初始化互斥锁。

  2. pthread_mutex_destroy: 销毁互斥锁,释放资源。

  3. pthread_mutex_lock: 加锁,阻塞当前线程直到锁可用。

  4. pthread_mutex_unlock: 解锁,允许其他等待的线程获取锁。

返回值:上面这些函数在调用成功时返回 0;如果失败,则返回相应的错误码 (errno)。

代码实例

以下是一个使用互斥锁的简单示例,展示了如何保护共享资源。

#include <iostream>
#include <thread>
#include <pthread.h>
#include <vector>

const int NUM_THREADS = 5; // 线程数量
pthread_mutex_t mutex; // 定义互斥锁
int sharedResource = 0; // 共享资源

void increment(int id) {
    for (int i = 0; i < 10; ++i) {
        // 加锁
        if (pthread_mutex_lock(&mutex) != 0) {
            std::cerr << "Thread " << id << " failed to lock mutex." << std::endl;
            return;
        }

        // 关键代码段
        ++sharedResource; // 修改共享资源
        std::cout << "Thread " << id << " incremented resource to: " << sharedResource << std::endl;

        // 解锁
        if (pthread_mutex_unlock(&mutex) != 0) {
            std::cerr << "Thread " << id << " failed to unlock mutex." << std::endl;
        }
    }
}

int main() {
    pthread_mutex_init(&mutex, nullptr); // 初始化互斥锁

    std::vector<std::thread> threads;
    
    // 创建多个线程
    for (int i = 0; i < NUM_THREADS; ++i) {
        threads.emplace_back(increment, i);
    }

    // 等待所有线程完成
    for (auto& thread : threads) {
        thread.join();
    }

    pthread_mutex_destroy(&mutex); // 销毁互斥锁

    std::cout << "Final value of shared resource: " << sharedResource << std::endl;
    return 0;
}

代码分析

  1. 初始化互斥锁:

  • main 函数中,调用 pthread_mutex_init(&mutex, nullptr); 初始化互斥锁。第二个参数用于设置互斥锁的属性,通常设置为 nullptr 表示使用默认属性。

  1. 创建线程:

  • 使用 std::thread 创建多个线程,每个线程都调用 increment 函数。

  1. 加锁:

  • increment 函数中,调用 pthread_mutex_lock(&mutex); 对互斥锁加锁。如果加锁失败,打印错误信息并返回。

  1. 关键代码段:

  • 线程进入关键代码段,安全地增加 sharedResource 的值,并打印当前值。

  1. 解锁:

  • 调用 pthread_mutex_unlock(&mutex); 解锁互斥量。成功解锁后,其他等待该互斥锁的线程将有机会获取锁并执行。

  1. 销毁互斥锁:

  • 调用 pthread_mutex_destroy(&mutex); 销毁互斥锁并释放占用的资源。

结论

互斥锁是多线程编程中重要的同步工具。通过合理的加锁和解锁机制,它可以有效防止数据竞争和不一致性。使用互斥锁的代码逻辑更加清晰且容易管理,同时在异常情况下能较好地保持资源的安全性和完整性。在实际应用中,确保每个互斥锁都得到适当的初始化、保护和销毁是非常重要的。

信号量

g++ -std=c++11 -pthread 01信号量实现生产者消费者.cc -o your_program

信号量是一种用于控制对共享资源访问同步机制,尤其在多线程与多进程编程中极为重要。信号量的两个主要操作——P(等待)操作和V(信号)操作,使得多个线程或进程可以安全地共享资源。以下是对信号量的详细分析:

信号量的基本概念

  • 信号量值:信号量的值通常是一个非负整数,代表当前可被访问的资源数量。

  • P操作(等待)

  • 本质上是尝试获取资源。如果信号量的值大于0,则将其值减1,表示成功获取一个资源。

  • 如果信号量的值为0,表示没有可用资源,当前线程或进程将进入阻塞状态,直到资源可用。

  • V操作(信号)

  • 本质上是释放资源或者通知资源可用。如果有线程或进程因信号量的值为0而等待,执行V操作将唤醒其中一个等待的实体。

  • 如果没有等待的线程或进程,信号量的值则增加1,表示增加一个可用资源。

类型

  1. 计数信号量

  • 可以具有大于1的值,适用于允许多个线程同时访问多个相同资源的场景。

  1. 二进制信号量

  • 取值仅为0或1,类似于互斥锁,通常用于简单的同步,例如保证一次只有一个线程在执行某段关键代码。

信号量的应用

  • 资源管理:通过信号量计数值,可以很方便地管理对资源池的访问,比如连接池或者任务队列。

  • 同步控制:在多线程环境中,通过信号量可以强制性地控制线程执行的顺序。

常用信号量函数

  1. sem_init

  • 初始化一个未命名的信号量。

  • 原型:int sem_init(sem_t *sem, int pshared, unsigned int value);

  • pshared: 0表示信号量用于线程间同步,非0表示用于进程间。

  • value: 初始信号量值。

  1. sem_destroy

  • 销毁一个信号量并释放其资源,不应该在有进程或线程等待的信号量上调用。

  • 原型:int sem_destroy(sem_t *sem);

  1. sem_wait

  • P操作,信号量值减1。如果该值为0,会阻塞直到信号量值增加。

  • 原型:int sem_wait(sem_t *sem);

  1. sem_post

  • V操作,信号量值加1。如果有线程/进程阻塞,会唤醒其中一个。

  • 原型:int sem_post(sem_t *sem);

实际使用中的例子

信号量通常用于解决生产者-消费者问题、哲学家就餐问题等经典同步问题。通过信号量,能够确保各个线程合理且安全地访问共享资源,避免竞争条件和死锁等问题。

#include <iostream>
#include <thread>
#include <semaphore.h>
#include <vector>
#include <queue>
#include <chrono>
#include <random>
#include <mutex>

const int BUFFER_SIZE = 5; // 缓冲区大小
std::queue<int> buffer; // 缓冲区
sem_t empty; // 表示缓冲区空的信号量
sem_t full; // 表示缓冲区满的信号量
std::mutex mutex; // 用于访问缓冲区的互斥量

// 生产者线程
void producer(int id) {
    for (int i = 0; i < 10; ++i) {
        std::this_thread::sleep_for(std::chrono::milliseconds(rand() % 100)); // 模拟生产时间
        int item = rand() % 100; // 生成一个随机数据项

        sem_wait(&empty); // 等待缓冲区有空位
        {
            std::lock_guard<std::mutex> lock(mutex); // 进入临界区
            buffer.push(item); // 将数据放入缓冲区
            std::cout << "Producer " << id << " produced: " << item << std::endl;
        }
        sem_post(&full); // 增加已填充的信号量
    }
}

// 消费者线程
void consumer(int id) {
    for (int i = 0; i < 10; ++i) {
        std::this_thread::sleep_for(std::chrono::milliseconds(rand() % 100)); // 模拟消费时间

        sem_wait(&full); // 等待缓冲区有数据
        {
            std::lock_guard<std::mutex> lock(mutex); // 进入临界区
            int item = buffer.front(); // 从缓冲区获取数据
            buffer.pop(); // 移除数据
            std::cout << "Consumer " << id << " consumed: " << item << std::endl;
        }
        sem_post(&empty); // 增加空位信号量
    }
}

int main() {
    srand(time(NULL)); // 随机数种子

    // 初始化信号量
    sem_init(&empty, 0, BUFFER_SIZE); // 初始空位数量
    sem_init(&full, 0, 0); // 初始满位数量为0

    std::vector<std::thread> producers;
    std::vector<std::thread> consumers;

    // 创建生产者和消费者线程
    for (int i = 0; i < 2; ++i) {
        producers.emplace_back(producer, i);
        consumers.emplace_back(consumer, i);
    }

    // 等待所有线程完成
    for (auto& producer : producers) {
        producer.join();
    }
    for (auto& consumer : consumers) {
        consumer.join();
    }

    // 销毁信号量
    sem_destroy(&empty);
    sem_destroy(&full);

    return 0;
}

总结

信号量是强有力的同步原语,它通过P和V操作自动管理共享资源的分配,是多线程编程,或进程间通信的重要组成部分。信号量不仅可以用来实现锁机制,也可以实现复杂的事件通知和资源管理策略。

条件变量

条件变量(Condition Variable)是一种重要的线程同步机制,通常与互斥锁(mutex)结合使用,来管理多线程程序中共享数据的访问。它在某些条件满足时,负责通知一个或多个线程继续执行,从而避免在忙等待中浪费资源。

关键函数分析

  1. pthread_cond_init:用于初始化条件变量。该函数通常在程序初始化时调用,以准备条件变量供使用。

  2. pthread_cond_destroy:用于销毁条件变量。当条件变量不再需要使用时调用此函数,以释放其占用的系统资源。

  3. pthread_cond_broadcast:通过广播的方式唤醒所有正在等待同一个条件变量的线程。适用于当条件变化影响所有等待线程的场景。

  4. pthread_cond_wait:使当前线程等待条件变量。此函数要求一个已加锁的互斥锁作为参数。在调用pthread_cond_wait时,它会自动解锁传入的互斥锁,并将线程置于等待队列。当线程被唤醒并返回时,互斥锁会被重新加锁。

使用实例

以下是一个简单的生产者-消费者模型,用来展示条件变量的用法。在该模型中,消费者线程等待生产者线程生产某个资源后再继续运行。

#include <iostream>
#include <pthread.h>
#include <queue>
#include <unistd.h>

std::queue<int> buffer; // 共享数据缓冲区
const unsigned int MAX_BUFFER_SIZE = 10;
pthread_mutex_t mutex;  // 互斥锁
pthread_cond_t cond;    // 条件变量

void* producer(void* arg) {
    for (int i = 0; i < 20; ++i) {  // 生产者生产20个产品
        pthread_mutex_lock(&mutex); // 加锁
        while (buffer.size() == MAX_BUFFER_SIZE) {
            pthread_cond_wait(&cond, &mutex); // 缓冲区满则等待
        }
        buffer.push(i); // 生产产品
        std::cout << "Produced: " << i << std::endl;
        pthread_cond_broadcast(&cond); // 唤醒所有消费者
        pthread_mutex_unlock(&mutex); // 解锁
        sleep(1);
    }
    return nullptr;
}

void* consumer(void* arg) {
    for (int i = 0; i < 20; ++i) {  // 消费者消费20个产品
        pthread_mutex_lock(&mutex); // 加锁
        while (buffer.empty()) {
            pthread_cond_wait(&cond, &mutex); // 缓冲区空则等待
        }
        int item = buffer.front(); // 消费产品
        buffer.pop();
        std::cout << "Consumed: " << item << std::endl;
        pthread_cond_broadcast(&cond); // 唤醒所有生产者
        pthread_mutex_unlock(&mutex); // 解锁
        sleep(1);
    }
    return nullptr;
}

int main() {
    pthread_t prodThread, consThread;
    pthread_mutex_init(&mutex, nullptr);
    pthread_cond_init(&cond, nullptr);

    pthread_create(&prodThread, nullptr, producer, nullptr);
    pthread_create(&consThread, nullptr, consumer, nullptr);

    pthread_join(prodThread, nullptr);
    pthread_join(consThread, nullptr);

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

    return 0;
}

代码说明

  • 生产者线程在缓冲区未满时,将生产的产品放入缓冲区,并使用pthread_cond_broadcast唤醒等待该条件变量的所有线程。

  • 消费者线程在缓冲区为空时,调用pthread_cond_wait进入等待状态;当产品有库存时,消费产品,并再次通知生产者。

  • 互斥锁(mutex)确保了对缓冲区的同步访问,防止同时读写引发的竞态条件。

通过这样的模式,可以实现高效的线程间通信和同步,避免轮询带来的资源浪费。

功能

锁机制的功能

实现多线程同步,通过锁机制,确保任一时刻只能有一个线程能进入关键代码段.

封装的功能

在现代 C++ 编程中,通过封装信号量和条件变量以实现更简洁和异常安全的同步结构是一种常见且重要的设计模式。下面详细分析这些封装的设计理念,并提供完整的代码实例。

信号量 (sem 类) 的封装

信号量是一种同步原语,用于控制对公共资源的访问。通过封装信号量,我们可以提高代码的异常安全性,简化使用流程。

信号量封装代码

#include <iostream>
#include <thread>
#include <vector>
#include <queue>
#include <chrono>
#include <random>
#include <mutex>
#include <exception>
#include <semaphore.h>

// 信号量类封装
class sem {
public:
    sem(unsigned int value = 0) {
        if (sem_init(&m_sem, 0, value) != 0) {
            throw std::exception();
        }
    }

    ~sem() {
        sem_destroy(&m_sem);
    }

    void wait() {
        sem_wait(&m_sem);
    }

    void signal() {
        sem_post(&m_sem);
    }

private:
    sem_t m_sem;
};

const int BUFFER_SIZE = 5; // 缓冲区大小
std::queue<int> buffer;    // 缓冲区
sem empty(BUFFER_SIZE);    // 表示缓冲区空的信号量,初始值为BUFFER_SIZE
sem full(0);               // 表示缓冲区满的信号量,初始值为0
std::mutex mutex;          // 用于访问缓冲区的互斥量

// 生产者线程
void producer(int id) {
    for (int i = 0; i < 10; ++i) {
        std::this_thread::sleep_for(std::chrono::milliseconds(rand() % 100)); // 模拟生产时间
        int item = rand() % 100; // 生成一个随机数据项

        empty.wait(); // 等待缓冲区有空位
        {
            std::lock_guard<std::mutex> lock(mutex); // 进入临界区
            buffer.push(item); // 将数据放入缓冲区
            std::cout << "Producer " << id << " produced: " << item << std::endl;
        }
        full.signal(); // 增加已填充的信号量
    }
}

// 消费者线程
void consumer(int id) {
    for (int i = 0; i < 10; ++i) {
        std::this_thread::sleep_for(std::chrono::milliseconds(rand() % 100)); // 模拟消费时间

        full.wait(); // 等待缓冲区有数据
        {
            std::lock_guard<std::mutex> lock(mutex); // 进入临界区
            int item = buffer.front(); // 从缓冲区获取数据
            buffer.pop(); // 移除数据
            std::cout << "Consumer " << id << " consumed: " << item << std::endl;
        }
        empty.signal(); // 增加空位信号量
    }
}

int main() {
    srand(time(NULL)); // 随机数种子

    std::vector<std::thread> producers;
    std::vector<std::thread> consumers;

    // 创建生产者和消费者线程
    for (int i = 0; i < 3; ++i) {
        producers.emplace_back(producer, i);
        consumers.emplace_back(consumer, i);
    }

    // 等待所有线程完成
    for (auto& producer : producers) {
        producer.join();
    }
    for (auto& consumer : consumers) {
        consumer.join();
    }

    return 0;
}

设计特点

  • RAII机制:在类构造函数中完成信号量的初始化(sem_init),在析构函数中自动销毁信号量(sem_destroy),从而保证资源的自动管理。

  • 异常安全:在构造函数中检测信号量初始化失败,使用 C++ 的异常机制来安全处理错误,防止资源泄漏。

  • 封装基本操作:提供简单的接口 waitsignal,无须直接调用底层信号量函数。

条件变量 (cond_var 类) 的封装

条件变量用于实现复杂的线程间通信,需要结合互斥锁使用。通过封装条件变量操作,我们可以简化使用并提高线程安全性。

条件变量封装代码

#include <iostream>
#include <thread>
#include <vector>
#include <queue>
#include <chrono>
#include <random>
#include <mutex>
#include <pthread.h>
#include <exception>

// 封装的条件变量类
class cond_var {
public:
    cond_var() {
        if (pthread_mutex_init(&m_mutex, nullptr) != 0 || pthread_cond_init(&m_cond, nullptr) != 0) {
            throw std::exception(); // 初始化失败时抛出异常
        }
    }

    ~cond_var() {
        pthread_mutex_destroy(&m_mutex); // 销毁互斥锁
        pthread_cond_destroy(&m_cond);   // 销毁条件变量
    }

    void wait(std::unique_lock<std::mutex>& lock) {
        lock.unlock(); // 解锁前的锁
        pthread_mutex_lock(&m_mutex); // 加锁,保护等待操作
        pthread_cond_wait(&m_cond, &m_mutex); // 等待条件变量触发
        pthread_mutex_unlock(&m_mutex); // 解锁
        lock.lock(); // 重新锁定外部锁
    }

    void signal() {
        pthread_cond_signal(&m_cond); // 唤醒一个等待线程
    }

    void broadcast() {
        pthread_cond_broadcast(&m_cond); // 唤醒所有等待线程
    }

private:
    pthread_mutex_t m_mutex;
    pthread_cond_t m_cond;
};

// 常量定义
const int BUFFER_SIZE = 5;

// 共享资源及同步原语
std::queue<int> buffer;
cond_var buffer_not_full;
cond_var buffer_not_empty;
std::mutex buffer_mutex;

// 生产者线程函数
void producer(int id) {
    for (int i = 0; i < 10; ++i) {
        std::this_thread::sleep_for(std::chrono::milliseconds(rand() % 100)); // 模拟生产时间
        int item = rand() % 100; // 生成一个随机数据项

        std::unique_lock<std::mutex> lock(buffer_mutex);
        while (buffer.size() == BUFFER_SIZE) {
            buffer_not_full.wait(lock); // 等待缓冲区未满
        }
        buffer.push(item);
        std::cout << "Producer " << id << " produced: " << item << std::endl;
        buffer_not_empty.signal(); // 通知消费者缓冲区非空
    }
}

// 消费者线程函数
void consumer(int id) {
    for (int i = 0; i < 10; ++i) {
        std::this_thread::sleep_for(std::chrono::milliseconds(rand() % 100)); // 模拟消费时间

        std::unique_lock<std::mutex> lock(buffer_mutex);
        while (buffer.empty()) {
            buffer_not_empty.wait(lock); // 等待缓冲区非空
        }
        int item = buffer.front();
        buffer.pop();
        std::cout << "Consumer " << id << " consumed: " << item << std::endl;
        buffer_not_full.signal(); // 通知生产者缓冲区未满
    }
}

int main() {
    srand(time(NULL)); // 设置随机数种子

    std::vector<std::thread> producers;
    std::vector<std::thread> consumers;

    // 创建生产者和消费者线程
    for (int i = 0; i < 3; ++i) {
        producers.emplace_back(producer, i);
        consumers.emplace_back(consumer, i);
    }

    // 等待所有线程结束
    for (auto& producer : producers) {
        producer.join();
    }
    for (auto& consumer : consumers) {
        consumer.join();
    }

    return 0;
}

设计特点

  • 简化操作流程:封装了加锁、解锁及条件变量等待过程,使调用更加直观和安全。

  • 安全性:确保所有条件变量操作都在加锁状态下进行,避免数据竞争和同步问题。

  • 代码复用性:统一了条件变量相关操作接口,提升代码可读性和易维护性。

总结

通过对信号量和条件变量的封装,我们实现了以下目标:

  1. RAII原则:自动管理资源生命周期,确保在程序运行期间不发生资源泄漏。

  2. 简化流程:调用变得更加直观和清晰,降低开发复杂度。

  3. 可靠性和可维护性:统一的接口封装减少了潜在的同步错误,增强了多线程程序的健壮性。

这些封装设计实践大大提高了多线程编程的代码质量和开发效率,特别适用于需要高可靠性和可维护性的应用程序。