【编程规范具体案例(基于Qt、微软、谷歌和AUTOSAR C++14 参考)】 C++ 编码规范 之并发篇



规范参考总结了Qt、微软、谷歌和AUTOSAR C++14 等规范。

并发

目录

12. 并发编程规范

12.1 线程创建与管理规则

12.1.1 [必须] 明确定义线程的生命周期管理策略

  • 无特殊需求时使用RAII技术管理线程资源,如使用std::threadstd::jthread对象。
  • 明确定义线程的启动、运行和终止条件,避免线程无限运行。
  • 提供显式的线程启动和停止方法,而不是在构造函数或析构函数中管理线程。
  • 在长时间运行的线程中实现可中断机制,定期检查中断标志。
  • 在对象析构时确保所有关联线程安全终止。

示例:

class Worker {
    std::jthread worker;
    std::atomic<bool> stop_flag = false;

public:
    Worker(std::function<void(std::atomic<bool>&)> task) 
        : worker([this, task](std::stop_token stoken) {
            while (!stoken.stop_requested() && !stop_flag) {
                task(stop_flag);
            }
        }) {}

    ~Worker() {
        stop_flag = true;
        worker.request_stop();
    }
};

12.1.2 [必须] 为关键线程设置明确的标识符

  • 为所有长期运行的关键线程设置有意义的名称。
  • 临时线程或短期任务可以使用自动生成的标识符。
  • 在日志和调试输出中包含线程标识符,以便于问题追踪。

示例:

#include <thread>
#include <string>
#include <sstream>

void setThreadName(const std::string& name) {
#ifdef __APPLE__
    pthread_setname_np(name.c_str());
#elif defined(__linux__)
    pthread_setname_np(pthread_self(), name.c_str());
#elif defined(_WIN32)
    // Windows 的实现略有不同,这里省略
#endif
}

std::string getThreadName() {
    char name[16] = {0};  // 大多数系统限制线程名为 16 字符
#ifdef __APPLE__
    pthread_getname_np(pthread_self(), name, sizeof(name));
#elif defined(__linux__)
    pthread_getname_np(pthread_self(), name, sizeof(name));
#elif defined(_WIN32)
    // Windows 的实现略有不同,这里省略
#endif
    return std::string(name);
}

void criticalTask() {
    setThreadName("CriticalTask");
    // 线程的主要工作...
    log("Critical operation performed in thread: " + getThreadName());
}

void temporaryTask() {
    std::stringstream ss;
    ss << "TempTask-" << std::this_thread::get_id();
    setThreadName(ss.str());
    // 临时任务的工作...
    log("Temporary task completed in thread: " + getThreadName());
}

int main() {
    std::thread critical(criticalTask);
    critical.join();

    std::thread temp(temporaryTask);
    temp.join();

    return 0;
}

12.1.3 [必须] 在多线程环境中安全地处理异常

  1. 线程本地异常处理:
    • 每个线程必须在其自身范围内捕获并处理异常。
    • 禁止让异常跨越线程边界传播。
  2. 异常传递机制:
    • 使用 std::promisestd::future 在线程间传递异常信息。
    • 对于线程池或长期运行的线程,实现自定义的异常传递机制。
  3. 资源管理:
    • 使用 RAII 技术确保即使在发生异常时资源也能被正确释放。
    • 在异常处理程序中不要抛出新的异常;如果必须这样做,请使用 std::nested_exception
  4. 异常日志记录:
    • 实现线程安全的日志机制,记录所有捕获的异常。
    • 日志中包含异常类型、错误信息、线程ID和相关上下文信息。
  5. 致命错误处理:
    • 对于无法恢复的致命错误,实现安全的程序终止机制。
    • 使用 std::terminate_handler 自定义程序终止行为。

示例代码:

