发布时间:2026/6/19 2:13:15
C++多线程编程入门教程(非常详细) 在 C11 标准之前C 语言并没有对并行编程提供语言级别的支持C 使用的多线程都由第三方库提供如POSIX标准pthread、OpenMG库或Windows线程库它们都是基于过程的多线程这使得 C 并行编程在可移植性方面存在诸多不足。为此C11 标准增加了线程及线程相关的类用于支持并行编程极大地提高了 C 并行编程的可移植性。多线程C11 标准提供了thread类模板用于创建线程该类模板定义在 thread 标准库中因此在创建线程时需要包含 thread 头文件。thread 类模板定义了一个无参构造函数和一个变参构造函数因此在创建线程对象时可以为线程传入参数也可以不传入参数。注意thread 类模板不提供拷贝构造函数、赋值运算符重载等函数因此线程对象之间不可以进行拷贝、赋值等操作。除了构造函数thread 类模板还定义了两个常用的成员函数join() 函数和 detach() 函数。1) join() 函数该函数将线程和线程对象连接起来即将子线程加入程序执行。join() 函数是阻塞的它可以阻塞主线程当前线程等待子线程工作结束之后再启动主线程继续执行任务。2) detach() 函数该函数分离线程与线程对象即主线程和子线程可同时进行工作主线程不必等待子线程结束。但是detach() 函数分离的线程对象不能再调用 join() 函数将它与线程连接起来。【示例1】下面通过案例演示 C11 标准中线程的创建与使用代码如下#includeiostream #includethread //包含头文件 using namespace std; void func() //定义函数func() { cout 子线程工作 endl; cout 子线程工作结束 endl; } int main() { cout 主线程工作 endl; thread t(func); //创建线程对象t t.join(); //将子线程加入程序执行 cout 主线程工作结束 endl; return 0; }运行结果主线程工作 子线程工作 子线程工作结束 主线程工作结束示例分析第 48 行代码定义了函数 func()第 12 行代码创建线程对象 t传入 func() 函数名作为参数即创建一个子线程去执行 func() 函数的功能第 13 行代码调用 join() 函数阻塞主线程。在 C 多线程中线程对象与线程是相互关联的线程对象出了作用域之后就会被析构如果此时线程函数还未执行完程序就会发生错误因此需要保证线程函数的生命周期在线程对象生命周期之内。一般通过调用 thread 中定义的 join() 函数阻塞主线程等待子线程结束或者调用 thread 中的 detach() 函数将线程与线程对象进行分离让线程在后台执行这样即使线程对象生命周期结束线程也不会受到影响。例如在【示例1】中将 join() 函数替换为 detach() 函数将线程对象与线程分离让线程在后台运行再次运行程序运行结果就可能发生变化。即使 main() 函数主线程结束子线程对象 t 生命周期结束子线程依然会在后台将 func() 函数执行完毕。小提示this_thread 命名空间C11 标准定义了 this_thread 命名空间该空间提供了一组获取当前线程信息的函数分别如下所示get_id() 函数获取当前线程 id。yeild() 函数放弃当前线程的执行权。操作系统会调度其他线程执行未用完的时间片当时间片用完之后当前线程再与其他线程一起竞争 CPU 资源。sleep_until() 函数让当前线程休眠到某个时间点。sleep_for() 函数让当前线程休眠一段时间。互斥锁在并行编程中为避免多线程对共享资源的竞争导致程序错误线程会对共享资源进行保护。通常的做法是对共享资源上锁当线程修改共享资源时会获取锁将共享资源锁上在操作完成之后再进行解锁。加锁之后共享资源只能被当前线程操作其他线程只能等待当前线程解锁退出之后再获取资源。为此C11 标准提供了互斥锁 mutex用于为共享资源加锁让多个线程互斥访问共享资源。mutex是一个类模板定义在 mutex 标准库中使用时要包含 mutex 头文件。mutex 类模板定义了三个常用的成员函数lock() 函数、unlock() 函数和 try_lock() 函数用于实现上锁、解锁功能。下面分别介绍这三个函数。1) lock() 函数lock() 函数用于给共享资源上锁。如果共享资源已经被其他线程上锁则当前线程被阻塞如果共享资源已经被当前线程上锁则产生死锁。2) unlock() 函数unlock() 函数用于给共享资源解锁释放当前线程对共享资源的所有权。3) try_lock() 函数try_lock() 函数也用于给共享资源上锁但它是尝试上锁如果共享资源已经被其他线程上锁try_lock() 函数返回false当前线程并不会被阻塞而是继续执行其他任务如果共享资源已经被当前线程上锁则产生死锁。【示例2】下面通过案例演示 C11 标准中 mutex 的上锁、解锁的过程C 代码如下#includeiostream #includethread #includemutex using namespace std; int num 0; //定义全局变量num mutex mtx; //定义互斥锁mtx void func() { mtx.lock(); //上锁 cout 线程id: this_thread::get_id() endl; //获取当前线程id num; cout counter: num endl; mtx.unlock(); //解锁 } int main() { thread ths[3]; //定义线程数组 for(int i 0; i 3; i) ths[i] thread(func); //分配线程任务 for(auto th : ths) th.join(); //将线程加入程序 cout 主线程工作结束 endl; return 0; }运行结果线程id: 11128 counter: 1 线程id: 10596 counter: 2 线程id: 9392 counter: 3 主线程工作结束示例分析第 5 行代码定义了一个全局变量 num初始值为 0第 6 行代码定义了互斥锁 mtx第 714 行代码定义了函数 func()在 func() 函数内部通过对象 mtx 调用 lock() 函数为后面的代码上锁第 13 行代码通过对象 mtx 调用 unlock() 函数解锁。当某个线程获取互斥锁 mtx 时该线程会为第 1012 行代码上锁即拥有了 func() 函数的所有权在解锁之前其他线程不能执行 func() 函数第 17 行代码定义了一个大小为 3 的线程数组 ths第 1819 行代码通过 for 循环为每个线程分配任务即让线程执行 func() 函数第 2021 行代码通过 for 循环调用 join() 函数将线程加入执行程序并阻塞当前线程主线程。由运行结果可知首先线程“11128”获取了互斥锁 mtx获得了 func() 函数的执行权输出 counter 的值为 1之后解锁然后线程“10596”获取了互斥锁 mtx获得了 func() 函数的执行权输出 counter 值为 2之后解锁最后线程“9392”获取了互斥锁 mtx获得了 func() 函数的执行权输出 counter 值为 3之后解锁。如果注释掉第 9 行和第 13 行代码即不给 func() 函数中的操作上锁则三个线程会同时执行 func() 函数输出的结果就会超出预期。例如连续输出三个线程 id或者先输出 counter 值为 3再输出 counter 值为 1。如果修改【示例2】调用 try_lock() 函数为 func() 函数上锁示例代码如下所示void func() { if (mtx.try_lock()) //调用try_lock()函数加锁 { cout 线程id: this_thread::get_id() endl; num; cout counter: num endl; mtx.unlock(); } }再次运行程序只有一个线程执行 func() 函数。当某个线程获取了互斥锁 mtx就会为 func() 函数上锁获得 func() 函数的执行权。另外两个线程调用 try_lock() 函数尝试上锁时发现 func() 函数已经被其他线程上锁这两个线程并没有被阻塞而是继续执行其他任务本案例中线程执行结束。因此最终只有一个线程执行 func()函数。lock_guard和unique_lock我们学习了互斥锁 mutex通过 mutex 的成员函数为共享资源上锁、解锁能够保证共享资源的安全性。但是通过 mutex 上锁之后必须要手动解锁如果忘记解锁当前线程会一直拥有共享资源的所有权其他线程不得访问共享资源造成程序错误。此外如果程序抛出了异常mutex 对象无法正确地析构导致已经被上锁的共享资源无法解锁。为此C11 标准提供了 RAII 技术的类模板lock_guard 和 unique_lock。lock_guard 和 unique_lock 可以管理 mutex 对象自动为共享资源上锁、解锁不需要程序设计者手动调用 mutex 的 lock() 函数和 unlock() 函数。即使程序抛出异常lock_guard 和 unique_lock 也能保证 mutex 对象正确解锁在简化代码的同时也保证了程序在异常情况下的安全性。下面分别介绍 lock_guard 和 unique_lock。1) lock_guardlock_guard 可以管理一个 mutex 对象在创建 lock_guard 对象时传入 mutex 对象作为参数。在 lock_guard 对象生命周期内它所管理的 mutex 对象一直处于上锁状态lock_guard 对象生命周期结束之后它所管理的 mutex 对象也会被解锁。【示例3】下面修改【示例2】来演示 lock_guard 的使用C 代码如下#includeiostream #includethread #includemutex using namespace std; int num 0; //定义全局变量num mutex mtx; //定义互斥锁mtx void func() { lock_guardmutex locker(mtx); //创建lock_guard对象locker cout 线程id: this_thread::get_id() endl; //获取当前线程id num; cout counter: num endl; } int main() { thread ths[3]; //定义线程数组 for (int i 0; i 3; i) ths[i] thread(func); //分配线程任务 for (auto th : ths) th.join(); //将线程加入程序 cout 主线程工作结束 endl; return 0; }运行结果线程id: 12300 counter: 1 线程id: 13316 counter: 2 线程id: 2316 counter: 3 主线程工作结束本例第 9 行代码创建了 lock_guard 对象 locker传入互斥锁 mtx 作为参数即对象 locker 管理互斥锁 mtx。当线程执行 func() 函数时locker 会自动完成对 func()函数的上锁、解锁功能。由运行结果可知程序运行时三个线程依旧是互斥执行 func() 函数。注意lock_guard 对象只是简化了 mutex 对象的上锁、解锁过程但它并不负责 mutex 对象的生命周期。在【示例3】中当 func() 函数执行结束时lock_guard 对象 locker 析构mutex 对象 mtx 自动解锁线程释放 func() 函数的所有权但对象 mtx 的生命周期并没有结束。2) unique_locklock_guard 只定义了构造函数和析构函数没有定义其他成员函数因此它的灵活性太低。为了提高锁的灵活性C11 标准提供了另外一个 RAII 技术的类模板unique_lock。unique_lock 与 lock_guard 相似都可以很方便地为共享资源上锁、解锁但 unique_lock 提供了更多的成员函数它有多个重载的构造函数而且 unique_lock对象支持移动构造和移动赋值。注意unique_lock 对象不支持拷贝和赋值。下面简单介绍几个常用的成员函数lock() 函数为共享资源上锁如果共享资源已经被其他线程上锁则当前线程被阻塞如果共享资源已经被当前线程上锁则产生死锁。try_lock() 函数尝试上锁如果共享资源已经被其他线程上锁该函数返回false当前线程继续其他任务如果共享资源已经被当前线程上锁则产生死锁。try_lock_for() 函数尝试在某个时间段内获取互斥锁为共享资源上锁如果在时间结束之前一直未获取互斥锁则线程会一直处于阻塞状态。try_lock_until() 函数尝试在某个时间点之前获取互斥锁为共享资源上锁如果到达时间点之前一直未获取互斥锁则线程会一直处于阻塞状态。unlock() 函数解锁。正是因为提供了更多的成员函数unique_lock 才能够更灵活地实现上锁和解锁控制例如转让 mutex 对象所有权移动赋值、在线程等待时期解锁等。但是更灵活的代价就是空间开销也更大运行效率相对较低。在编程过程中如果只是为了保证数据同步那么 lock_guard 完全能够满足使用需求。如果除了同步还需要结合条件变量进行线程阻塞则要选择 unique_lock。小提示RAII技术RAIIResource Acquisition Is Initialization资源获取初始化是 C 语言管理资源、避免内存泄漏的一个常用技术。RAII 技术利用 C 创建的对象最终被销毁的原则在创建对象时获取对应的资源在对象生命周期内控制对资源的访问使资源始终有效。当对象生命周期结束后释放资源。条件变量在多线程编程中多个线程可能会因为竞争资源而导致死锁一旦产生死锁程序将无法继续运行。为了解决死锁问题C11 标准引入了条件变量condition_variable类模板用于实现线程间通信避免产生死锁。condition_variable 类模板定义了很多成员函数用于实现进程通信的功能下面介绍几个常用的成员函数。1) wait()函数wait() 函数会阻塞当前线程直到其他线程调用唤醒函数将线程唤醒。当线程被阻塞时wait() 函数会释放互斥锁使得被阻塞在互斥锁上的其他线程能够获取互斥锁以继续执行代码。一旦当前线程被唤醒它就会重新夺回互斥锁。wait() 函数有两种重载形式函数声明分别如下所示void wait(unique_lockmutex lck); templateclass Predicate void wait(unique_lockmutex lck, Predicate pred);第一种重载形式称为无条件阻塞它以 mutex 对象作为参数在调用 wait() 函数阻塞当前线程时wait() 函数会在内部自动通过 mutex 对象调用 unlock() 函数解锁使得阻塞在互斥锁上的其他线程恢复执行。第二种重载形式称为有条件阻塞它有两个参数第一个参数是 mutex 对象第二个参数是一个条件只有当条件为 false 时调用 wait() 函数才能阻塞当前线程在收到其他线程的通知后只有当条件为 true 时当前线程才能被唤醒。2) wait_for()函数wait_for() 函数也用于阻塞当前线程但它可以指定一个时间段当收到通知或超过时间段时线程就会被唤醒。wait_for() 函数声明如下所示cv_status wait_for(unique_lockmutex lck, const chrono :: durationRepPeriod rel_time);在上述函数声明中wait_for() 函数第一个参数为 unique_lock 对象第二个参数为设置的时间段。函数返回值为 cv_status 类型cv_status是 C11 标准定义的枚举类型它有两个枚举值no-timeout 和 timeout。no-timeout 表示没有超时即在规定的时间段内当前线程收到了通知timeout 表示超时。3) wait_until()函数可以指定一个时间点当收到通知或超过时间点时线程就会被唤醒。wait_until() 函数声明如下所示cv_status wait_until(unique_lockmutex lck,const chrono::time_pointClock,Duration abs_tim);在上述函数声明中wait_until() 函数第一个参数为 unique_lock 对象第二个参数为设置的时间点。函数返回值为 cv_status 类型。4) notify_one()函数用于唤醒某个被阻塞的线程。如果当前没有被阻塞的线程则该函数什么也不做如果有多个被阻塞的线程则唤醒哪一个线程是随机的。notify_one() 函数声明如下所示void notify_one() noexcept;在上述函数声明中notify_one() 函数没有参数没有返回值并且不抛出任何异常。5) notify_all()函数用于唤醒所有被阻塞的线程。如果当前没有被阻塞的线程则该函数什么也不做。notify_all() 函数声明如下所示void notify_all() noexcept;条件变量用于实现线程间通信防止死锁发生为了实现更灵活的上锁、解锁控制条件变量通常与 unique_lock 结合使用。【示例4】下面通过案例演示条件变量在并行编程中的使用C 代码如下#includeiostream #includechrono #includethread #includemutex #includequeue using namespace std; queueint products; //创建队列容器products mutex mtx; //创建互斥锁mtx condition_variable cvar; //定义条件变量cvar bool done false; //定义变量done表示产品是否生产完毕 bool notified false; //定义变量notified表示是否唤醒线程 void produce() //生产函数 { for(int i 1; i 5; i) { //让当前线程休眠2 s this_thread::sleep_for(chrono::seconds(2)); //创建unique_lock对象locker获取互斥锁mtx unique_lockmutex locker(mtx); //生产产品并将产品存放到products容器中 cout 生产产品 i ; products.push(i); //将notified值设置为true notified true; //唤醒一个线程 cvar.notify_one(); } done true; //生产完毕设置done的值为true cvar.notify_one(); //唤醒一个线程 } void consume() //定义消费函数 { //创建unique_lock对象locker获取互斥锁mtx unique_lockmutex locker(mtx); while(!done) //判断产品是否生产完毕 { while(!notified) //避免虚假唤醒 { cvar.wait(locker); //继续阻塞 } while(!products.empty()) //如果products容器不为空 { //消费产品 cout 消费产品 products.front() endl; products.pop(); } notified false; //消费完之后将notified的值设置为false } } int main() { thread producer(produce); //创建生产线程 thread consumer(consume); //创建消费线程 producer.join(); consumer.join(); return 0; }运行结果生产产品1 消费产品1 生产产品2 消费产品2 生产产品3 消费产品3 生产产品4 消费产品4 上产产品5 消费产品5示例分析第 711 行代码分别定义了 queueint 类型的容器 products、互斥锁 mtx、条件变量 cvar 以及 bool 类型的变量 done、notified第 1230 行代码定义了生产函数 produce()在该函数内部通过 for 循环生产产品第 17 行代码先调用 sleep_for() 让当前线程休眠 2 s第 19 行代码创建unique_lock对象 locker获取互斥锁 mtx第 2122 行代码生产产品 i并调用 push() 函数将i存储到 proudcts 队列容器中第 2426 行代码每生产完一个产品就将 notified 的值设置为 true然后通过条件变量 cvar 调用notified_one()函数唤醒一个线程。第 34 行代码创建了 unique_lock 对象 locker获取互斥锁 mtx第 3548 行代码通过 while(!done) 循环判断生产是否完毕在该 while 循环中消费产品第 3740 行代码通过判断 notified 的值是否为 true来判断是否唤醒消费线程避免虚假唤醒第 4147 行代码判断容器 products 是否为空如果不为空就消费产品当产品消费完之后即容器 products 为空则设置 notified 的值为 false将消费线程阻塞。第 5255 行代码创建生产线程 producer 和消费线程 consumer分别调用生产函数produce()和消费函数consume()。程序运行结果为生产线程每生产一个产品消费线程就消费一个产品。生产线程每生产完一个产品就会将 notified 的值设置为 true然后通过条件变量 cvar 调用notify_one()函数唤醒消费线程消费产品。原子类型在并行编程中共享资源同时只能有一个线程进行操作这些最小的不可并行执行的操作称为原子操作。原子操作都是通过上锁、解锁实现的虽然使用lock_guard和unique_lock简化了上锁、解锁过程但是由于上锁、解锁过程涉及许多对象的创建和析构内存开销太大。为了减少多线程的内存开销提高程序运行效率C11 标准提供了原子类型atomic。atomic 是一个类模板它可以接受任意类型作为模板参数。创建的 atomic 对象称为原子变量使用原子变量就不需要互斥锁保护该变量进行的操作了。【示例4】在使用原子类型之前来看一个案例C 代码如下#includeiostream #includethread #includemutex using namespace std; mutex mtx; //定义互斥锁 int num 0; //定义全局变量num void func() { lock_guardmutex locker(mtx); //加锁 for(int i 0; i 100000; i) { num; //通过for循环修改num的值 } cout func()num: num endl; } int main() { thread t1(func); //创建线程t1执行func()函数 thread t2(func); //创建线程t2执行func()函数 t1.join(); t2.join(); cout main()num: num endl; return 0; }运行结果func()num: 100000 func()num: 200000 main()num: 200000示例分析第 56 行代码定义了互斥锁 mtx 和全局变量 num第 715 行代码定义了 func() 函数在该函数内部通过 for 循环修改 num 的值循环结束后输出 num 的值并且使用 lock_guard 为 func() 函数上锁第 1819 行代码创建两个线程 t1 和 t2 执行 func() 函数两个线程执行结束后输出 num 的值。由运行结果可知第一次 func() 函数输出 num 值为100000。func() 函数执行完毕之后线程释放锁接着另一个线程获取互斥锁执行 func() 函数再次修改 num 的值并输出。第二次 func() 函数输出 num 值为 200000。func() 函数执行完毕之后线程释放锁。两个线程执行完毕之后返回 main() 函数主线程输出 num 的值。如果使用原子类型定义全局变量 num在修改 num 的值时就不需要再给操作代码上锁也能实现多个线程的互斥访问保证某一时刻只有一个线程修改 num 的值。【示例5】下面修改【示例4】使用原子类型定义全局变量 numC 代码如下#includeiostream #includethread #includeatomic using namespace std; atomicint num 0; void func() { for(int i 0; i 100000; i) { num; } cout func()num: num endl; } int main() { thread t1(func); thread t2(func); t1.join(); t2.join(); cout main()num: num endl; return 0; }运行结果func()num: 165456 func()num: 200000 main()num: 200000本例第 5 行代码将 num 定义为全局的原子变量在 func() 函数中修改 num 的值时未上锁。【示例5】程序运行过程中线程 t1 与线程 t2 交叉执行 func() 函数修改 num 的值并不是一个线程先执行完成所有 for 循环。输出 num 值之后另一个线程才能去执行 for 循环进行修改。因此第一次输出的 num 值并不是 100000但最终结果是正确的。原子变量只保证num是原子操作第 10 行代码使得原子操作颗粒度更细【示例4】中原子操作为第 1014 行代码。它相当于是在“num”操作上上了锁示例代码如下所示int num0; for(int i 0; i 100000; i) { lock_guardmutex locker(mtx); //加锁 num; }上述代码中在 for 循环内部上了互斥锁循环结束locker 对象失效。如果有多个线程修改 num则多个线程会交叉修改 num 的值。但是相比于上锁原子类型实现的是无锁编程内存开销小程序的运行效率会得到极大提高并且代码更简洁。

