C++并发编程:如何编写多线程代码

本文详细介绍了C++多线程编程的基础概念、应用场景、基本原理,涵盖了线程库选择、创建、同步、互斥、销毁,以及高级特性如线程池、原子变量和无锁数据结构。还探讨了生产者-消费者模型、并发网络编程实例,并提出了死锁、竞态条件和线程安全性的解决方案。
摘要由CSDN通过智能技术生成

一、多线程概述

1 多线程的概念与优劣

多线程是指在程序中同时运行多个线程,每个线程都可以独立执行不同的代码段,且各个线程之间共享程序的数据空间和资源。

粉丝福利, 免费领取C/C++ 开发学习资料包、技术视频/项目代码,1000道大厂面试题,内容包括(C++基础,网络编程,数据库,中间件,后端开发/音视频开发/Qt开发/游戏开发/Linuxn内核等进阶学习资料和最佳学习路线)↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓

优劣:

优点:提高程序的处理能力,增加响应速度和交互性。

缺点:线程的切换有一定的开销,且多线程容易引发数据竞争和死锁等问题。

2 多线程的应用场景

多线程常用于需要同时完成多个任务或执行多个耗时操作的应用场景,如并发服务器、GUI程序、游戏开发等。

3 多线程的基本原理

多线程的核心就是将程序分为多个线程并发执行,其中每个线程都独立运行,但共享同一组全局变量和操作系统资源,由于资源共用,使用线程时需要保证对资源的安全访问。

二、C++多线程编程基础

1 多线程库的选择

C++常用的多线程库有Windows API、POSIX Threads和C++11标准库,根据编译环境和目标系统选择不同的库。

2 线程的创建

以下是一个简单的使用C++11标准库创建线程的例子:

#include <iostream>
#include <thread> // 多线程库头文件

// 线程函数
void hello(int num) {
    std::cout << "Hello, concurrent world! There are " << num << " threads." << std::endl;
}

int main() {
    int num = std::thread::hardware_concurrency(); // 获取并发线程数
    std::thread t(hello, num); // 创建线程,并传递参数
    t.join(); // 等待线程结束
    return 0;
}

3 线程的同步与互斥

多个线程同时访问共享资源时,会出现数据竞争的问题。为了保证资源安全访问,需要进行同步与互斥。

以下是一个简单的使用C++11标准库进行同步与互斥的例子:

#include <iostream>
#include <thread>
#include <mutex> // 互斥量头文件

std::mutex mtx; // 互斥量对象

// 线程函数
void hello(int num) {
    mtx.lock(); // 上锁
    std::cout << "Hello, concurrent world! There are " << num << " threads." << std::endl;
    mtx.unlock(); // 解锁
}

int main() {
    int num = std::thread::hardware_concurrency();
    std::thread t1(hello, num);
    std::thread t2(hello, num); // 创建两个线程
    t1.join();
    t2.join(); // 等待两个线程结束
    return 0;
}

4 线程的销毁

线程的销毁可以通过join()或detach()方法实现,其中join()方法会阻塞调用线程直到被调用的线程执行完毕,而detach()方法则会将调用线程和被调用的线程分离,使两个线程可以独立运行。

5 线程安全性问题

多线程编程常见的线程安全性问题有数据竞争、死锁、优先级反转等,需要使用锁、条件变量、原子变量等工具进行保护,以保证程序的正确性和高效性。

三、C++多线程编程高级特性

1 线程池

线程池是一组预先创建好的线程资源,它们可以被多个任务共享使用,而不必每次都创建线程,从而减少线程的创建、销毁和切换时间,提高程序的效率。

2 原子变量

原子变量是一种特殊类型的变量,支持原子操作,这些操作能保证在多线程环境下的可靠性和一致性。

以下是一个简单的使用C++11标准库进行原子操作的例子:

#include <iostream>
#include <thread>
#include <atomic> // 原子变量头文件

std::atomic<int> cnt(0); // 原子变量

// 线程函数
void increase() {
    for (int i = 0; i < 100; ++i) {
        cnt++; // 原子操作
    }
}

int main() {
    std::thread t1(increase);
    std::thread t2(increase); // 创建两个线程
    t1.join();
    t2.join(); // 等待两个线程结束
    std::cout << "cnt = " << cnt << std::endl; // 输出结果
    return 0;
}

3 无锁数据结构

无锁数据结构是一种常见的高并发数据结构,它可以避免线程之间的互斥和等待,从而提高程序的并发性能。

4 多线程的性能调优

多线程程序的性能调优可以从多个角度入手,如线程数的优化、任务切分和负载均衡等方面。同时还可以使用一些性能分析工具和调试工具来进行监测和调试,以保证程序的正确性和高效性。

四、多线程编程实践案例

1 生产者-消费者模型

生产者-消费者模型是一种经典的多线程模型,用于解决线程间同步和数据共享的问题。生产者线程负责产生数据,消费者线程负责消费数据,并且两者都需要共享同样的数据缓冲区。这个模型的实现可以采用多种方式,例如使用信号量、条件变量和互斥量等同步机制来实现。

