C++ I/O与进程同步

目录

C++条件变量

使用条件变量的步骤

重点问题

线程同步和线程异步

互斥量

读写锁

RAII

信号量

屏障

std::call_once

互斥信号量

std::thread

std::async

I/O操作

标准输入和输出

重定向

清除输入缓冲区


本文不分析具体实现原理,《Linux线程学习》对原理层进行学习,该处重点学习C++11下的接口使用


C++条件变量

std::condition_variable线程间同步机制,允许线程在某个条件不满足的时候等待,直到其他线程通知该条件满足,线程才会继续执行。

使用条件变量的步骤

创建条件变量和互斥锁

std::mutex mtx;
std::condition_variable cv;
bool ready = false;

等待线程 

  • std::unique_lock 用于管理互斥锁的生命周期。
  • cv.wait(lock, []{ return ready; }); 会在 readytrue 时解除阻塞,否则当前线程会一直处于等待状态
void wait_for_ready() {
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, []{ return ready; });
    // 当ready为true时继续执行
    std::cout << "Thread is ready to proceed." << std::endl;
}

通知线程

  • std::lock_guard 确保在设置 ready 变量时互斥锁被持有
  • cv.notify_one() 通知一个正在等待的线程(如果有的话)条件已经满足。cv.notify_all()通知所有等待的线程
void set_ready() {
    {
        std::lock_guard<std::mutex> lock(mtx);
        ready = true;
    }
    cv.notify_one(); // 或 cv.notify_all();
}

创建10个线程,等待read条件变为true,然后通知所有线程运行实现设置好的函数

  • 条件变量要和互斥锁配合使用,保证访问共享资源的时候线程安全
  • notify_one()notify_all() 不会自动释放锁,它们只会通知等待线程去重新竞争锁
  • 如果通知时没有线程在等待,通知会被“丢弃

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>

std::mutex mtx;
std::condition_variable cv;
bool ready = false;

void print_id(int id) {
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, [] { return ready; }); // 等待 ready 变为 true
    std::cout << "Thread " << id << std::endl;
}

void go() {
    std::lock_guard<std::mutex> lock(mtx);
    ready = true;
    cv.notify_all(); // 通知所有线程
}

int main() {
    std::thread threads[10];
    // 创建 10 个线程
    for (int i = 0; i < 10; ++i)
        threads[i] = std::thread(print_id, i);

    std::cout << "10 threads ready to race..." << std::endl;
    go(); // 设置 ready 为 true 并通知所有线程

    for (auto& th : threads) th.join();

    return 0;
}

重点问题

条件变量是什么?C++中的作用是什么?

  • 条件变量是 C++ 中用于线程间同步的机制,通常与互斥锁一起使用。它允许一个或多个线程在某个条件不满足时进入等待状态,直到其他线程通知条件满足。这样可以避免忙等待,提高程序的效率和可扩展性

 条件变量与互斥锁的关系是什么?为什么需要一起使用

  • 条件变量必须与互斥锁一起使用,因为条件变量在检查和修改共享数据时,需要确保线程安全。互斥锁用于保护共享数据的访问,防止多个线程同时修改数据而导致数据竞争。在等待条件满足时,线程会先锁定互斥锁,然后进入等待状态。当条件满足时,线程会重新竞争互斥锁以继续执行

 std::condition_variablewait 函数是如何工作的

  • wait 函数接收一个 std::unique_lock<std::mutex> 对象,并在该对象锁定的情况下调用。wait 会使当前线程进入等待状态,直到其他线程调用 notify_onenotify_all 并且指定的条件满足。wait 函数会自动释放互斥锁以允许其他线程修改条件,然后在被唤醒后重新获得互斥锁

 什么是“虚假唤醒”?如何应对这种情况

  • “虚假唤醒”是指线程在没有明显原因的情况下被唤醒。为了应对虚假唤醒,通常在调用 wait 函数时使用一个条件判断的循环。例如,cv.wait(lock, []{ return ready; });,这样可以确保线程在条件未满足的情况下继续等待,而不是直接继续执行

