Bootstrap

仓鼠cpp复习笔记:多线程入门(1)

前言

这个系列是我发现自己编程能力比较不足,所以想写blog来促进自己复习

希望能帮助到大家(先少误导)有问题可以直接在评论区指出 

本文环境是macOS和自带的clang, C++11,cmd内编译使用:

clang++ -std=c++11 -o my_program your_code.cpp

线程基础概念

首先要明白四个基本概念:

1. 进程:进程是操作系统分配资源的基本单位,每个进程都有独立的内存空间。

2. 线程:线程是进程内的一个执行单元,多个线程共享进程的资源。

3. 并发:并发是指多个任务在同一时间段内交替执行,但不一定是同时进行。

4. 并行:并行是指多个任务在同一时间点同时执行,通常依赖于多核处理器。

然后衍生一下:

进程&线程查询: windows的话Ctrl + Shift + Esc/cmd里用tasklist命令,mac的话只要在终端里输入top即可,也可以用ps命令(和linux一样)。可以发现一个进程往往是对应多个线程的,然后每个运行的程序往往对应一个进程,当然这里之后的篇章会再说,毕竟光看还是比较杂的。

进程&线程关系理解:

一个进程至少有一个线程,即使它只有一个主线程。

一个进程中的多个线程共享进程的资源,例如内存空间,但每个线程有自己的执行栈和程序计数器。

线程之间可以通过共享进程的资源进行通信,而进程之间的通信需要额外的机制

处理器:也就是CPU,可以打开网购平台看一下,一般指出的最明显的就是处理器了,游戏本还会指出一个显卡(或许以后我复习到了会再仔细介绍一下处理器相关)。处理器分单核核多核。处理器的“核”指的是处理器内部的独立计算单元。每个核都可以独立地执行指令。从处理器的角度可以进一步理解并发与并行:

并发(Concurrency):指多个任务在同一时间段内进行,可能是交替执行的,不一定是同时进行的,但因为CPU时钟频率很高,在高速交替进行下模拟出来了同时进行的效果,因此,单核处理器也可以并发。关键点:可以“交替进行”

并行(Parallelism):指多个任务在同一时刻由多个核心同时执行,在多核处理器上,任务可以真正地并行处理。关键点:“同时”

Cpp:Thread库

 C++11 开始,<thread> 库就成为了一个很重要的工具了。

最基础地,创建一个进程:

#include <iostream>
#include <thread>

void func(int x) {
    std::cout << x << "GET\n";
}

int main() {
    std::cout << "hello\n";

    std::thread t(func, 8);
    t.join();
    //创建了一个线程 t,并将 func 函数和参数 8 传递给它,即新线程会执行 func(8)。t.join() 阻塞当前线程,直到新线程完成执行。

    std::cout << "end thread\n";

    return 0;
}

以上thread 类和join 函数两行

展现了基础的“创建并启动进程”和“等待线程完成”这样一个过程

join在后文会继续讲

并且可以看出,thread是可以传递参数的,这里参数包括函数本身(func)+函数参数(8)

在这里特别提一点:这里函数参数传递有时可以考虑:std::ref (引用包装器,避免拷贝)

例子:

#include <iostream>
#include <thread>

void threadFunction(int n, const std::string& message) {
    for (int i = 0; i < n; ++i) {
        std::cout << message << std::endl;
    }
}

int main() {
    std::string msg = "仓鼠复习";
    std::thread t(threadFunction, 5, std::ref(msg));
    t.join();
    return 0;
}

lambda表达式

lambda表达式也能结合thread用

lambda表达式因为我不算掌握,之后我会单独开一篇进行复习(大概可以认为是一个更浓缩的函数表达方法),在这里就举一个基本例子:

#include <iostream>
#include <thread>

int main() {
    std::thread t([]() {
        std::cout << "hamster is coming" << std::endl;
    });
    t.join();
    return 0;
}

get_ID

可以用get_id方法去获取

#include <iostream>
#include <thread>

void getThreadID() {
    std::cout  << std::this_thread::get_id() << std::endl;
}

int main() {
    std::thread t(getThreadID);
    t.join();
    return 0;
}