class ThreadSafeTask {
public:
    void run() {
        std::promise<void> promise;
        std::future<void> future = promise.get_future();

        std::thread worker([this, &promise]() {
            try {
                // 执行可能抛出异常的任务
                doWork();
                promise.set_value();
            } catch (...) {
                try {
                    // 捕获异常并通过 promise 传递
                    promise.set_exception(std::current_exception());
                } catch (...) {
                    // 处理 set_exception 可能抛出的异常
                    std::terminate();
                }
            }
        });

        try {
            // 等待任务完成或异常发生
            future.get();
        } catch (const std::exception& e) {
            // 处理从工作线程传递的异常
            logException("Worker thread exception", e);
            // 可能的恢复逻辑
        }

        worker.join();
    }

private:
    void doWork() {
        // 实际的工作逻辑
    }

    void logException(const std::string& context, const std::exception& e) {
        // 线程安全的日志记录
        std::lock_guard<std::mutex> lock(logMutex);
        std::cerr << "Exception in " << context << ": " << e.what() 
                  << " (Thread ID: " << std::this_thread::get_id() << ")\n";
    }

    std::mutex logMutex;
};

12.2 线程同步规则

12.2.1 [必须] 使用线程安全的数据结构和访问模式
  • 使用std::atomic进行简单的线程安全计数或标志。
  • 对共享数据的访问进行封装,使用互斥锁或读写锁保护。
  • 优先使用高级同步原语(如std::shared_mutex)而不是低级原语。

示例:

class ThreadSafeCounter {
    mutable std::shared_mutex mutex_;
    long value_ = 0;
public:
    long increment() {
        std::unique_lock lock(mutex_);
        return ++value_;
    }
    long get() const {
        std::shared_lock lock(mutex_);
        return value_;
    }
};
12.2.2 [必须] 使用RAII技术管理锁的生命周期
  • 使用std::lock_guardstd::unique_lockstd::scoped_lock自动管理锁的生命周期。
  • 尽量避免直接调用 mutex.lock()mutex.unlock(),手动锁定和解锁操作应在确有必要时,经团队审查后使用。

示例:

std::mutex mutex1, mutex2;
{
    std::scoped_lock lock(mutex1, mutex2);
    // 临界区代码
} // 锁在此自动释放
12.2.3 [必须] 使用适当的粒度管理锁
  • 最小化锁的作用范围,避免在持有锁时执行耗时操作。
  • 使用读写锁(std::shared_mutex)区分读写操作,提高并发性。

示例:

class Cache {
    mutable std::shared_mutex mutex_;
    std::unordered_map<Key, Value> data_;
public:
    std::optional<Value> get(const Key& key) const {
        std::shared_lock lock(mutex_);
        auto it = data_.find(key);
        return it != data_.end() ? std::optional<Value>{it->second} : std::nullopt;
    }
    void set(const Key& key, const Value& value) {
        std::unique_lock lock(mutex_);
        data_[key] = value;
    }
};
12.2.4 [必须] 正确使用条件变量避免虚假唤醒
  • 使用条件变量的 wait 操作时,总是提供一个谓词(通常是 lambda 表达式)来检查等待条件。
  • 确保在通知条件变量之前修改共享状态,并持有相关的互斥锁。
  • 优先使用 wait 的重载版本,该版本接受一个谓词,而不是手动实现 while 循环。

示例:

std::mutex mutex;
std::condition_variable cv;
std::queue<int> work_queue;

void consumer() {
    std::unique_lock<std::mutex> lock(mutex);
    cv.wait(lock, [&]{ return !work_queue.empty(); });
    int work = work_queue.front();
    work_queue.pop();
    lock.unlock();
    process(work);
}

void producer(int item) {
    std::lock_guard<std::mutex> lock(mutex);
    work_queue.push(item);
    cv.notify_one();
}
12.2.5 [必须] 实施一致的加锁顺序以预防死锁
  • 在需要多个锁时,始终以相同的顺序获取锁。
  • 使用std::lock()std::scoped_lock同时锁定多个互斥量。

示例:

void transfer(Account& from, Account& to, double amount) {
    auto lock_both = [](Account& a1, Account& a2) {
        std::scoped_lock lock(a1.mutex, a2.mutex);
    };
    
    if (&from < &to)
        lock_both(from, to);
    else
        lock_both(to, from);
    
    from.balance -= amount;
    to.balance += amount;
}
12.2.6 [建议] 使用线程本地存储(TLS)避免共享状态冲突
  • 在多线程环境中,对于每个线程都需要独立副本的数据,使用thread_local关键字声明。
  • 适用场景:线程特定的计数器、缓存、随机数生成器等。
  • 使用TLS可以避免不必要的同步开销,同时保证线程安全。