notify_onenotify_all 有什么区别?在什么情况下使用?

  •  notify_one 唤醒一个等待的线程,notify_all 唤醒所有等待的线程。如果只有一个线程需要被唤醒,使用 notify_one 更加高效;而如果多个线程需要同时继续执行,则使用 notify_all。选择使用哪个取决于具体的应用场景和条件逻辑

线程同步和线程异步

  • 线程同步:协调多个线程对共享资源的访问,确保同一时间只有一个线程可以访问到共享资源,避免数据竞争。常用的有互斥量、条件变量、信号量等
  • 线程异步:允许多个线程独立执行,不需要同步,也就是线程的执行顺序不确定,而且其他可能不会等待其他线程完成

互斥量

互斥量用于保护临界资源,确保一次只有一个线程可以访问资源

#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx;

void print_thread_id(int id) {
    mtx.lock();
    std::cout << "Thread " << id << std::endl;
    mtx.unlock();
}

int main() {
    std::thread threads[10];
    for (int i = 0; i < 10; ++i) {
        threads[i] = std::thread(print_thread_id, i);
    }
    for (auto& th : threads) th.join();
    return 0;
}

 为什么需要使用互斥量?

  • 互斥量用于防止多个线程同时访问共享资源时发生数据竞争,从而确保数据的一致性

 解释递归互斥量的应用场景

  • 递归互斥量允许同一线程多次加锁,而不会导致死锁。这在递归函数或在同一线程中多次需要锁的情况下非常有用

读写锁

允许多个线程同时读取数据,但是写操作只可以被一个线程独占(shared_mtx)

#include <iostream>
#include <thread>
#include <shared_mutex>

std::shared_mutex shared_mtx;

void read_data(int id) {
    std::shared_lock<std::shared_mutex> lock(shared_mtx);
    std::cout << "Thread " << id << " is reading" << std::endl;
}

void write_data(int id) {
    std::unique_lock<std::shared_mutex> lock(shared_mtx);
    std::cout << "Thread " << id << " is writing" << std::endl;
}

int main() {
    std::thread reader1(read_data, 1);
    std::thread reader2(read_data, 2);
    std::thread writer(write_data, 3);

    reader1.join();
    reader2.join();
    writer.join();
    return 0;
}

RAII

一种资源管理方式,常用于包装互斥量,确保资源在对象生命周期结束的时候被释放

#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx;

void print_thread_id(int id) {
    std::lock_guard<std::mutex> lock(mtx);
    std::cout << "Thread " << id << std::endl;
}

int main() {
    std::thread threads[10];
    for (int i = 0; i < 10; ++i) {
        threads[i] = std::thread(print_thread_id, i);
    }
    for (auto& th : threads) th.join();
    return 0;
}

 RAII 在 C++ 中的重要性

  • RAII 确保资源在对象生命周期结束时自动释放,防止资源泄漏。在多线程编程中,RAII 可以自动管理锁的释放,避免因异常或提前返回导致的死锁

信号量

信号量用于控制对资源访问的及计数器,C++20引入 std::conuting_semaphore 和 std::binary_semaphore

#include <iostream>
#include <thread>
#include <semaphore.h>

std::counting_semaphore<3> sem(3);

void task(int id) {
    sem.acquire();
    std::cout << "Thread " << id << " is in critical section" << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "Thread " << id << " is leaving critical section" << std::endl;
    sem.release();
}

int main() {
    std::thread threads[5];
    for (int i = 0; i < 5; ++i) {
        threads[i] = std::thread(task, i);
    }
    for (auto& th : threads) th.join();
    return 0;
}

信号量和互斥量的主要区别

  • 互斥量用于独占访问资源,而信号量可以控制多个线程同时访问有限资源。信号量是计数器,允许指定数量的线程同时访问共享资源,而互斥量通常只允许一个线程访问

屏障

用于同步多个线程,使得所有线程在某个点上等待,直到所有线程都到达改点后再继续执行

#include <iostream>
#include <thread>
#include <barrier>

std::barrier sync_point(3);

void task(int id) {
    std::cout << "Thread " << id << " is working" << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));
    sync_point.arrive_and_wait();
    std::cout << "Thread " << id << " is continuing after barrier" << std::endl;
}