相关新闻

2026/6/19 3:13:15

Meshroom完全教程:零基础掌握免费开源3D重建技术

Meshroom完全教程:零基础掌握免费开源3D重建技术 【免费下载链接】Meshroom Node-based Visual Programming Toolbox 项目地址: https://gitcode.com/gh_mirrors/me/Meshroom 想要将普通照片变成专业级3D模型吗?Meshroom正是你需要的终极解决方案…

2026/6/19 3:13:15

MCP3302/04 ADC芯片应用全解析:从SPI通信到硬件降噪实战

1. 项目概述:深入理解MCP3302/04这颗高性价比ADC在嵌入式系统开发,尤其是数据采集和传感器信号处理领域,模数转换器(ADC)的性能和易用性直接决定了整个系统的精度和稳定性。Microchip的MCP3302和MCP3304是两款非常经典…

2026/6/19 3:13:15

Talkie 角色互动新手入门指南

很多刚接触 AI 角色扮演平台的朋友,往往卡在第一步:明明下载了应用,注册了账号,却对着空荡荡的界面不知所措,或者创建出的角色对话生硬、毫无灵魂。大家期待的不仅仅是一个能回复消息的机器人,而是一个有记忆、有性格、能真正沉浸其中的虚拟伙伴。然而,复杂的设置选项、…

