)
线程什么是线程之前学过进程是一个正在运行的程序那线程就是进程内部的一条执行路径更官方更专业的说法则是线程是 CPU 调度的最小单位。形象的比喻进程 工厂拥有场地、原料、设备等资源线程 工人多个工人在同一个工厂中协作共享工厂资源但各自独立工线程管理函数#include pthread.h int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);功能创建新线程参数pthread_t* thread存储新线程ID对比线程的键值来记忆参数const pthread_attr_t* attr线程属性NULL表示默认一般就写NULL参数void* (*start_routine)(void* )线程入口函数一个参数为void*返回值也为void*的函数参数void *arg传递给入口函数的参数即第三个参数里的void*参数代码示例这段代码运行之后会出现一个结果那就是可能只运行了五遍main run原因在于主函数运行太快主函数结束之后没有等待另一个线程运行完就结束当前进程了所以导致输出结果出错为此引出另外两个函数void pthread_exit(void *retval);功能显式终止当前线程该函数在当前线程内部使用参数void* retval给另一个函数pthread_join传递参数int pthread_join(pthread_t thread, void **retval);功能阻塞等待线程结束并回收资源参数pthread_t thread线程id即线程标识符参数void** retval获取线程的返回值即pthread_exit()里面的参数下面我们来看这段代码→我们创建五个线程每次将当前的i传递给这个线程然后让当前线程打印出这个i的值并退出线程那么结果会打印出什么呢我们来运行看下结果→我们发现每次结果都不一样为什么会出现这样的结果呢我们每次创建新线程时会将i的地址传递给fun()函数此时系统会创建线程同时循环也会继续运行而i的值会根据循环次数不断改变当在fun()函数中获取i的值时i已经不再是当时的值了所以会导致出现各种各样的情况。解决办法每次循环malloc一个变量存储i的值将这片空间传递给fun()函数再来看一段代码→依旧创建五个线程同时创建一个全局变量g_val1每个线程循环1000次每次让g_val请问循环结束之后g_val的值会是多少呢我们发现结果并没有出现问题因为给虚拟机分配的处理器只有4个这样导致并行的情况并不常见在并发情况下不会出现问题而如果处理器数量多起来的时候数字会少于5000大概会变成4999或者4998。原因在并行处理的过程中两个线程会同时读取到g_val的值而这两个线程读取到的值也是相同的这时候同时并进行返回结果就会导致两个线程运行两次但是只了一次实际上这种同时读取的概率并不高5个进程循环运行1000次也只会出现1-2次但数据越大出错的次数也会越多所以希望通过这两段代码帮助自己理解线程的运行过程。C11 之后的 thread 类拷贝构造与移动构造std::thread的拷贝构造函数被删除delete也就是说线程对象不允许被复制。原因很好理解一个线程对象代表一条真实的执行路径如果允许复制就会出现两个对象同时管理同一个线程的情况析构时谁负责回收资源、谁负责 join 都会变得混乱容易引发未定义行为。因此下面的写法是编译错误的#include iostream #include thread void fun() { std::cout 子线程运行中 std::endl; } int main() { std::thread t1(fun); std::thread t2 t1; // 错误拷贝构造已被删除 return 0; }虽然不能拷贝但std::thread支持移动构造和移动赋值可以把线程的所有权从一个对象转移给另一个对象。转移之后原来的线程对象就变为空不再关联任何线程不能再对它调用join()或detach()。#include iostream #include thread void fun() { std::cout 子线程运行中 std::endl; } int main() { std::thread t1(fun); std::thread t2 std::move(t1); // 移动构造t1 变为空 t2.join(); // 由 t2 负责等待线程结束 return 0; }移动赋值也是类似的但要注意如果目标对象已经关联了一个线程直接移动赋值会调用std::terminate()终止程序。所以移动赋值前应确保目标对象是空的或者已经join()/detach()过。#include iostream #include thread void fun() { std::cout 子线程运行中 std::endl; } int main() { std::thread t1(fun); std::thread t2; t2 std::move(t1); // 移动赋值t2 原本为空安全 t2.join(); return 0; }小结一下std::thread是只可移动、不可复制的类型这保证了每个线程对象在同一时刻只被一个变量管理避免了资源重复释放的问题。前面介绍的 pthread 是 Linux 下 C 语言风格的线程接口使用起来需要手动管理线程 id、入口函数指针和返回值比较繁琐。C11 标准库提供了std::thread类把线程封装成面向对象的形式使用起来更加简洁和安全。基本使用使用std::thread需要包含头文件thread创建线程时直接传入可调用对象函数、函数对象、lambda 表达式等即可。#include iostream #include thread void fun() { std::cout 子线程运行中 std::endl; } int main() { std::thread t(fun); // 创建线程并启动 t.join(); // 阻塞等待线程结束 std::cout 主线程结束 std::endl; return 0; }join 与 detach和 pthread 类似std::thread也提供了等待线程结束和分离线程两种方式join()阻塞当前线程直到子线程执行完毕相当于 pthread 中的pthread_join。detach()将子线程与主线程分离子线程在后台独立运行主线程不再等待它相当于把线程交给系统托管。#include iostream #include thread void fun() { std::cout 子线程运行中 std::endl; } int main() { std::thread t(fun); t.detach(); // 分离线程主线程不再等待 std::cout 主线程结束 std::endl; return 0; }注意使用detach()后子线程可能还没运行完主线程就结束了此时子线程会继续在后台运行直到系统回收资源。带参数的线程std::thread的构造函数支持传入参数参数会以值传递的方式复制到线程内部因此不会出现前面 pthread 中传递 i 的地址导致值被修改的问题。#include iostream #include thread void fun(int n) { std::cout 参数为 n std::endl; } int main() { for (int i 0; i 5; i) { std::thread t(fun, i); // 直接传值安全可靠 t.join(); } return 0; }如果需要传递引用可以使用std::ref包装否则默认按值传递。lambda 表达式创建线程使用 lambda 表达式可以让代码更紧凑尤其适合简单的线程任务。#include iostream #include thread int main() { std::thread t([](int a, int b) { std::cout 两数之和 a b std::endl; }, 3, 4); t.join(); return 0; }小结相比 pthreadC11 的std::thread有以下优势面向对象封装使用更直观。参数按值传递避免共享变量带来的数据竞争问题。支持 lambda、函数对象等多种可调用形式。配合std::mutex、std::lock_guard等标准库组件可以更方便地实现线程同步。线程间同步上信号量#include semaphore.h int sem_init(sem_t *sem, int pshared, unsigned int value);功能用于初始化一个未命名的信号量参数sem_t * sem指向要初始化的信号量对象的指针结合进程中信号量的参数来记忆参数int pshared指示信号量是用于线程间共享还是进程间共享0表示只在线程间共享非0表示可以在多个进程间共享参数unsigned int value信号量的初始值非负整数返回值0代表成功-1代表失败#include semaphore.h int sem_wait(sem_t *sem);功能如果信号量值为0则堵塞信号量1时进行减1并通行即p操作参数sem_t* sem指向要操作的信号量对象的指针返回值0代表成功-1代表失败#include semaphore.h int sem_post(sem_t *sem);功能将信号量的值加1即v操作参数sem_t* sem指向要操作的信号量对象的指针返回值0代表成功-1代表失败那么我们现在可以通过信号量来控制只让一个线程让g_val下面是演示代码→如果前面对于进程中使用信号量的过程比较熟悉的话那进程中使用就会非常简单了。互斥锁上面那种情况下使用互斥锁也完全没有问题int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);功能初始化一个互斥锁对象参数pthread_mutex_t* mutex指向互斥锁对象的指针依旧参考信号量的参数参数const pthread_mutexattr_t* attr属性对象暂时不需要理解啥意思填NULL就行返回值0为成功-1失败int pthread_mutex_lock(pthread_mutex_t *mutex);功能将互斥锁锁定参数pthread_mutex_t* mutex指向互斥锁对象的指针返回值0为成功失败返回错误码int pthread_mutex_unlock(pthread_mutex_t *mutex);功能将互斥锁解锁参数pthread_mutex_t* mutex指向互斥锁对象的指针返回值0为成功失败返回错误码int pthread_mutex_destroy(pthread_mutex_t *mutex);功能销毁互斥锁释放资源参数pthread_mutex_t* mutex指向互斥锁对象的指针返回值0为成功失败返回错误码我们下面来看运用互斥锁的代码→可以发现和信号量的使用一模一样了解其中原理会发现线程间同步非常简单。C11 中的 std::mutex前面介绍的 pthread 互斥锁是 C 语言风格的接口需要手动调用 init、lock、unlock、destroy 四个函数使用起来比较繁琐而且一旦忘记 unlock 或者中途抛出异常就很容易造成死锁。C11 标准库提供了std::mutex类把互斥锁封装成面向对象的形式使用起来更加简洁和安全。使用std::mutex需要包含头文件mutex核心成员函数如下void lock();功能锁定互斥量如果已经被其他线程锁定则阻塞等待void unlock();功能解锁互斥量释放锁的所有权bool try_lock();功能尝试锁定互斥量如果当前已被锁定则立即返回 false不会阻塞下面是一个使用std::mutex保护共享变量的示例#include iostream #include thread #include mutex std::mutex mtx; int g_val 1; void fun() { for (int i 0; i 1000; i) { mtx.lock(); // 加锁 g_val; // 临界区同一时刻只有一个线程能进入 mtx.unlock(); // 解锁 } } int main() { std::thread t1(fun); std::thread t2(fun); t1.join(); t2.join(); std::cout g_val g_val std::endl; return 0; }运行结果g_val 2001无论运行多少次结果都一致因为互斥锁保证了每次只有一个线程能对 g_val 进行 操作不会出现前面提到的数据竞争问题。std::lock_guard 自动管理锁手动调用 lock() 和 unlock() 有一个隐患如果临界区代码抛出异常unlock() 就不会被执行导致死锁。为此 C11 提供了std::lock_guard它是一个RAII 风格的锁管理器构造时自动加锁析构时自动解锁无论是否发生异常都能保证锁被正确释放。#include iostream #include thread #include mutex std::mutex mtx; int g_val 1; void fun() { for (int i 0; i 1000; i) { std::lock_guardstd::mutex lock(mtx); // 构造时加锁 g_val; // 临界区 } // 析构时自动解锁 } int main() { std::thread t1(fun); std::thread t2(fun); t1.join(); t2.join(); std::cout g_val g_val std::endl; return 0; }可以看到使用std::lock_guard后代码更加简洁不需要手动调用 unlock()也从根本上避免了因异常导致的死锁问题。小结对比 pthread 互斥锁和 C11 的std::mutexpthread 是 C 语言接口需要手动管理 init、lock、unlock、destroy 四个步骤。std::mutex是面向对象的封装构造即初始化析构自动释放。std::lock_guard进一步封装了加锁/解锁RAII 风格保证异常安全。在实际 C 项目中推荐优先使用std::mutexstd::lock_guard的组合。线程间同步目前简单了解了信号量与互斥锁下一次会学习条件变量与读写锁。Linux学习这段时间以来从一开始的新奇到进程时的有点迷茫到现在感觉略微有点得心应手了也学习了很多全新的知识希望自己每天都能有进步牢记学过的知识