int main() {
    std::thread t1(task, 1);
    std::thread t2(task, 2);
    std::thread t3(task, 3);

    t1.join();
    t2.join();
    t3.join();

    return 0;
}

什么是屏障?它在多线程编程中的作用是什么?

  • 屏障用于同步多个线程,使所有线程在某个点上等待,直到所有线程都到达该点。它在并行算法中很有用,当需要确保多个线程同步执行到某个步骤时,可以使用屏障

std::call_once

保证某个初始化操作只会执行一次,无论多少个线程调用它

#include <iostream>
#include <thread>
#include <mutex>

std::once_flag flag;

void init() {
    std::cout << "Initialized" << std::endl;
}

void task() {
    std::call_once(flag, init);
    std::cout << "Thread is running" << std::endl;
}

int main() {
    std::thread t1(task);
    std::thread t2(task);

    t1.join();
    t2.join();

    return 0;
}

 std::call_once 的典型应用场景是什么

  • std::call_once 主要用于确保某个初始化操作(如单例模式的初始化)只执行一次。这在多线程环境中避免了多次初始化导致的竞态条件和资源浪费

互斥信号量

C++20 引入了 std::binary_semaphore,它是一种特殊的信号量,用于控制对共享资源的独占访问。互斥信号量类似于互斥量,但它更通用,可以用于更广泛的同步场景。互斥信号量要么是 0(锁定状态),要么是 1(解锁状态),这使得它能够保证一次只有一个线程访问共享资源

#include <iostream>
#include <thread>
#include <semaphore>

std::binary_semaphore sem(1);  // 初始化为 1,表示解锁状态

void critical_section(int id) {
    sem.acquire();  // 获取信号量,进入临界区
    std::cout << "Thread " << id << " is in critical section" << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));  // 模拟工作
    std::cout << "Thread " << id << " is leaving critical section" << std::endl;
    sem.release();  // 释放信号量,离开临界区
}

int main() {
    std::thread t1(critical_section, 1);
    std::thread t2(critical_section, 2);

    t1.join();
    t2.join();

    return 0;
}

互斥信号量和互斥量有什么区别

  • 互斥信号量和互斥量都用于控制对共享资源的独占访问。区别在于互斥信号量更通用,它不仅可以用于二元的锁定/解锁,还可以扩展为计数信号量,用于控制多个线程对资源的并发访问。而互斥量通常只用于控制单一线程访问资源。此外,信号量的 acquire 和 release 操作是独立的,而互斥量的 lock 和 unlock 操作必须成对出现

 在什么场景下你会选择使用互斥信号量而不是互斥量

  • 如果仅需要控制对共享资源的独占访问,那么互斥量通常是更简洁的选择。然而,如果需要更灵活的同步控制,例如将信号量作为事件通知机制或需要在多个不同函数中管理临界区的进入和退出(acquire 和 release 不必在同一作用域中),那么互斥信号量会更合适

 什么是二元信号量

  • 二元信号量是一种信号量,它的取值只能是 0 或 1。它用于控制一个资源的独占访问,类似于互斥量。二元信号量可以看作是计数信号量的特例,其中最大计数为 1

 如何确保使用互斥信号量不会引起死锁

  • 确保所有线程以相同的顺序获取多个信号量。
  • 尽量减少临界区的代码,尽快释放信号量。
  • 避免嵌套获取多个信号量,除非非常必要。
  • 使用超时机制来获取信号量,如果无法获取,则可以选择放弃操作或重新尝试。

std::thread

C++11中引入用于创建和管理线程的类,前面的事例已经经常使用,此处不赘述。

如何启动一个线程

  • 创建一个thread对象,然后传递给一个可调用对象(函数指针、函数对象、lambda表达式都可以)来启动线程

 std::thread::join 的作用是什么

  • join() 用于阻塞调用它的线程,直到被 join 的线程完成执行。它确保当前线程等待子线程执行完毕后再继续。如果不调用 join()detach()std::thread 对象在析构时会终止程序

什么是 std::thread::detach

  • detach() 将线程与 std::thread 对象分离,使得线程在后台继续运行,而不需要同步或等待它完成。分离后的线程自行运行,不能再被 join,如果主线程先于分离的线程结束,程序将继续运行,直到分离的线程完成