2026/6/19 3:13:15

Microchip 24AA32AF与24LC32AF EEPROM选型指南与I2C实战

1. 项目概述:为什么需要一份EEPROM选型指南?在嵌入式开发里,存储配置参数、校准数据或者运行日志是家常便饭。直接用MCU内部的Flash不是不行,但擦写次数有限,频繁操作容易“折寿”,而且掉电数据就没了。这时…

2026/6/19 3:13:15

Harness Engineering:线束工程的本质是系统级物理接口设计

1. 项目概述:这不是“线束工程师”,而是系统级接口设计的底层逻辑 你点开这个标题,大概率是被“YouTube高赞”吸引来的——毕竟现在刷到一个真正讲清楚技术概念的视频,比在早高峰地铁里抢到座位还难。但我要先泼一盆冷水&#xff…

2026/6/19 2:13:15

C++多线程编程入门教程(非常详细)

在 C11 标准之前,C 语言并没有对并行编程提供语言级别的支持,C 使用的多线程都由第三方库提供,如 POSIX标准(pthread)、OpenMG 库或 Windows 线程库,它们都是基于过程的多线程,这使得 C 并行编…

2026/6/19 0:13:13

嵌入式系统时钟与电源设计:从MPC801看精准与节制的平衡艺术

1. 项目概述:嵌入式系统的“心脏”与“脉搏”在嵌入式系统的世界里,微处理器就像大脑,而时钟与电源模块则是维持这个大脑正常工作的“心脏”与“脉搏”。我接触过不少嵌入式项目,从早期的8位机到如今复杂的32位SoC,一个…

2026/6/19 0:13:13

深入解析SCF5250 UART与QSPI寄存器配置与驱动开发实战

1. 项目概述与核心价值在嵌入式开发的日常里,串口(UART)和SPI通信是绕不开的两座大山。无论是调试信息输出、连接传感器,还是驱动显示屏、存储器,都离不开它们。但很多时候,我们只是调用现成的库函数&#…