文章目录
一、主线程阻塞等待子线程返回
主线程同步等待一个线程,此线程会开始连接一个服务器并循环读取服务器存储的值,主线程会阻塞直到连接服务器成功
。因为如果不阻塞,可能上层业务刚开始读不到数据。
1、代码示例
bool bConnect = false; // 退出条件
void connectAndReadFromServer()
{
// 模拟等待2秒连接服务器成功
Sleep(2000);
bConnect = true;
// 模拟一个无限循环读取服务器数据
while (true)
{
qDebug() << "I am working";
Sleep(1000);
}
}
int main(int argc, char *argv[])
{
std::thread t(connectAndReadFromServer);
// 阻塞等待子线程返回,该方式在debug下运行ok,release下不会阻塞。
while (!bConnect)
{
}
qDebug() << "main thread continue, start read " << bConnect;
return 0;
}
坑点:上述代码在debug下运行是没有问题的,因为编译器会关闭大部分优化,目的是保留代码的原始行为,方便调试。 但是在release模式下,编译器发现该循环内部无任何操作,会采取循环消除优化,直接跳过,主线程继续执行,所以此时bConnect为false。
2、代码改进(while循环)
int main()
{
int num = 0;
std::thread t(connectAndReadFromServer);
while (!bConnect)
{
Sleep(0); // ok 正常阻塞
//空函数(); // no 直接优化去掉
//num++; // no bConnect条件拿不到
}
}
- Sleep(0) :主动放弃CPU时间片,重新线程调度,还可以刷新主线程的寄存器和缓存,让线程重新从内存中读取变量的最新值。
- 空函数() :在release下,该循环不执行,直接被编译器优化掉。
- 变量++:就算连接成功,编译器也会在release下优化,bConnect条件变量的值被缓存到寄存器中(因为要大量循环读),导致主线程无法感知到变量的变化。循环一直执行。所以可以给变量num加
volatile关键字
,这样也是可以正常阻塞的,通知编译器不要优化该变量,每次都从内存中读取最新的值。
tips:方式依然不太好。while循环会一直大量占用CPU,效率较低,不断地检查某个条件以查看子线程是否已经完成并产生了结果。并且随着代码复杂度增加,手动管理线程同步和通信很容易出错,特别是在多线程环境中。
3、代码改进(std::future 和 std::promise)
void connectAndReadFromServer(std::promise<int> pe)
{
Sleep(2000);
pe.set_value(9999); // 立即返回结果,pe只能设置一次
while (true)
{
qDebug() << "I am continuing to work";
Sleep(1000);
}
}
int main(int argc, char *argv[])
{
std::promise<int> result_promise;
std::future<int> result = result_promise.get_future();
std::thread t(connectAndReadFromServer, std::move(result_promise));
std::future_status status = result.wait_for(std::chrono::seconds(5)); // 设置阻塞主线程五秒钟后超时
if (status == std::future_status::ready)
{
qDebug() << result.get(); // 等待2秒钟后,获得结果,提前结束阻塞主线程。
}
else
{
qDebug() << "time out"; // 当子线程从Sleep 休眠2秒到10秒时,主线程等待5秒钟后,还获取不到值,会触发超时机制,结束阻塞主线程。
}
qDebug() << "main thread continue";
Sleep(1000 * 1000);
return 0;
}
- 运行结果:两秒钟后,主线程获取到子线程的值,输出9999,输出"main thread continue",子线程每隔一秒循环输出"I am continuing to work"。
- std::future 和 std::promise:是C++11中引入的用于线程间同步和通信的类。是线程安全的。
- 优点:会阻塞主线程等待异步结果,可以在不浪费CPU资源的情况下等待子线程的结果。而且还可以捕获到异常。也大大简化了线程间通信和同步的代码。
二、主线程创建三个子线程并传参,同步与互斥运行。
老板有三个工人,每个人每隔1s赚一块。
1、代码示例(无同步无互斥)
void connectAndReadFromServer(const std::string& name, int age)
{
static int nRMB = 0; // 老板总共赚了多少钱
while (true)
{
qDebug() << "I am working" << name.c_str() << age << nRMB++;
Sleep(1000);
}
}
int main(int argc, char *argv[])
{
std::thread t1(connectAndReadFromServer, "sunwukong", 24);
std::thread t2(connectAndReadFromServer, "huangzhong", 56);
std::thread t3(connectAndReadFromServer, "zhugeliang", 35);
qDebug() << "main thread continue ";
Sleep(1000 * 1000);
return 0;
}
坑点:可能某个时间点,两个线程同时修改变量的值,比如之前是20,然后++,两个都是21。
2、代码改进(互斥无同步 - std::mutex)
std::mutex = mtx;
void connectAndReadFromServer(const std::string& name, int age)
{
static int nRMB = 0; // 老板总共赚了多少钱
while (true)
{
// std::lock_guard<std::mutex> lock(mtx); // no
// 不能写到括号外面,否则,只会运行一个线程,while循环内,锁出不了作用域,当前会一直占有。
{
std::lock_guard<std::mutex> lock(mtx); // yes
qDebug() << "I am working" << name.c_str() << age << nRMB++;
}
Sleep(1000);
}
}
int main(int argc, char *argv[])
{
std::thread t1(connectAndReadFromServer, "sunwukong", 24);
std::thread t2(connectAndReadFromServer, "huangzhong", 56);
std::thread t3(connectAndReadFromServer, "zhugeliang", 35);
qDebug() << "main thread continue ";
Sleep(1000 * 1000);
return 0;
}
- 为了防止多个线程同时访问导致的数据不一致问题,可以使用
std::lock_guard<std::mutex>
,用于简化互斥锁(mutex)的管理,确保互斥锁在作用域开始时被自动锁定,在作用域结束时被自动释放。这样做可以避免手动管理锁的锁定和解锁过程,减少因忘记解锁而导致的死锁风险。- 当然,如果想手动管理锁,提前在出作用域前释放锁,可以使用
std::unique_lock<std::mutex>
上锁,想释放时使用lock.unlock()
。tips:
·多线程中,虽然按序创建三个线程,但是并不是按序执行,依赖系统调度,只能保证在一定时间内,都执行,也就是雨露均沾。但是可以使用条件变量,使其同步执行。
3、代码改进(同步和互斥 - std::condition_variable)
std::mutex = mtx;
std::condition_variable cv;
int current_thread_index = -1; // -1 表示没有线程正在执行
void connectAndReadFromServer(const std::string& name, int age, int thread_id)
{
static int nRMB = 0; // 老板总共赚了多少钱
while (true)
{
{
std::lock_guard<std::mutex> lock(mtx);
cv.wait(lock, [&](){ return current_thread_index = thread_id});
qDebug() << "I am working" << name.c_str() << age << nRMB++;
current_thread_index = (current_thread_index + 1) % 3;
cv.notify_all();
lock.unlock();
}
Sleep(1000);
}
}
int main(int argc, char *argv[])
{
std::thread t1(connectAndReadFromServer, "sunwukong", 24, 0);
std::thread t2(connectAndReadFromServer, "huangzhong", 56, 1);
std::thread t3(connectAndReadFromServer, "zhugeliang", 35, 2);
// 启动第一个线程
{
std::lock_guard<std::mutex> lock(m_mutex);
current_thread_index = 0; // 设置初始执行的线程索引
}
t1.join();
t2.join();
t3.join();
qDebug() << "main thread continue "; // 由于线程进行join状态,并且线程循环执行,没有退出条件,所以该日志不会输出。
return 0;
}
- 1、这样实现了线程的同步与互斥,按序执行,数据一致。
- 2、
join()
:同步等待,主线程会阻塞,直到被join的线程完成执行。- 3、
detach()
:异步不等待,主线程不会等待子线程完成,会继续执行。detach方法允许线程在后台独立运行。但主线程结束后,detach也会立即结束。- 4、
既不join也不detach
:main函数执行完后,t 线程离开作用域时,程序将调用std::terminate()并异常终止。