C++ 线程常见的实际场景解决方案

一、主线程阻塞等待子线程返回

主线程同步等待一个线程,此线程会开始连接一个服务器并循环读取服务器存储的值,主线程会阻塞直到连接服务器成功。因为如果不阻塞,可能上层业务刚开始读不到数据。

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()并异常终止。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值