示例:

class ThreadSafeLogger {
    static thread_local std::string thread_name;
    static thread_local std::ostringstream log_buffer;

public:
    static void setThreadName(const std::string& name) {
        thread_name = name;
    }

    static void log(const std::string& message) {
        log_buffer << "[" << thread_name << "] " << message << '\n';
    }

    static std::string flushLog() {
        std::string result = log_buffer.str();
        log_buffer.str("");  // Clear the buffer
        return result;
    }
};

// 使用示例
void workerThread(int id) {
    ThreadSafeLogger::setThreadName("Worker-" + std::to_string(id));
    ThreadSafeLogger::log("Starting work");
    // ... 执行任务 ...
    ThreadSafeLogger::log("Work completed");
    std::cout << ThreadSafeLogger::flushLog();
}
12.2.7 [建议] 优先考虑使用原子操作代替低粒度锁,并根据场景选择适当的内存序
  • 对于简单的共享状态(如标志、计数器),优先使用原子操作而非互斥锁。
  • 对于 bool 类型的原子变量,可以考虑使用 memory_order_relaxed,因为单个位的读写通常是原子的。
  • 在性能关键路径上,可以考虑使用较弱的内存序,但必须通过压力测试验证正确性。

示例:

// 使用原子操作代替互斥锁
class AtomicFlag {
    std::atomic<bool> flag_{false};
public:
    void set() {
        // 对于 bool,relaxed 通常足够,因为硬件保证了原子性
        flag_.store(true, std::memory_order_relaxed);
    }
    bool test_and_set() {
        // 这里使用 seq_cst 是为了确保完全的可见性和顺序
        return flag_.exchange(true, std::memory_order_seq_cst);
    }
    void clear() {
        flag_.store(false, std::memory_order_relaxed);
    }
};

// 在性能关键路径上使用较弱的内存序
class LockFreeQueue {
    std::atomic<Node*> head_{nullptr};
    std::atomic<Node*> tail_{nullptr};
public:
    void enqueue(T value) {
        Node* new_node = new Node(std::move(value));
        Node* prev_tail = tail_.exchange(new_node, std::memory_order_acq_rel);
        if (prev_tail) {
            prev_tail->next.store(new_node, std::memory_order_release);
        } else {
            head_.store(new_node, std::memory_order_release);
        }
    }
    // dequeue 方法省略...
};
12.2.8 [建议] 避免忙等待,使用适当的同步机制
  1. 条件变量:
    • 优先使用 std::condition_variable 代替简单的循环检查。
    • 配合 std::mutex 使用,确保正确的同步。
  2. 事件通知:
    • 对于简单的一次性通知,考虑使用 std::promisestd::future 对。
  3. 信号量:
    • 在需要限制并发访问资源数量时,使用 std::counting_semaphore(C++20)或自定义信号量。
  4. 屏障同步:
    • 对于需要多个线程同时达到某个点的情况,使用 std::barrier(C++20)或自定义屏障。
  5. 超时机制:
    • 使用带超时的等待函数,如 std::condition_variable::wait_for()std::future::wait_for()
  6. 退避策略:
    • 在确实需要轮询的场景,实现指数退避策略而不是持续的忙等待。

示例代码:

class ThreadSafeQueue {
    std::queue<int> queue_;
    mutable std::mutex mutex_;
    std::condition_variable cv_;

public:
    void push(int value) {
        {
            std::lock_guard<std::mutex> lock(mutex_);
            queue_.push(value);
        }
        cv_.notify_one();
    }

    bool try_pop(int& value, std::chrono::milliseconds timeout) {
        std::unique_lock<std::mutex> lock(mutex_);
        if (cv_.wait_for(lock, timeout, [this] { return !queue_.empty(); })) {
            value = queue_.front();
            queue_.pop();
            return true;
        }
        return false;
    }
};