输出会是一个十六位进制且唯一的数,如:(但我并不确定换了编译器是否会改变,大家可以自己试试) 

0x16db8b000

 注意,这个ID不表示和内存地址有关的东西

join 和 detach和joinable

join():等待线程完成-----当前线程将等待,直到目标线程执行完毕。

🌟使用场景:当主线程或其他线程依赖某个线程的执行结果时,我们需要使用 join() 来确保该线程执行完毕。

detach():分离线程,使其独立运行-----将线程分离,使其在后台独立运行,当前线程继续执行

🌟使用场景:当我们不关心线程的执行结果,且希望线程在后台继续运行时使用 detach()。

joinable() 是 std::thread 类中的一个成员函数,用于判断一个线程对象是否可以被 join(),即该线程是否仍然处于活动状态并且未被 detach() 或已经完成执行。

joinable() :顾命思意:join+able?

🌟 判断线程是否可 join:线程在被创建后,只有在没有被 join() 或 detach() 之后,它才是 “joinable” 的。

🌟避免调用无效的 join():如果线程已经被 join() 或 detach(),则调用 join() 或 detach() 会导致崩溃(报错见下图)。因此,在调用 join() 之前,应该首先检查线程是否是 “joinable”。

libc++abi: terminating due to uncaught exception of type std::__1::system_error: thread::join failed: Invalid argument
zsh: abort      ./my_program

再这里更详细解释线程从创建起的逻辑:(还是AI会总结)

1. 当一个 std::thread 对象被创建时,线程就会启动并开始执行。此时线程是活动的,并且处于 “joinable” 状态,即线程可以被 join() 或 detach()

2. 当调用 join() 方法时,主线程会阻塞,直到该线程执行完毕。一旦调用 join(),该线程对象就进入了不可 joinable状态。也就是说,线程已经被“加入”了主线程,不能再对同一线程再次调用 join() 或 detach()。

3. 调用 detach() 方法会将线程与主线程分离,使其在后台独立运行。分离的线程会继续执行,但主线程不再等待它完成。一旦调用 detach(),线程也不再是 “joinable” 的,因为它已经从主线程中独立出来,无法再被 join() 或 detach() 操作。

逻辑上举一个例子理解

比如说,你现在有10个线程,分别在对一个向量的10个部分进行操作

现在你希望把它们的结果加起来

你必须等子线程确认算出答案了,主线程才可以去取用那个答案

因此,这些会要被关心算没算完的东西就是join 你需要知道这个小任务结束了没

否则就是detach

综合代码

注意在这里,我们用chrono里的sleep_for函数模拟复杂问题中一些线程需要一定的时间(凸显多线程必要性)。

#include <iostream>
#include <thread>
#include <chrono>

void worker(int id) {
    std::cout << "Thread " << id << " is working...\n";
    if(id==2){
    std::this_thread::sleep_for(std::chrono::seconds(2));  // 模拟工作延迟,2延迟更长
    }
    std::cout << "Thread " << id << " finished work.\n";
}

int main() {
    std::thread t1(worker, 1);  // 创建线程 t1
    std::thread t2(worker, 2);  // 创建线程 t2

    // 使用 joinable 检查线程是否可以连接
    if (t1.joinable()) {
        std::cout << "Thread 1 is joinable, joining now...\n";
        t1.join();  // 等待 t1 完成
    }

    // 使用 detach 来分离线程
    if (t2.joinable()) {
        std::cout << "Thread 2 is joinable, detaching now...\n";
        t2.detach();  // 让 t2 独立执行,主线程不等待 t2
    }

    // 由于 t2 已被分离,主线程无需等待它)
    std::cout << "Main thread finishes.\n";

    return 0;
}

输出(注意被detach的2线程是没有finish,主线程直接结束了)

Thread Thread 1 is joinable, joining now...
Thread 21 is working...
Thread 1 finished work.
 is working...
Thread 2 is joinable, detaching now...
Main thread finishes.

后文:

之后会随着自己复习进度先出续篇2,补充讲一下thread相关的东西以及线程同步,欢迎关注~

;