下面是一个C++实现的生产者-消费者模型的示例代码,其中假设数据缓冲区的大小为10:

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

std::mutex mtx; // 互斥量,用于保护共享数据的访问
std::condition_variable cond; // 条件变量,用于线程间同步
std::queue<int> queue; // 数据缓冲区

const int MAX_SIZE = 10; // 缓冲区大小

void producer()
{
    for (int i = 0; i < 20; ++i) // 生产20个数据
    {
        std::unique_lock<std::mutex> lock(mtx); // 加锁
        // 如果缓冲区满了,等待消费者消费
        cond.wait(lock, []() {return queue.size() < MAX_SIZE; });

        queue.push(i); // 生产者向缓冲区中添加数据
        std::cout << "producer: produce " << i << std::endl;
        cond.notify_one(); // 通知一个等待的消费者线程
    }
}

void consumer()
{
    int data = 0;
    while (data != 19) // 消费者消费20个数据
    {
        std::unique_lock<std::mutex> lock(mtx); // 加锁
        // 如果缓冲区为空,等待生产者生产
        cond.wait(lock, []() {return queue.size() > 0; });

        data = queue.front(); // 消费者从缓冲区中取出数据
        queue.pop();
        std::cout << "consumer: consume " << data << std::endl;
        cond.notify_one(); // 通知一个等待的生产者线程
    }
}

int main()
{
    std::thread t1(producer);
    std::thread t2(consumer);

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

    return 0;
}

这个程序演示了一个基本的生产者-消费者模型,其中使用了互斥量和条件变量来保证线程间同步,并使用队列作为共享数据缓冲区。在生产者线程中,如果缓冲区已满,则等待消费者线程消费。在消费者线程中,如果缓冲区已空则等待生产者线程生产。

注意:使用条件变量时需要加上互斥量,以确保条件的正确性。

2 多线程数据分析

多线程数据分析是在多线程环境下对大量数据进行处理的一种常见应用。对于需要处理大量数据的应用,使用多线程可以有效提高程序的运行效率。例如,在数据挖掘、机器学习、图像处理和模拟等领域,线程并行化已成为一种常用的技术手段。

下面是一个简化的C++实现的多线程数据分析示例代码,其中假设需要处理10万个数:

#include <iostream>
#include <thread>
#include <vector>

const int MAX_NUM = 100000; // 待处理的数据个数
const int THREAD_NUM = 4; // 线程数量

int nums[MAX_NUM]; // 数据数组
int result[THREAD_NUM] = { 0 }; // 处理结果数组

std::mutex mtx; // 互斥量,用于保护共享数据的访问

void worker(int id)
{
    int start = id * (MAX_NUM / THREAD_NUM); // 计算该线程处理的数据区间
    int end = (id + 1) * (MAX_NUM / THREAD_NUM);

    int sum = 0;
    for (int i = start; i < end; ++i) // 处理数据
    {
        sum += nums[i];
    }

    {
        std::unique_lock<std::mutex> lock(mtx); // 加锁
        result[id] = sum; // 更新处理结果
    }
}

int main()
{
    for (int i = 0; i < MAX_NUM; ++i)
    {
        nums[i] = i % 100;
    }

    std::vector<std::thread> threads; // 存储线程对象

    for (int i = 0; i < THREAD_NUM; ++i)
    {
        threads.emplace_back(worker, i); // 创建线程并加入到线程向量
    }

    for (auto& thread : threads)
    {
        thread.join(); // 等待线程结束
    }

    int final_sum = 0;
    for (int i = 0; i < THREAD_NUM; ++i)
    {
        final_sum += result[i]; // 汇总处理结果
    }

    std::cout << "final sum is " << final_sum << std::endl;

    return 0;
}

这个程序演示了一个基本的多线程数据分析过程,其中使用了4个线程来并行处理10万个数据。在每个线程中,通过指定数据分块的方式,处理部分数据,并累加处理结果。最后主线程将每个线程的处理结果进行汇总,得到最终的处理结果。

注意:在使用多线程时需要注意对共享数据的访问控制,例如使用互斥量来保证数据的正确性。

3 并发网络编程

并发网络编程是将多线程和网络编程技术结合起来,用于构建高并发网络应用程序的一种技术手段。在网络编程中,需要处理大量的来自不同客户端的连接请求,并且需要同时处理多个客户端之间的数据交换。通过使用多线程来并发处理不同客户端的请求和数据交换,可以提高网络应用程序的性能和可扩展性。

下面是一个简单的C++实现的并发网络编程示例代码,其中使用了Boost库来简化网络编程的过程。这个程序监听本地端口8090,接受来自客户端的连接请求,并利用不同的线程来并发处理不同客户端的数据交换:

#include <iostream>
#include <boost/asio.hpp>
#include <boost/bind.hpp>
#include <boost/thread/thread.hpp>

const int MAX_LEN = 1024; // 接收缓冲区大小

class Session
{
public:
    Session(boost::asio::io_service& io_service)
        :m_socket(io_service)
    {
    }

    boost::asio::ip::tcp::socket& socket()
    {
        return m_socket;
    }