std::async

启动异步任务的函数模版

#include <iostream>
#include <future>

int add(int a, int b) {
    return a + b;
}

int main() {
    std::future<int> result = std::async(std::launch::async, add, 5, 3);

    // 在这里可以执行其他操作

    int sum = result.get();  // 获取异步任务的返回值
    std::cout << "Sum: " << sum << std::endl;

    return 0;
}

std::asyncstd::thread 有什么区别

  • std::asyncstd::thread 都可以用于创建并发任务,但 std::async 更加灵活,它返回一个 std::future 对象,用于获取异步任务的结果。而 std::thread 不提供这种功能,线程结束后不会有返回值。std::async 还可以根据策略选择是否延迟执行或在调用 get() 时同步执行

 std::future 是什么

  • std::future 是 C++11 引入的一种同步机制,用于从异步操作中获取结果。通过 std::async 返回的 std::future 对象,你可以在稍后的时间点调用 get() 方法来获取异步操作的结果,get() 会阻塞直到结果可用

thread和async对比

  • std::thread 用于启动一个新的线程,适合需要手动管理线程生命周期和同步的场景。它简单直接,但不提供返回值和异常管理功能。

  • std::async 提供了一种更高级的异步任务管理方式,可以轻松地获取返回值并处理异常,适合需要返回结果或自动管理线程生命周期的场景

 std::asyncstd::launch 参数有什么作用?

  • std::async 的第二个参数是 std::launch,它用于控制异步任务的启动策略:

    • std::launch::async:强制任务在新线程中异步启动。
    • std::launch::deferred:任务在调用 get()wait() 时才会同步执行,不会创建新线程。
  • 默认情况下,可以同时使用这两个选项,如 std::launch::async | std::launch::deferred,这样由系统决定以哪种方式启动任务

I/O操作

标准输入和输出

常用操作总结

  • std::cin:标准输入流,通常从键盘接收数据。
  • std::cout:标准输出流,通常将数据输出到控制台。
  • std::cerr:标准错误输出流,通常用于输出错误信息
#include <iostream>

int main() {
    int number;
    std::cout << "Enter a number: ";  // 输出提示信息
    std::cin >> number;  // 从标准输入读取一个整数
    std::cout << "You entered: " << number << std::endl;  // 输出读取的整数
    return 0;
}

重定向

输入、输出信息指定输出到指定的设备或者文件中(下文事例中指定输出到文件中)

#include <iostream>
#include <fstream>

int main() {
    std::ofstream outFile("output.txt");
    if (!outFile) {
        std::cerr << "Error opening file!" << std::endl;
        return 1;
    }

    std::streambuf* coutBuf = std::cout.rdbuf();  // 保存原始的缓冲区
    std::cout.rdbuf(outFile.rdbuf());  // 将 `std::cout` 的缓冲区重定向到文件

    std::cout << "This will be written to the file instead of the console." << std::endl;

    std::cout.rdbuf(coutBuf);  // 恢复原始缓冲区
    outFile.close();

    return 0;
}

如何将 std::cout 的输出重定向到文件?

  • 可以通过 std::cout.rdbuf() 函数将 std::cout 的缓冲区重定向到文件流的缓冲区。例如:std::cout.rdbuf(outFile.rdbuf()); 会将 std::cout 的输出重定向到文件 outFile

 如何将输入从文件重定向到 std::cin

  • 可以使用 std::cin.rdbuf()std::cin 的缓冲区重定向到文件流的缓冲区。例如:std::ifstream inFile("input.txt"); std::cin.rdbuf(inFile.rdbuf()); 将使 std::cininput.txt 文件读取输入

清除输入缓冲区

std::cin::ignore对缓冲区进行处理,处理输入缓冲区的所有内容,直到遇到换行符为止

#include <iostream>
#include <limits>

int main() {
    int number;

    std::cout << "Enter a number: ";
    std::cin >> number;

    // 清除输入缓冲区
    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');

    std::cout << "Enter another number: ";
    std::cin >> number;

    std::cout << "You entered: " << number << std::endl;

    return 0;
}
  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值