thread基础篇
1.thread
1.1 thread与pthread_create创建线程的区别
特性 | pthread_create (POSIX) | std::thread (C++ 标准库) |
---|---|---|
相同点 | 创建线程 编译时需要加-pthread | |
所属标准 | POSIX(Unix/Linux 系统层面) | C++ 标准库(C++11+) |
语言范式 | C 语言风格(函数调用) | C++ 类(面向对象) |
资源管理 | 手动管理(需调用 pthread_join 或 pthread_detach ) | 自动管理(通过 RAII) |
跨平台性 | 主要用于 POSIX 系统(Linux、macOS) | 跨平台(Windows、Linux、macOS 等) |
异常安全 | 不提供异常安全机制 | 支持异常安全(如 std::jthread 自 C++20 起) |
易用性 | 较低(需手动处理线程属性、参数传递等) | 较高(语法简洁,支持 lambda、参数自动转发) |
文档参见 | https://en.cppreference.com/w/cpp/thread/thread.html | man pthread_create |
头文件 | #include | #include <pthread.h> |
1.2 构造函数原型
template< class F, class… Args >
explicit thread( F&& f, Args&&… args );
示例:创建线程的几种方式
#include <thread>
#include <iostream>
using namespace std;
void print_hello()
{
cout << "hello" << endl;
}
void print_msg(string msg)
{
cout << msg << endl;
}
void print_int(int num)
{
cout << num << endl;
}
void print_int_ref(int& num)
{
cout << ++num << endl;
}
int main() {
//无参
thread t1(print_hello);
//有参
thread t2(print_msg, "world");//传递值
thread t3(print_int, 5);//传递值
int num = 8;
thread t4(print_int_ref, ref(num));//传递引用
cout << num << endl;
//lambda表达式
thread t5([](string msg){cout << msg << endl;}, "chenglin");
t1.join();
t2.join();
t3.join();
t4.join();
t5.join();
return 0;
}
输出:
chenglin@ubuntu:~/work/Thread$ g++ test3.cpp -o test3 -pthread;./test3
world
5
hello
8
9
chenglin
1.3 常用方法
方法 | 功能 |
---|---|
thread t1(testfunc); | 线程创建 |
t1.detach(); | 当前线程后台运行 |
t1.join(); | 阻塞当前线程 |
t1.get_id(); | 获取线程id |
t1.joinable(); | 判断当前线程是否在执行 |
this_thread::sleep_for(chrono::seconds(5)) | 休眠 |
示例:演示thread常用方法的使用
#include <thread>
#include <iostream>
using namespace std;
void print_hello()
{
cout << "hello" << endl;
}
void print_chenglin()
{
cout << "chenglin" << endl;
}
int main()
{
cout << "test start" << endl;
cout << "main thread id:" << this_thread::get_id() << endl;
thread t1(print_hello);
cout << "hello thread id:" << t1.get_id() << endl;
t1.detach();
thread t2(print_chenglin);
cout << "chenglin thread id:" << t2.get_id() << endl;
t2.detach();
//this_thread::sleep_for(chrono::seconds(5));
if(t1.joinable())
{
cout << "t1 thread running ...." << endl;
t1.join();
}
else
{
cout << "t1 thread end" << endl;
}
if(t2.joinable())
{
cout << "t2 thread running ...." << endl;
t2.join();
}
else
{
cout << "t2 thread end" << endl;
}
cout << "test end" << endl;
return 0;
}
1.4 detach和join方法的差异
示例:演示主线程结束, detach和join方法的差异
#include <thread>
#include <iostream>
using namespace std;
void print_hello()
{
cout << "hello" << endl;
}
void print_chenglin()
{
cout << "chenglin" << endl;
}
int main()
{
cout << "test start" << endl;
thread t1(print_hello);
t1.join();
thread t2(print_chenglin);
t2.detach();
//this_thread::sleep_for(chrono::seconds(5));
cout << "test endl" << endl;
return 0;
}
输出:
chenglin@ubuntu:~/work/Thread$ g++ test0.cpp -o test0 -pthread;./test0
test start
hello
test endl
取消Line27注释输出:
chenglin@ubuntu:~/work/Thread$ g++ test0.cpp -o test0 -pthread;./test0
test start
hello
chenglin
test endl
示例:防止主线程结束,datach线程未执行完,优化代码如下:
#include <thread>
#include <iostream>
#include <atomic>
using namespace std;
atomic<bool> isStop(false);
void print_hello()
{
cout << "hello" << endl;
}
void print_chenglin()
{
cout << "chenglin" << endl;
isStop = true;
}
int main()
{
cout << "test start" << endl;
thread t1(print_hello);
t1.join();
thread t2(print_chenglin);
t2.detach();
while(!isStop)
{
this_thread::sleep_for(chrono::milliseconds(1));
}
cout << "test endl" << endl;
return 0;
}
输出:
chenglin@ubuntu:~/work/Thread$ g++ test0.cpp -o test0 -pthread;./test0
test start
hello
chenglin
test endl
2.this_thread
2.1 常用方法
函数 | 说明 |
---|---|
get_id | 获取线程id |
yield | 放弃线程执行,回到就绪状态 |
sleep_for | 程序休眠 |
sleep_until | 程序休眠,直到某一时刻往下执行 |
示例:this_thread常用方法的使用
#include <thread>
#include <iostream>
using namespace std;
void print_hello()
{
cout << "hello thread id :" << this_thread::get_id() << endl;
cout << "hello" << endl;
}
int main()
{
printf("test start\n");
//get_id()
cout << "main thread id :" << this_thread::get_id() << endl;
thread t1(print_hello);
//yield()
auto start = std::chrono::high_resolution_clock::now();
this_thread::yield();
auto end1 = std::chrono::high_resolution_clock::now();
//sleep_for() 后接参数参考https://en.cppreference.com/w/cpp/chrono/duration.html
this_thread::sleep_for(chrono::microseconds(1));
auto end2 = std::chrono::high_resolution_clock::now();
auto duration1 = chrono::duration_cast<chrono::microseconds>(end1 - start);
auto duration2 = chrono::duration_cast<chrono::microseconds>(end2 - end1);
cout << "duration1:" << duration1.count() << " " << "duration2:" << duration2.count() << endl;
this_thread::sleep_for(chrono::seconds(2));
std::cout << "sleep_for 2s" << std::endl;
//sleep_until()
auto wake_time = std::chrono::steady_clock::now() + std::chrono::seconds(2);
std::this_thread::sleep_until(wake_time);
std::cout << "sleep_until 2s" << std::endl;
t1.join();
printf("test end\n");
return 0;
}
输出:
chenglin@ubuntu:~/work/Thread$ g++ test1.cpp -o test1 -pthread;./test1
test start
main thread id :139971679848256
hello thread id :139971679844096
hello
duration1:2 duration2:130
sleep_for 2s
sleep_until 2s
test end
2.2 方法yield的作用
示例:对比yield/sleep_for死循环的CPU占用
//test2.cpp
#include <thread>
#include <iostream>
using namespace std;
int main() {
while(1);
return 0;
}
//test3.cpp
#include <thread>
#include <iostream>
using namespace std;
int main() {
while(1)
{
this_thread::yield();
}
return 0;
}
//test4.cpp
#include <thread>
#include <iostream>
using namespace std;
int main() {
while(1)
{
this_thread::sleep_for(chrono::milliseconds(100));
}
return 0;
}
输出:
chenglin@ubuntu:~/work/Thread$ ps -aux | grep 'test*'
kernoops 1142 0.0 0.0 24400 76 ? Ss Jun26 0:02 /usr/sbin/kerneloops --test
chenglin 2477 0.0 0.1 389580 2936 ? Sl Jun26 0:00 /usr/lib/gvfs/gvfsd-fuse /run/user/1000/gvfs -f -o big_writes
chenglin 12472 99.8 0.0 9728 1716 pts/1 R+ 08:56 4:08 ./test2
chenglin 12473 99.6 0.0 9728 1716 pts/2 R+ 08:57 4:02 ./test3
chenglin 12598 0.0 0.0 9732 1712 pts/4 S+ 08:57 0:00 ./test4
chenglin 12643 0.0 0.0 8124 992 pts/3 S+ 09:01 0:00 grep --color=auto test*
总结:在现代多线程编程中,std::this_thread::yield()
在忙等待场景中的效率通常较低,甚至可能比空循环更差。这是因为频繁的 yield()
会导致大量上下文切换开销,而现代 CPU 对空循环的优化(如指令预取、分支预测)可能使其在短时间内反而更高效。yield()
的原始设计初衷是用于协作式多任务,但在现代抢占式调度系统中,其适用场景已非常有限。在实际开发中,应根据等待时间长短和性能需求选择更合适的同步机制,避免盲目使用 yield()
导致性能反降。yield()
是一种底层线程控制工具,适用于特定的高性能场景或协作式系统。在日常开发中,应优先使用 C++ 标准库提供的高级同步机制(如 mutex
、condition_variable
、future
),它们更安全、更高效,且行为更可预测。只有在充分理解其局限性并通过性能测试验证后,才考虑使用 yield()
。
3.atomic
3.1 atomic的作用以及使用注意事项
3.1.1 atomic
的作用
- 原子操作保证:
atomic
类型提供了原子操作,确保对该类型变量的读、写和修改操作是不可分割的,要么全部执行,要么全部不执行。 - 线程同步的轻量级替代:相比传统的基于锁(如
std::mutex
)的同步机制,atomic
操作通常更轻量级。锁机制需要在进入和离开临界区时进行加锁和解锁操作,这可能涉及上下文切换等开销。 - 内存顺序控制:
atomic
类型允许指定不同的内存顺序(如memory_order_relaxed
、memory_order_seq_cst
等)。通过合理选择内存顺序,可以在保证线程安全的同时,优化程序性能。不同的内存顺序规定了操作在不同线程之间的可见性和执行顺序。
3.1.2 使用注意事项
-
类型局限性:并非所有类型都能直接声明为
atomic
。C++ 标准库仅支持基本算术类型(如int
、long
等)、指针类型以及某些用户定义类型(满足特定条件,如可平凡复制构造且不含非atomic
类型的非静态数据成员)可以声明为atomic
。如果需要对复杂类型进行原子操作,可能需要自行实现或使用更高级的同步机制。 -
操作局限性:虽然
atomic
类型仅提供了一些基本操作(如读、写、自增、自减等),但并非所有对普通变量的操作都有对应的原子版本。 -
内存顺序选择:选择合适的内存顺序至关重要。过于严格的内存顺序(如
memory_order_seq_cst
)可能会导致不必要的性能开销,因为它会对处理器的指令重排等优化进行更多限制。而过于宽松的内存顺序(如memory_order_relaxed
)可能会导致程序出现难以调试的并发问题,因为不同线程对操作的可见性和顺序可能不符合预期。在实际使用中,需要根据程序的具体需求和性能要求仔细选择内存顺序。 -
与其他同步机制的结合:在复杂的多线程场景中,
atomic
操作可能需要与其他同步机制(如锁、条件变量等)结合使用。例如,当需要对多个atomic
变量进行复合操作,且这些操作之间有依赖关系时,仅靠atomic
操作可能无法保证数据的一致性,此时可能需要使用锁来保护整个复合操作。示例:
#include <thread> #include <iostream> #include <atomic> using namespace std; #define MAX 100000 atomic<int> num(0); //int num = 0; //volatile int num = 0; void print_num() { for(int i = 0;i < MAX;i++) { num++; //num = num + 1; } } int main() { thread t1(print_num); thread t2(print_num); t1.join(); t2.join(); cout << "num :" << num <<endl; return 0; }
启用Line8输出:
chenglin@ubuntu:~/work/Thread$ g++ test7.cpp -o test7 -pthread;./test7;./test7;./test7 num :200000 num :200000 num :200000
启用Line9输出:
chenglin@ubuntu:~/work/Thread$ g++ test7.cpp -o test7 -pthread;./test7;./test7;./test7 num :120727 num :125055 num :130288
启用Line10输出:
chenglin@ubuntu:~/work/Thread$ g++ test7.cpp -o test7 -pthread;./test7;./test7;./test7 num :147818 num :105670 num :136935
将Line16代码替换成num = num + 1,输出:
chenglin@ubuntu:~/work/Thread$ g++ test7.cpp -o test7 -pthread;./test7;./test7;./test7 num :105532 num :104071 num :199654
总结:
volatile
关键字主要用于确保变量的读写操作按照预期进行,避免编译器优化带来的意外行为,同时在一定程度上保证多线程环境下变量的可见性,但它不能替代真正的线程同步机制来保证数据的一致性和原子性。atomic
类型**仅提供了一些基本操作(如读、写、自增、自减等)**的原子操作,所以编码时尽量使用其他同步机制(如锁、条件变量等)
4.mutex
4.1 常用方法
方法 | 功能 |
---|---|
lock | 资源上锁 lock状态下再次调用lock会造成死锁 |
unlock | 资源解锁 unlock状态下调用多次unlock不会造成死锁 |
try_lock | 查看是否上锁,它有下列3种类情况: (1)未上锁返回true,并锁住; (2)其他线程已经上锁,返回false; (3)同一个线程已经对它上锁,不会产生死锁。 |
示例1:演示常用方法多次调用的影响
#include <thread>
#include <iostream>
#include <mutex>
using namespace std;
mutex mtx;
int main() {
cout << "start ..." << endl;
bool isGetLock = mtx.try_lock();
cout << "unlock ---> try_lock | isGetLock1 : " << isGetLock << endl;
mtx.unlock();
cout << "lock -> unlock" << endl;
mtx.unlock();
cout << "unlock -> unlock" << endl;
mtx.unlock();
cout << "unlock -> unlock" << endl;
cout << endl;
mtx.lock();
cout << "lock" << endl;
isGetLock =mtx.try_lock();
cout << "lock ---> try_lock | isGetLock2 : " << isGetLock << endl;
mtx.unlock();
cout << "lock -> unlock" << endl;
cout << "endl ..." << endl;
return 0;
}
输出:
chenglin@ubuntu:~/work/Thread$ g++ test6.cpp -o test6 -pthread;./test6
start ...
unlock ---> try_lock | isGetLock1 : 1
lock -> unlock
unlock -> unlock
unlock -> unlock
lock
lock ---> try_lock | isGetLock2 : 0
lock -> unlock
endl ...
示例2:演示mutex保护共享资源,防止多个线程同时访问导致数据竞争
#include <thread>
#include <iostream>
#include <mutex>
using namespace std;
#define MAX 100
mutex mtx;
int i = 0;
void print_num(int index)
{
while(i < MAX)
{
mtx.lock();
cout << "thread" << index << " : " << i <<endl;
i++;
mtx.unlock();
}
}
int main() {
thread t1(print_num, 1);
thread t2(print_num, 2);
t1.join();
t2.join();
return 0;
}
输出:
chenglin@ubuntu:~/work/Thread$ g++ test4.cpp -o test4 -pthread;./test4
thread1 : 0
thread1 : 1
thread1 : 2
thread1 : 3
thread1 : 4
thread1 : 5
thread1 : 6
thread2 : 7
thread2 : 8
...........
............
thread1 : 95
thread2 : 96
thread2 : 97
thread2 : 98
thread2 : 99
thread1 : 100
4.2 trylock使用
#include <thread>
#include <iostream>
#include <mutex>
using namespace std;
#define MAX 100
mutex mtx;
mutex mtx1;
int i = 0;
void print_num(int index)
{
while(i < MAX)
{
if(mtx.try_lock())
{
mtx1.lock();
cout << "thread" << index << " : " << i << endl;
mtx1.unlock();
i++;
mtx.unlock();
}
else
{
mtx1.lock();
cout << "thread" << index << " get lock fail" << endl;
mtx1.unlock();
}
}
}
int main() {
thread t1(print_num, 1);
thread t2(print_num, 2);
t1.join();
t2.join();
return 0;
}
输出:
chenglin@ubuntu:~/work/Thread$ g++ test5.cpp -o test5 -pthread;./test5
thread1 : 0
thread1 : 1
thread2 get lock fail
thread2 get lock fail
.....................
thread2 get lock fail
thread2 get lock fail
thread2 get lock fail
thread1 : 50
thread1 : 51
thread1 : 52
thread1 : 53
............
thread2 : 99
thread1 get lock fail
**结论:**使用try_lock可以防止未获取到锁的线程一直处于阻塞状态
4.3 lock_guard与unique_lock的使用
4.3.1 lock_guard与unique_lock的异同点
特性 | std::lock_guard | std::unique_lock |
---|---|---|
基于 RAII | 是,构造时锁定互斥锁,析构时解锁 | 是,构造时可选择锁定或延迟锁定,析构时解锁 |
锁定方式 | 构造时立即锁定关联的互斥锁 | 可在构造时延迟锁定(std::defer_lock ),也可立即锁定;还能手动调用lock() 锁定 |
解锁方式 | 析构时自动解锁 | 析构时自动解锁;可手动调用unlock() 解锁 |
尝试锁定 | 无此功能 | 提供try_lock() 方法尝试锁定,不阻塞线程 |
锁所有权转移 | 不可转移 | 支持通过移动语义转移锁的所有权 |
灵活性 | 低,功能相对单一,主要用于简单的作用域内锁管理 | 高,支持多种锁操作,适用于复杂同步场景 |
性能 | 轻量级,性能开销小,适合简单场景 | 因支持更多功能,内部维护状态带来额外开销,在简单场景下性能略逊 |
能否在条件变量中使用 | 不能 | 能 |
4.3.2 lock_guard的使用
mutex mtx;
lock_guard<mutex> lock(mtx);
如果要锁定部分代码,可以用{}
{
lock_guard<mutex> lock(mtx);
//要处理的逻辑代码
}
4.3.3 unique_lock的等效写法
{
unique_lock<mutex> lock(mtx);
//要处理的逻辑代码
}
{
unique_lock<mutex> lock(mtx, std::defer_lock);
lock.lock();
//要处理的逻辑代码
}
{
unique_lock<mutex> lock(mtx, std::defer_lock);
lock.lock();
//要处理的逻辑代码
lock.unlock();
}
4.4 死锁产生/规避
示例1:同一线程中未解锁的情况下,重复加锁导致死锁,类似于嵌套锁
#include <thread>
#include <iostream>
#include <mutex>
using namespace std;
mutex mtx;
int main() {
cout << "start ..." << endl;
mtx.lock();
cout << "first lock ..." << endl;
mtx.lock();
cout << "second lock ..." << endl;
cout << "endl ..." << endl;
return 0;
}
输出:
chenglin@ubuntu:~/work/Thread$ g++ test6.cpp -o test6 -pthread;./test6
start ...
first lock ...
解决死锁问题,可以将Line6修改成recursive_mutex mtx,输出:
chenglin@ubuntu:~/work/Thread$ g++ test11.cpp -o test11 -pthread;./test11
start ...
first lock ...
second lock ...
endl ...
示例:不同线程中多个锁锁定顺序不一致导致死锁
#include <thread>
#include <iostream>
#include <mutex>
using namespace std;
mutex mtx1;
mutex mtx2;
void testDiedLock1()
{
mtx1.lock();
this_thread::sleep_for(100ms);
cout << "testDiedLock1 mtx1 lock" << endl;
mtx2.lock();
this_thread::sleep_for(100ms);
cout << "testDiedLock1 mtx2 lock" << endl;
mtx2.unlock();
cout << "testDiedLock1 mtx2 unlock" << endl;
mtx1.unlock();
cout << "testDiedLock1 mtx1 unlock" << endl;
}
void testDiedLock2()
{
mtx2.lock();
this_thread::sleep_for(100ms);
cout << "testDiedLock2 mtx2 lock" << endl;
mtx1.lock();
this_thread::sleep_for(100ms);
cout << "testDiedLock2 mtx1 lock" << endl;
mtx1.unlock();
cout << "testDiedLock2 mtx1 unlock" << endl;
mtx2.unlock();
cout << "testDiedLock2 mtx2 unlock" << endl;
}
int main() {
thread t1(testDiedLock1);
thread t2(testDiedLock2);
t1.join();
t2.join();
return 0;
}
输出:
chenglin@ubuntu:~/work/Thread$ ./test10
testDiedLock2 mtx2 lock
testDiedLock1 mtx1 lock
解决死锁问题,可以将Line41修改成thread t2(testDiedLock1),保证锁定顺序一致,输出:
chenglin@ubuntu:~/work/Thread$ g++ test10.cpp -o test10 -pthread;./test10
testDiedLock1 mtx1 lock
testDiedLock1 mtx2 lock
testDiedLock1 mtx2 unlock
testDiedLock1 mtx1 unlock
testDiedLock1 mtx1 lock
testDiedLock1 mtx2 lock
testDiedLock1 mtx2 unlock
testDiedLock1 mtx1 unlock
5.condition_variable
5.1 常用方法
方法 | 函数原型 | 功能描述 |
---|---|---|
wait | void wait(std::unique_lock<std::mutex>& lock); template<class Predicate> void wait(std::unique_lock<std::mutex>& lock, Predicate pred); | - 第一个版本:自动释放传入的锁,阻塞当前线程,直到被notify_one 或notify_all 唤醒,唤醒后重新获取锁。 - 第二个版本:在循环中等待,直到谓词 pred 为true ,同样自动处理锁的释放与获取。 |
wait_for | template<class Rep, class Period> std::cv_status wait_for(std::unique_lock<std::mutex>& lock, const std::chrono::duration<Rep, Period>& rel_time); template<class Rep, class Period, class Predicate> bool wait_for(std::unique_lock<std::mutex>& lock, const std::chrono::duration<Rep, Period>& rel_time, Predicate pred); | - 第一个版本:等待被通知或等待指定时长rel_time ,自动释放和重新获取锁,返回等待结果。 - 第二个版本:在循环中等待,直到谓词 pred 为true 或等待超时,返回谓词是否为true 。 |
wait_until | template<class Clock, class Duration> std::cv_status wait_until(std::unique_lock<std::mutex>& lock, const std::chrono::time_point<Clock, Duration>& abs_time); template<class Clock, class Duration, class Predicate> bool wait_until(std::unique_lock<std::mutex>& lock, const std::chrono::time_point<Clock, Duration>& abs_time, Predicate pred); | - 第一个版本:等待被通知或等待到指定时间点abs_time ,自动处理锁的操作并返回等待结果。 - 第二个版本:在循环中等待,直到谓词 pred 为true 或到达指定时间点,返回谓词是否为true 。 |
notify_one | void notify_one() noexcept; | 唤醒一个正在等待该条件变量的线程,若有多个等待线程则随机选择一个唤醒,被唤醒线程尝试重新获取锁后继续执行。 |
notify_all | void notify_all() noexcept; | 唤醒所有正在等待该条件变量的线程,被唤醒线程均尝试重新获取锁,获取到锁的线程继续执行,未获取到的继续等待。 |
示例:使用condition_variable打印多个线程id
#include <iostream>
#include <mutex>
#include <condition_variable>
#include <atomic>
#include <thread>
using namespace std;
#define THREAD_NUM 10
mutex mtx;
bool isReady = false;
condition_variable cv;
void print_id(int index)
{
unique_lock<mutex> lock(mtx);
cout << "thread" << index << " wait..." << endl;
cv.wait(lock,[]{return isReady;});
cout << "thread" << index << " complete" << endl;
//cv.notify_one();
}
int main()
{
thread th[10];
for(int i = 0;i < THREAD_NUM;i++)
{
th[i] = thread(print_id, i);
}
this_thread::sleep_for(100ms);
unique_lock<mutex> lock(mtx);
isReady = true;
//cv.notify_one();
cv.notify_all();
lock.unlock();
for(int i = 0;i < THREAD_NUM;i++)
{
th[i].join();
}
return 0;
}
输出:
chenglin@ubuntu:~/work/Thread$ g++ test12.cpp -o test12 -pthread;./test12
thread0 wait...
thread5 wait...
thread1 wait...
thread6 wait...
thread3 wait...
thread4 wait...
thread2 wait...
thread7 wait...
thread8 wait...
thread9 wait...
thread2 complete
thread8 complete
thread3 complete
thread6 complete
thread4 complete
thread9 complete
thread1 complete
thread7 complete
thread5 complete
thread0 complete
6.补充知识点
- 线程的状态及状态切换
- 信号量