    void start()
    {
        m_socket.async_read_some(boost::asio::buffer(m_data, MAX_LEN),
            boost::bind(&Session::handle_read, this,
                boost::asio::placeholders::error,
                boost::asio::placeholders::bytes_transferred));
    }

    void handle_read(const boost::system::error_code& error,
        size_t bytes_transferred)
    {
        if (!error)
        {
            boost::asio::async_write(m_socket,
                boost::asio::buffer(m_data, bytes_transferred),
                boost::bind(&Session::handle_write, this,
                    boost::asio::placeholders::error));
        }
        else
        {
            std::cout << "error: " << error.message() << std::endl;
            delete this;
        }
    }

    void handle_write(const boost::system::error_code& error)
    {
        if (!error)
        {
            m_socket.async_read_some(boost::asio::buffer(m_data, MAX_LEN),
                boost::bind(&Session::handle_read, this,
                    boost::asio::placeholders::error,
                    boost::asio::placeholders::bytes_transferred));
        }
        else
        {
            std::cout << "error: " << error.message() << std::endl;
            delete this;
        }
    }

private:
    boost::asio::ip::tcp::socket m_socket;
    char m_data[MAX_LEN];
};

class Server
{
public:
    Server(boost::asio::io_service& io_service, int port)
        : m_io_service(io_service),
        m_acceptor(io_service,
            boost::asio::ip::tcp::endpoint(
                boost::asio::ip::tcp::v4(), port))
    {
        start_accept();
    }

    void start_accept()
    {
        Session* new_session = new Session(m_io_service);
        m_acceptor.async_accept(new_session->socket(),
            boost::bind(&Server::handle_accept, this, new_session,
                boost::asio::placeholders::error));
    }

    void handle_accept(Session* new_session,
        const boost::system::error_code& error)
    {
        if (!error)
        {
            boost::thread t(boost::bind(&Session::start, new_session));
        }
        else
        {
            delete new_session;
        }

        start_accept();
    }

private:
    boost::asio::io_service& m_io_service;
    boost::asio::ip::tcp::acceptor m_acceptor;
};

int main()
{
    try
    {
        boost::asio::io_service io_service;
        Server s(io_service, 8090);
        io_service.run();
    }
    catch (std::exception& e)
    {
        std::cout << "error: " << e.what() << std::endl;
    }

    return 0;
}

这个程序是一个简单的Echo服务器,监听本地端口8090,并利用不同的线程来处理每个客户端的请求和响应数据。具体地,每当有一个新的客户端接入时,就创建一个新的Session对象,并利用一个新的线程来并发处理该客户端连接上的数据传输。这个程序的编写需要熟悉网络编程、多线程编程和Boost库的基本用法。

五、C++多线程编程的常见问题与应对策略

1 死锁与饥饿

死锁和饥饿是多线程编程中常见的问题,需要特殊注意。死锁是指两个或多个线程互相等待对方释放锁的情况,导致线程无法继续执行的问题。饥饿则是指某个线程无法获得所需资源,导致该线程无法继续执行的问题。

对于死锁问题一种常见的解决方式是避免使用多个锁或在使用多个锁时统一获取锁的顺序,以避免出现环路依赖死锁的情况。另一种常见的解决方式是使用RAII技术,将锁的获取和释放放在同一个类中,使用智能指针管理这些类,避免手动操作锁的获取和释放,减少人为错误。

对于饥饿问题需要让所有线程公平竞争资源,避免一些线程独占资源导致其他线程无法继续执行。一种常见的解决方式是使用队列等数据结构,在多个线程之间共享数据资源,让所有线程均有机会获得资源,从而避免饥饿问题的发生。

2 竞态条件和原子操作

竞态条件是指多个线程同时访问和修改同一个共享资源时,导致最终结果依赖于不同线程执行顺序的情况。原子操作则是指不可被中断的操作,可以保证对一个共享变量的操作是不可分割、完整的。

对于竞态条件问题一种常见的解决方式是使用锁和互斥量等同步机制来控制共享资源的访问和修改,保证同一时间只有一个线程可以访问和修改共享资源。另一种常见的解决方式是使用原子操作,通过CAS(Compare-and-Swap)等机制保证对共享变量的操作是原子性的,从而避免竞态条件的发生。

3 线程安全性

多线程编程中线程安全性是一个非常重要的问题,指的是在多个线程并发执行时,程序的行为仍然是正确的。对于线程安全性的保证,可以采用许多不同的技术手段,例如使用互斥量、条件变量、原子操作、Thread-Local Storage等技术,避免共享资源的访问冲突和数据竞争,从而保证线程安全性。

需要注意的是线程安全并不是绝对的,为了保证线程安全性,必须深入了解程序的细节,并对可能出现的并发问题进行充分的测试和验证,从而保证多线程程序的稳定性和可靠性。

原文地址:https://www.toutiao.com/article/7329398366352261671/

粉丝福利, 免费领取C/C++ 开发学习资料包、技术视频/项目代码,1000道大厂面试题,内容包括(C++基础,网络编程,数据库,中间件,后端开发/音视频开发/Qt开发/游戏开发/Linuxn内核等进阶学习资料和最佳学习路线)↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值