C++多线程

提示:文章

文章目录

前言

前期疑问:
本文目标:


一、背景

今天看小米,提到很多需要c++多线程,然后我联想到MFC的代码,感觉那个多线程我也是很晕。然后认为那个多线程是建立在MFC框架上的。 所以我就在想

c++的多线程是什么样的呢?

二、 初识

2.1 运行一个多线程

我就百度,查到菜鸟教程中关于c++多线程的文章c++多线程

使用了里面的代码,

cpppthread.cpp文件

#include <iostream>
// 必须的头文件
#include <pthread.h>
 
using namespace std;
 
#define NUM_THREADS 5
 
// 线程的运行函数
void* say_hello(void* args)
{
    cout << "Hello Runoob!" << endl;
    return 0;
}
 
int main()
{
    // 定义线程的 id 变量,多个变量使用数组
    pthread_t tids[NUM_THREADS];
    for(int i = 0; i < NUM_THREADS; ++i)
    {
        //参数依次是:创建的线程id,线程参数,调用的函数,传入的函数参数
        int ret = pthread_create(&tids[i], NULL, say_hello, NULL);
        if (ret != 0)
        {
           cout << "pthread_create error: error_code=" << ret << endl;
        }
    }
    //等各个线程退出后,进程才结束,否则进程强制结束了,线程可能还没反应过来;
    pthread_exit(NULL);
}

这个代码我在codeblock中运行,报错

error: pthread.h: No such file or directory|

遇到问题百度