// 使用示例
ThreadSafeQueue queue;

// 生产者线程
void producer() {
    for (int i = 0; i < 10; ++i) {
        queue.push(i);
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

// 消费者线程
void consumer() {
    int value;
    while (true) {
        if (queue.try_pop(value, std::chrono::seconds(1))) {
            std::cout << "Consumed: " << value << std::endl;
        } else {
            std::cout << "Timeout, queue might be empty" << std::endl;
            break;
        }
    }
}

// 使用 std::future 进行一次性通知
std::promise<void> ready_promise;
std::future<void> ready_future = ready_promise.get_future();

std::thread worker([&ready_future]() {
    // 做一些准备工作
    std::this_thread::sleep_for(std::chrono::seconds(2));
    ready_future.wait();  // 等待主线程通知
    // 开始实际工作
});

// 主线程
// ... 做一些其他工作 ...
ready_promise.set_value();  // 通知 worker 线程开始工作
worker.join();

12.3 并发优化策略

12.3.1 [建议] 数据量大时谨慎使用全局锁

  • 使用分段锁(如std::shared_mutex)代替全局锁。

    分段锁:通过为数据结构的不同部分使用不同的锁,可以降低锁的争用程度,从而提高并行度。这对于大型数据结构特别有效,比如大型数组或哈希表。

  • 在高并发场景中,考虑使用无锁算法或数据结构。

示例:

class ConcurrentHashMap {
    static constexpr size_t NUM_BUCKETS = 101;
    std::array<std::shared_mutex, NUM_BUCKETS> mutexes;
    std::array<std::unordered_map<Key, Value>, NUM_BUCKETS> buckets;

    size_t bucket(const Key& key) const {
        return std::hash<Key>{}(key) % NUM_BUCKETS;
    }

public:
    std::optional<Value> get(const Key& key) const {
        size_t b = bucket(key);
        std::shared_lock lock(mutexes[b]);
        auto it = buckets[b].find(key);
        return it != buckets[b].end() ? std::optional<Value>{it->second} : std::nullopt;
    }

    void set(const Key& key, const Value& value) {
        size_t b = bucket(key);
        std::unique_lock lock(mutexes[b]);
        buckets[b][key] = value;
    }
};

12.3.2 [建议] 使用并行算法提高性能

  • 在适当的场景中,使用C++17引入的并行算法。
  • 使用std::execution::parstd::execution::par_unseq策略来并行化算法。

示例:

std::vector<int> vec(10000);
std::iota(vec.begin(), vec.end(), 0);

// 并行排序
std::sort(std::execution::par, vec.begin(), vec.end());

// 并行查找
auto it = std::find(std::execution::par_unseq, vec.begin(), vec.end(), 42);

12.3.3 [建议] 合理应用原子操作以优化性能

  • 原子操作应在无需完整锁定资源但需保持操作原子性的场景中使用,如单个变量的增减。
  • 推荐使用std::atomic类型保证操作的原子性,避免引入重的同步机制。

结语

在我们的编程学习之旅中,理解是我们迈向更高层次的重要一步。然而,掌握新技能、新理念,始终需要时间和坚持。从心理学的角度看,学习往往伴随着不断的试错和调整,这就像是我们的大脑在逐渐优化其解决问题的“算法”。

这就是为什么当我们遇到错误,我们应该将其视为学习和进步的机会,而不仅仅是困扰。通过理解和解决这些问题,我们不仅可以修复当前的代码,更可以提升我们的编程能力,防止在未来的项目中犯相同的错误。

我鼓励大家积极参与进来,不断提升自己的编程技术。无论你是初学者还是有经验的开发者,我希望我的博客能对你的学习之路有所帮助。如果你觉得这篇文章有用,不妨点击收藏,或者留下你的评论分享你的见解和经验,也欢迎你对我博客的内容提出建议和问题。每一次的点赞、评论、分享和关注都是对我的最大支持,也是对我持续分享和创作的动力。


阅读我的CSDN主页,解锁更多精彩内容:泡沫的CSDN主页
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

泡沫o0

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值