参考这个帖子:C1083: 无法打开包括文件: “pthread.h”: No such file or directory`

文中说到,pthread.h 是一个POSIX线程库的头文件,通常用于在Unix/Linux系统上进行多线程编程。

我就把代码放在linux上运行,要先编译

至于怎么编译,我又百度了一下

文章说,编译这个代码可以使用下面的命令,

gcc example.c -o example -lpthread

而我的代码是c++的,所以编译命令是

g++ example.c -o example -lpthread

在我的机器上成功编译,编译出example文件

执行

./example

执行结果如下

main() : 创建线程, 0
main() : 创建线程, 1
Hello Runoob! 线程 ID, 0
main() : 创建线程, 2
Hello Runoob! 线程 ID, 1
main() : 创建线程, 3
Hello Runoob! 线程 ID, 2
main() : 创建线程, 4
Hello Runoob! 线程 ID, 3
Hello Runoob! 线程 ID, 4

文中还提到,确保你的系统已经安装了pthread库。如果没有安装,你可能需要先安装它。在基于Debian的系统(如Ubuntu)上,你可以使用以下命令安装:

sudo apt-get install libc6-dev

在基于Red Hat的系统(如Fedora或CentOS)上,你可以使用以下命令安装:

sudo yum install glibc-headers

上面就是对C++多线程的初步了解。

三、多线程

3.1多线程读文件

今天突发奇想,为什么不用多线程读文件,像kitMap那样多线程读文件,感觉这样更好理解多线程啊。

然后就搜了段代码验证下

#include <iostream>
#include <fstream>
#include <thread>
#include <vector>
#include <chrono>
#include <iomanip>
#include <unistd.h>

constexpr int PATH_MAX = 256;

std::vector<std::string> vecTime;

//获取时间
std::string current_time_millis()
{
    auto now = std::chrono::system_clock::now();
    auto millis = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()) % 1000;
    auto now_without_millis = std::chrono::system_clock::to_time_t(now);
    std::tm now_tm = *std::localtime(&now_without_millis);

    std::ostringstream oss;
    oss << std::put_time(&now_tm, "%Y-%m-%d %H:%M:%S.")
        << std::setfill('0') << std::setw(3) << millis.count();
    return oss.str();
}

std::string GetCurrentTime()
{
    time_t now = time(0);
    char* currentTime = ctime(&now);
    return currentTime;
}

std::string GetCurrentPath()
{
    char cwd[PATH_MAX];
    if (getcwd(cwd, sizeof(cwd)) != nullptr) {
        std::cout << "当前工作目录是: " << cwd << std::endl;
    } else {
        std::cerr << "错误: 无法获取当前工作目录。" << std::endl;
    }

    return cwd;
}

// 线程函数,用于读取文件
void readFile(const std::string &filename)
{
    std::ifstream file(filename);
    std::string line;
    while (std::getline(file, line)) {
    }
}

int main()
{
    std::string time = current_time_millis();
    std::string mutiThreadStartTime= "mutiThread start time:" + time;
    vecTime.push_back(mutiThreadStartTime);

    // 文件名列表
    std::vector<std::string> filenames = {"part00.txt", "part01.txt", "CMakeLists.txt"};
    std::vector<std::string> filePaths;
    std::string currentPath = GetCurrentPath();
    currentPath = currentPath.substr(0, currentPath.find_last_of("/") + 1) + "data/";
    for (std::string fileName: filenames) {
        filePaths.push_back(currentPath + fileName);
    }


    // 创建线程
    std::vector<std::thread> threads;
    for (const auto &filename: filePaths) {
        threads.emplace_back(readFile, filename);
    }

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


    time = current_time_millis();
    std::string mutiThreadFinishTime= "mutiThread finish time:" + time;
    vecTime.push_back(mutiThreadFinishTime);

    //非多线程
    for (const auto &filename: filePaths) {
        std::ifstream file(filename);
        std::string line;
        while (std::getline(file, line)) {

        }
    }

    time = current_time_millis();
    std::string notMutiThreadFinishTime= "not mutiThread finish time:" + time;
    vecTime.push_back(notMutiThreadFinishTime);

    for (int i = 0; i < vecTime.size(); i++) {
        std::cout << vecTime[i] << std::endl;
    }

    return 0;
}

用上述代码验证线程和非线程读文件的时间差异。使用了3份文件,其中文件part00.txt大小195M,part01.txt大小196M,cmakelist.txt文件大小2Kb。打印时间如下

mutiThread start time:2024-05-28 14:33:32.165
mutiThread finish time:2024-05-28 14:33:32.293
not mutiThread finish time:2024-05-28 14:33:32.531

这样可以计算出多线程使用时间为130ms,单线程240ms。还是有差异的。

这时候我想如果放大数据量,会不会效果更明显呢?增加大文件数量。

然后增加了18个大小为387M的文件。再次跑一下程序

mutiThread start time:2024-05-28 15:11:36.797
mutiThread finish time:2024-05-28 15:11:37.989
not mutiThread finish time:2024-05-28 15:11:42.985

可以计算出多线程使用时间为1190ms,单线程5000ms。差异就变大了。

所以可以看出多线程的优势了。

其实关于这个我还想了一个问题,就是为什么多线程就能省时间呢?都是一个机器啊。我能想到的就是因为可能在一个线程读取数据过程中,可能是涉及到cpu短暂空闲的时候,这时候多线程就可以利用这个时间去访问其他文件。等于是充分利用了cpu。
附上cmakelist

cmake_minimum_required(VERSION 3.16.5)

message("this is cmakelist log")
message(${CMAKE_CURRENT_SOURCE_DIR})

get_filename_component(ProjectId ${CMAKE_CURRENT_SOURCE_DIR} NAME)
message(${ProjectId})
message(${ProjectId})
message(NAME)
string(REPLACE " " "_" ProjectId ${ProjectId})
message(${ProjectId})
project(${ProjectId} CXX)

#添加宏定义Debug为CMAKE_BUILD_TYPE
SET(CMAKE_BUILD_TYPE "Debug")

set(CMAKE_CXX_STANDARD 17)

if (CMAKE_BUILD_TYPE STREQUAL Debug)
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -pthread -g -D_DEBUG")
else ()
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -pthread -D_FORTIFY_SOURCE=2 -O3 -DNDEBUG -s")
endif ()

#添加头文件
#例如:include_directories(/usr/include/abc /usr/include/xxx)
#是将“/usr/include/abc”和“/usr/include/xxx”这两个目录添加至编译器的头文件搜索目录(两个目录用空格分隔)。

#通过编译源文件来创建一个可执行文件,其中,name是生成的可执行文件的名称,source是创建可执行文件所需要的源文件。
#源文件可以使用aux_source_directory指令返回的变量(将源文件保存在这个变量中),也可以是指定的.cpp文件(注意路径)。
add_executable(${ProjectId}
        main.cpp)

以上多线程是基于自己的之前看的资料中的多线程代码,线程池初步了解,但是这个代码已经验证过读文件时会出现存入的文件和读出的文件不一致的情况。不过确实是验证出了多线程对大量文件读取时的优势。不过还是不严谨。至于到底是哪里除了问题我还没有搞懂。下面的内容——再次理解多线程,其实也没有涉及到对这个线程池代码的验证。知识增加了一次自己对信号锁的认知。

四、再次理解多线程

一、背景

2024年6月8日19:25:36

因为在跟威奇调整统计代码时,需要将时间控制在55s内。所以我做了一下的工作和尝试

第一步就是创建状态机来控制多个请求。然后我还是用了信号锁来控制对静态成员map的访问,这个map是用来存访问结果的。关键问题是我设计的是访问类的静态方法,在这个静态方法中增加了信号锁。后面想了下好像不对。如果是静态方法的话,那就是串行执行,应该就不需要锁了吧。后面按照威奇说的,改成类的对象形式,通过java请求触发多个对象,实现某种程度上的多线程。这时候再增加线程锁。

所以接下来我验证了两个知识点,一个就是线程锁的使用,自己写了demo验证一下信号锁的效果。

第二个就是想验证线程池。

二、验证过程

2.1 信号锁

代码

int g_num = 0;
void addNum2()
{
    for(int i = 0; i < 1000; i++)
    {
        g_num = g_num + 10;
        std::cout << "g_num:  " << g_num << std::endl;
    }
}

int main
{
    std::vector<std::thread> numThreads;
    //读取变量的线程,用于验证变量的原子操作
    for(int i = 0; i < 100; i++)
    {
        numThreads.emplace_back(addNum2);
    }

    for (auto &thread: numThreads) {
        thread.join();
    }

    std::cout << "final g_num" << g_num << std::endl;
    return 0;
}

上述代码创建了100个线程,每个线程执行1000次对g_num的加10操作,预测最后的结果是1000 * 10 * 100 = 100万。

查看最后打印结果

g_num:  998880
g_num:  998890
g_num:  998900
g_num:  998910
g_num:  998920
g_num:  998930
g_num:  998940
g_num:  998950
g_num:  998960
g_num:  998970
g_num:  998980
g_num:  998990
g_num:  999000
g_num:  999010
final g_num999010

进程已结束,退出代码为 0

应该是实现了效果。

下面验证加锁后的效果

修改addNum2函数

#include mutex mt;
mutex mt;

void addNum2()
{
    for(int i = 0; i < 1000; i++)
    {
        mt.lock();
        g_num = g_num + 10;
        mt.unlock();
        std::cout << "g_num:  " << g_num << std::endl;
    }
}

查看打印结果

g_num:  999920
g_num:  999930
g_num:  999940
g_num:  999950
g_num:  999960
g_num:  999970
g_num:  999980
g_num:  999990
g_num:  1000000
final g_num1000000

确实验证了互斥锁的效果。

下面验证线程池

2.2 线程池

还是找到之前自己整理的文章:线程池初步了解

按照文章中记录的线程池代码,再次运行线程池

// threadPool.h文件

#ifndef _THREADPOOL_H
#define _THREADPOOL_H
#include <vector>
#include <queue>
#include <thread>
#include <iostream>
#include <condition_variable>
using namespace std;

const int MAX_THREADS = 1000; //最大线程数目

template <typename T>
class threadPool
{
public:
    threadPool(int number = 1);
    ~threadPool();
    bool append(T *task);
    //工作线程需要运行的函数,不断的从任务队列中取出并执行
    static void *worker(void *arg);
    void run();

private:
    //工作线程
    vector<thread> workThread;
    //任务队列
    queue<T *> taskQueue;
    mutex mt;
    condition_variable condition;
    bool stop;
};

template <typename T>
threadPool<T>::threadPool(int number) : stop(false)
{
    if (number <= 0 || number > MAX_THREADS)
        throw exception();
    for (int i = 0; i < number; i++)
    {
        cout << "create thread:" << i << endl;
        workThread.emplace_back(worker, this);
    }
}
template <typename T>
inline threadPool<T>::~threadPool()
{
    {
        unique_lock<mutex> unique(mt);
        stop = true;
    }
    condition.notify_all();
    for (auto &wt : workThread)
        wt.join();
}
template <typename T>
bool threadPool<T>::append(T *task)
{
    //往任务队列添加任务的时候,要加锁,因为这是线程池,肯定有很多线程
    unique_lock<mutex> unique(mt);
    taskQueue.push(task);
    unique.unlock();
    //任务添加完之后,通知阻塞线程过来消费任务,有点像生产消费者模型
    condition.notify_one();
    return true;
}
template <typename T>
void *threadPool<T>::worker(void *arg)
{
    threadPool *pool = (threadPool *)arg;
    pool->run();
    return pool;
}
template <typename T>
void threadPool<T>::run()
{
    while (!stop)
    {
        unique_lock<mutex> unique(this->mt);
        //如果任务队列为空,就停下来等待唤醒,等待另一个线程发来的唤醒请求
        while (this->taskQueue.empty())
            this->condition.wait(unique);
        T *task = this->taskQueue.front();
        this->taskQueue.pop();
        if (task)
            task->process();
    }
}
#endif

// task.h文件

#include "threadPool.h"
#include <string>
using namespace std;
class Task
{
private:
    int total = 0;

public:
    void process();
};

//任务具体实现什么功能,由这个函数实现
void Task::process()
{
    //这里就输出一个字符串
    cout << "task successful!" << endl;
    this_thread::sleep_for(chrono::seconds(1));
}

// main.cpp文件

#include "threadPool.h"
#include "task.h"

template class std::queue<Task>;
int main(void)
{
    threadPool<Task> pool(4);
    std::string str;
    while (1)
    {
        Task *task = new Task();
        pool.append(task);
        delete task;
    }
}

然后程序可以正常执行,我还回答了之前文章中遗留的几个问题。

目前有个疑问就是线程池threadPool析构函数什么时候执行。我把代码做了两处修改验证一下

一、析构函数增加打印

inline threadPool<T>::~threadPool()
{
    {
        unique_lock<mutex> unique(mt);
        stop = true;
    }
    condition.notify_all();
    for (auto &wt : workThread)
        wt.join();
}

二、main函数只创建10个task

template class std::queue<Task>;
int main(void)
{
    threadPool<Task> pool(4);
    std::string str;
    int num = 10;
    while (num--)
    {
        Task *task = new Task();
        pool.append(task);
        delete task;
    }
}

打印信息如下:

create thread:0
create thread:1
create thread:2
create thread:3
task successful!
task successful!
task successful!
task successful!
task successful!
task successful!
task successful!
task successful!
~threadPool
task successful!
task successful!

看上述信息虽然看起来~theadPool先析构了,但是是因为我的log位置打的不对,析构时还是会等所有任务执行完的。

最后我意识到一件事情,就是虽然我理解了threadPool的代码,但是想写出这样的代码还是很不容易的。但是不用纠结在此,先理解。

五、遗留问题

接下文
C++对线程对于类的静态成员处理


总结

未完待续

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值