【C++】多线程(thread)使用详解

这篇具有很好参考价值的文章主要介绍了【C++】多线程(thread)使用详解。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

一、前言

1. 多线程的含义

多线程(multithreading),是指在软件或者硬件上实现多个线程并发执行的技术。具有多核CPU的支持的计算机能够真正在同一时间执行多个程序片段,进而提升程序的处理性能。在一个程序中,这些独立运行的程序片段被称为“线程”(Thread),利用其编程的概念就叫作“多线程处理”。

2. 进程与线程的区别

进程是指一个程序的运行实例,而线程是指进程中独立的执行流程。一个进程可以有多个线程,多个线程之间可以并发执行。

  • 一个程序有且只有一个进程,但可以拥有至少一个的线程。
  • 不同进程拥有不同的地址空间,互不相关,而不同线程共同拥有相同进程的地址空间。

二、创建线程

1. thread

C++支持多线程编程,主要使用的是线程库<thread>

示例1: 创建线程使用std::thread

#include <iostream>
#include <thread>    //必须包含<thread>头文件

void threadFunctionA()
{
	std::cout << "Run New thread: 1" << std::endl;
}
void threadFunctionB(int n)
{
	std::cout << "Run New thread: "<< n << std::endl;
}

int main()
{
	std::cout << "Run Main Thread" << std::endl;

	std::thread newThread1(threadFunctionA);
	std::thread newThread2(threadFunctionB,2);

	newThread1.join();
	newThread2.join();

	return 0;
}
//result
Run Main Thread
Run New thread: 1
Run New thread: 2

上述示例中,我们创建了两个线程newThread1newThread2,使用函数threadFunctionA()threadFunctionB()作为线程的执行函数,并使用join()函数等待线程执行完成。

示例2: 执行函数有引用参数

#include <iostream>
#include <thread>    //必须包含<thread>头文件

void threadFunc(int &arg1, int arg2)
{
	arg1 = arg2;
	std::cout << "arg1 = " << arg1 << std::endl;
}

int main()
{
    std::cout << "Run Main Thread!" << std::endl;
	int a = 1;
	int b = 5;
	std::thread newTh(threadFunc, a, b);  //此处会报错
	newTh.join();
	return 0;
}

注意: 若编译上述代码,编译器会报如下错误:

错误	C2672	“std::invoke”: 未找到匹配的重载函数
错误	C2893	未能使函数模板“unknown-type std::invoke(_Callable &&,_Types &&...) noexcept(<expr>)”专用化

这是因为thread在传递参数时,是以右值传递的,如果要传递一个左值可以使用std::refstd::cref

  • std::ref 可以包装按引用传递的值为右值。
  • std::cref 可以包装按const引用传递的值为右值。

因此,示例2代码可修改为:

#include <iostream>
#include <thread>    //必须包含<thread>头文件

void threadFunc(int &arg1, int arg2)
{
	arg1 = arg2;
	std::cout << "New Thread arg1 = " << arg1 << std::endl;
}

int main()
{
	std::cout << "Run Main Thread!" << std::endl;
	int a = 1, b = 5;
	std::thread newTh(threadFunc, std::ref(a), b);  //使用ref
	newTh.join();
	return 0;
}
//result
Run Main Thread!
arg1 = 5

2. join() 和 detach()

在C++中,创建了一个线程时,它通常被称为一个可联接(joinable)的线程,可以通过调用join()函数或detach()函数来管理线程的执行。

方法 说明
1 join() 等待一个线程完成,如果该线程还未执行完毕,则当前线程(一般是主线程)将被阻塞,直到该线程执行完成,主线程才会继续执行。
2 detach() 将当前线程与创建的线程分离,使它们分别运行,当分离的线程执行完毕后,系统会自动回收其资源。如果一个线程被分离了,就不能再使用join()函数了,因为线程已经无法被联接了。
3 joinable() 判断线程是否可以执行join()函数,返回true/false

示例3:

#include <iostream>
#include <thread>
#include <windows.h>

void foo()
{
	std::cout << "Run New thread!\n";
	Sleep(2000);   		//需要头文件<windows.h>
}

int main()
{
	std::thread t(foo);

	if (t.joinable())
	{
		t.join();  		// 等待线程t执行完毕

		// t.detach();  // 分离线程t与主线程
	}

	std::cout << "Run Main thread!\n";
	return 0;
}

在上述的示例中,创建了一个可联接的线程t,使用t.join()主线程将被阻塞,直到t线程执行完毕。如果使用t.detach()t线程分离,那么它们将同时执行,主线程将不会阻塞。

注意:

  • 线程是在thread对象被定义的时候开始执行的,而不是在调用join()函数时才执行的,调用join()函数只是阻塞等待线程结束并回收资源。
  • 分离的线程(执行过detach()的线程)会在调用它的线程结束或自己结束时自动释放资源。
  • 线程会在函数运行完毕后自动释放,不推荐利用其他方法强制结束线程,可能会因资源未释放而导致内存泄漏。
  • 若没有执行join()detach()的线程在程序结束时会引发异常。

3. this_thread

在C++中,this_thread类提供了一些关于当前线程的功能函数。具体如下:

使用 说明
1 std::this_thread::sleep_for() 当前线程休眠指定的时间
2 std::this_thread::sleep_until() 当前线程休眠直到指定时间点
3 std::this_thread::yield() 当前线程让出CPU,允许其他线程运行
4 std::this_thread::get_id() 获取当前线程的ID

此外,this_thread还包含重载运算符==!=,用于比较两个线程是否相等。

示例4:

#include <iostream>
#include <thread>
#include <chrono>

void my_thread()
{
	std::cout << "Thread " << std::this_thread::get_id() << " start!" << std::endl;

	for (int i = 1; i <= 5; i++)
	{
		std::cout << "Thread " << std::this_thread::get_id() << " running: " << i << std::endl;
		std::this_thread::yield();	// 让出当前线程的时间片
		std::this_thread::sleep_for(std::chrono::milliseconds(200));  // 线程休眠200毫秒
	}

	std::cout << "Thread " << std::this_thread::get_id() << " end!" << std::endl;
}

int main()
{
	std::cout << "Main thread id: " << std::this_thread::get_id() << std::endl;
	
    std::thread t1(my_thread);
	std::thread t2(my_thread);
	
	t1.join();
	t2.join();
	return 0;
}
//result 程序输出的结果可能如下:
Main thread id: 43108
Thread 39272 start!
Thread 33480 start!
Thread 33480 running: 1
Thread 39272 running: 1
Thread 33480 running: 2
Thread 39272 running: 2
Thread 33480 running: 3
Thread 39272 running: 3
Thread 33480 running: 4
Thread 39272 running: 4
Thread 39272 running: 5
Thread 33480 running: 5
Thread 39272 ends
Thread 33480 ends

三、std::mutex

在多线程编程中,需要注意以下问题:

  • 线程之间的共享数据访问需要进行同步,以防止数据竞争和其他问题。可以使用互斥量条件变量等机制进行同步。
  • 可能会发生死锁问题,即多个线程互相等待对方释放锁,导致程序无法继续执行。
  • 可能会发生竞态条件问题,即多个线程执行的顺序导致结果的不确定性。

1. lock() 与 unlock()

std::mutex是 C++11 中最基本的互斥量,一个线程将mutex锁住时,其它的线程就不能操作mutex,直到这个线程将mutex解锁。

方法 说明
1 lock() 将mutex上锁。如果mutex已经被其它线程上锁,那么会阻塞,直到解锁;如果mutex已经被同一个线程锁住,那么会产生死锁
2 unlock() 将mutex解锁,释放其所有权。如果有线程因为调用lock()不能上锁而被阻塞,则调用此函数会将mutex的主动权随机交给其中一个线程;如果mutex不是被此线程上锁,那么会引发未定义的异常。
3 try_lock() 尝试将mutex上锁。如果mutex未被上锁,则将其上锁并返回true;如果mutex已被锁则返回false

示例: 使用互斥量

#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx;
int num = 0;

void thread_function(int &n)
{
	for (int i = 0; i < 100; ++i)
	{
		mtx.lock();
		n++;
		mtx.unlock();
	}
}

int main()
{
	std::thread myThread[500];
	for (std::thread &a : myThread)
	{
		a = std::thread(thread_function, std::ref(num));
		a.join();
	}

	std::cout << "num = " << num << std::endl;
	std::cout << "Main thread exits!" << std::endl;
	return 0;
}
//result
num = 50000
Main thread exits!

注意: 在使用互斥量时,需要注意以下问题:

  • 加锁和解锁的顺序必须相同。
  • 不能在未获得锁的情况下对共享数据进行操作。
  • 由于使用了 std::mutex 来控制对共享资源的访问,因此可能会对程序的性能造成影响,如果需要优化程序性能,可以考虑使用无锁编程等技术。

2. lock_guard

std::lock_guard是C++标准库中的一个模板类,用于实现资源的自动加锁和解锁。它是基于RAII(资源获取即初始化)的设计理念,能够确保在作用域结束时自动释放锁资源,避免了手动管理锁的复杂性和可能出现的错误。

std::lock_guard的主要特点如下:

  • 自动加锁: 在创建std::lock_guard对象时,会立即对指定的互斥量进行加锁操作。这样可以确保在进入作用域后,互斥量已经被锁定,避免了并发访问资源的竞争条件。
  • 自动解锁:std::lock_guard对象在作用域结束时,会自动释放互斥量。无论作用域是通过正常的流程结束、异常抛出还是使用return语句提前返回,std::lock_guard都能保证互斥量被正确解锁,避免了资源泄漏和死锁的风险。
  • 适用于局部锁定: 由于std::lock_guard是通过栈上的对象实现的,因此适用于在局部范围内锁定互斥量。当超出std::lock_guard对象的作用域时,互斥量会自动解锁,释放控制权。

使用std::lock_guard的一般步骤如下:

  1. 创建一个std::lock_guard对象,传入要加锁的互斥量作为参数。
  2. 执行需要加锁保护的代码块。
  3. std::lock_guard对象的作用域结束时,自动调用析构函数解锁互斥量。

示例:

#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx;  // 互斥量

void thread_function()
{
    std::lock_guard<std::mutex> lock(mtx);  // 加锁互斥量
    std::cout << "Thread running" << std::endl;
    // 执行需要加锁保护的代码
}  // lock_guard对象的析构函数自动解锁互斥量

int main()
{
    std::thread t1(thread_function);
    t1.join();
    std::cout << "Main thread exits!" << std::endl;
    return 0;
}

在上述示例中,std::lock_guard对象lock会在thread_function中加锁互斥量,保护了输出语句的执行。当thread_function结束时,lock_guard对象的析构函数会自动解锁互斥量。这样可以确保互斥量在合适的时候被锁定和解锁,避免了多线程间的竞争问题。

总而言之,std::lock_guard提供了一种简单而安全的方式来管理互斥量的锁定和解锁,使多线程编程更加方便和可靠。


3. unique_lock

std::unique_lock是C++标准库中的一个模板类,用于实现更加灵活的互斥量的加锁和解锁操作。它提供了比std::lock_guard更多的功能和灵活性。

std::unique_lock的主要特点如下:

  • 自动加锁和解锁:std::lock_guard类似,std::unique_lock在创建对象时立即对指定的互斥量进行加锁操作,确保互斥量被锁定。在对象的生命周期结束时,会自动解锁互斥量。这种自动加锁和解锁的机制避免了手动管理锁的复杂性和可能出现的错误。

  • 支持灵活的加锁和解锁: 相对于std::lock_guard的自动加锁和解锁,std::unique_lock提供了更灵活的方式。它可以在需要的时候手动加锁和解锁互斥量,允许在不同的代码块中对互斥量进行多次加锁和解锁操作。

  • 支持延迟加锁和条件变量:std::unique_lock还支持延迟加锁的功能,可以在不立即加锁的情况下创建对象,稍后根据需要进行加锁操作。此外,它还可以与条件变量(std::condition_variable)一起使用,实现更复杂的线程同步和等待机制。

使用std::unique_lock的一般步骤如下:

  1. 创建一个std::unique_lock对象,传入要加锁的互斥量作为参数。
  2. 执行需要加锁保护的代码块。
  3. 可选地手动调用lock函数对互斥量进行加锁,或者在需要时调用unlock函数手动解锁互斥量。

示例:

#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx;  // 互斥量

void thread_function()
{
    std::unique_lock<std::mutex> lock(mtx);  // 加锁互斥量
    std::cout << "Thread running" << std::endl;
    // 执行需要加锁保护的代码
    lock.unlock();  // 手动解锁互斥量
    // 执行不需要加锁保护的代码
    lock.lock();  // 再次加锁互斥量
    // 执行需要加锁保护的代码
}  
// unique_lock对象的析构函数自动解锁互斥量

int main()
{
    std::thread t1(thread_function);
    t1.join();
    std::cout << "Main thread exits!" << std::endl;
    return 0;
}

在上述示例中,std::unique_lock对象lock会在创建时自动加锁互斥量,析构时自动解锁互斥量。我们可以通过调用lockunlock函数手动控制加锁和解锁的时机,以实现更灵活的操作。

总而言之,std::unique_lock提供了更灵活和功能丰富的互斥量的加锁和解锁机制,使多线程编程更加便捷和安全。它在处理复杂的同步需求、延迟加锁以及与条件变量的结合等方面非常有用。


四、condition_variable

std::condition_variable是C++标准库中的一个类,用于在多线程编程中实现线程间的条件变量和线程同步。它提供了等待通知的机制,使得线程可以等待某个条件成立时被唤醒,或者在满足某个条件时通知其他等待的线程。其提供了以下几个函数用于等待和通知线程:

方法 说明
1 wait 使当前线程进入等待状态,直到被其他线程通过notify_one()notify_all()函数唤醒。该函数需要一个互斥锁作为参数,调用时会自动释放互斥锁,并在被唤醒后重新获取互斥锁。
2 wait_for wait_for(): 使当前线程进入等待状态,最多等待一定的时间,直到被其他线程通过notify_one()notify_all()函数唤醒,或者等待超时。该函数需要一个互斥锁和一个时间段作为参数,返回时有两种情况:等待超时返回std::cv_status::timeout,被唤醒返回std::cv_status::no_timeout
3 wait_until wait_until(): 使当前线程进入等待状态,直到被其他线程通过notify_one()notify_all()函数唤醒,或者等待时间达到指定的绝对时间点。该函数需要一个互斥锁和一个绝对时间点作为参数,返回时有两种情况:时间到达返回std::cv_status::timeout,被唤醒返回std::cv_status::no_timeout
4 notify_one notify_one(): 唤醒一个等待中的线程,如果有多个线程在等待,则选择其中一个线程唤醒。
5 notify_all notify_all(): 唤醒所有等待中的线程,使它们从等待状态返回。

std::condition_variable的主要特点如下:

  • 等待和通知机制:std::condition_variable允许线程进入等待状态,直到某个条件满足时才被唤醒。线程可以调用wait函数进入等待状态,并指定一个互斥量作为参数,以确保线程在等待期间互斥量被锁定。当其他线程满足条件并调用notify_onenotify_all函数时,等待的线程将被唤醒并继续执行。

  • 与互斥量配合使用:std::condition_variable需要与互斥量(std::mutexstd::unique_lock<std::mutex>)配合使用,以确保线程之间的互斥性。在等待之前,线程必须先锁定互斥量,以避免竞争条件。当条件满足时,通知其他等待的线程之前,必须再次锁定互斥量。

  • 支持超时等待:std::condition_variable提供了带有超时参数的等待函数wait_forwait_until,允许线程在等待一段时间后自动被唤醒。这对于处理超时情况或限时等待非常有用。

使用std::condition_variable的一般步骤如下:

  1. 创建一个std::condition_variable对象。
  2. 创建一个互斥量对象(std::mutexstd::unique_lock<std::mutex>)。
  3. 在等待线程中,使用std::unique_lock锁定互斥量,并调用wait函数进入等待状态。
  4. 在唤醒线程中,使用std::unique_lock锁定互斥量,并调用notify_onenotify_all函数通知等待的线程。
  5. 等待线程被唤醒后,继续执行相应的操作。

示例:

#include <iostream>
#include <thread>
#include <condition_variable>

std::mutex mtx;  // 互斥量
std::condition_variable cv;  // 条件变量
bool isReady = false;  // 条件

void thread_function()
{
    std::unique_lock<std::mutex> lock(mtx);
    while (!isReady) 
    {
        cv.wait(lock);  // 等待条件满足
    }
    std::cout << "Thread is notified" << std::endl;
}

int main()
{
    std::thread t(thread_function);

    // 模拟一段耗时操作
    std::this_thread::sleep_for(std::chrono::seconds(2));

    {
        std::lock_guard<std::mutex> lock(mtx);
        isReady = true;  // 设置条件为true
    }
    cv.notify_one();  // 通知等待的线程

    t.join();

    return 0;
}

上述示例中,创建了一个线程,该线程在等待状态下通过cv.wait(lock)等待条件满足。主线程经过一段时间后将条件设置为true,然后通过cv.notify_one()通知等待的线程。等待的线程被唤醒后输出一条消息。


五、std::atomic

std::mutex可以很好地解决多线程资源争抢的问题,但它每次循环都要加锁、解锁,这样固然会浪费很多的时间。

在 C++ 中,std::atomic 是用来提供原子操作的类,atomic,本意为原子,原子操作是最小的且不可并行化的操作。这就意味着即使是多线程,也要像同步进行一样同步操作原子对象,从而省去了互斥量上锁、解锁的时间消耗。

使用 std::atomic 可以保证数据在操作期间不被其他线程修改,这样就避免了数据竞争,使得程序在多线程并发访问时仍然能够正确执行。

示例:

#include <iostream>
#include <thread>
#include <mutex>
#include <atomic>   //必须包含

std::atomic_int num = 0;

void thread_function(std::atomic_int &n)  //修改类型
{
	for (int i = 0; i < 100; ++i)
	{
		n++;
	}
}

int main()
{
	std::thread myThread[500];
	for (std::thread &a : myThread)
	{
		a = std::thread(thread_function, std::ref(num));
		a.join();
	}

	std::cout << "num = " << num << std::endl;
	std::cout << "Main thread exits!" << std::endl;
	return 0;
}
//result
num = 50000
Main thread exits!

说明:std::atomic_intstd::atomic<int>的别名。


如果这篇文章对你有所帮助,渴望获得你的一个点赞!

c++ thread,编程概念,C++,c++,开发语言,面试文章来源地址https://www.toymoban.com/news/detail-625797.html

到了这里,关于【C++】多线程(thread)使用详解的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • C++之C++11 thread线程示例(一百三十八)

    简介: CSDN博客专家,专注Android/Linux系统,分享多mic语音方案、音视频、编解码等技术,与大家一起成长! 优质专栏: Audio工程师进阶系列 【 原创干货持续更新中…… 】🚀 人生格言: 人生从来没有捷径,只有行动才是治疗恐惧和懒惰的唯一良药. 更多原创,欢迎关注:An

    2023年04月15日
    浏览(47)
  • C++ thread编程(Linux系统为例)—thread成员函数

    c++ 11 之后有了标准的线程库:std::thread。 参考thread库的使用 thread的构造函数有下面四个重载 默认构造函数 初始化构造函数 该函数使用可变参数模板来构造一个线程对象,用来代表一个新的可join的执行线程。这个执行线程通过可变参数传入线程函数对象fn,以及函数的参数

    2024年02月05日
    浏览(42)
  • C++多线程编程(第三章 案例1,使用互斥锁+ list模拟线程通信)

    主线程和子线程进行list通信,要用到互斥锁,避免同时操作 1、封装线程基类XThread控制线程启动和停止; 2、模拟消息服务器线程,接收字符串消息,并模拟处理; 3、通过Unique_lock和mutex互斥方位list 消息队列 4、主线程定时发送消息给子线程; 代码包含了XThread类(基类)、

    2024年02月15日
    浏览(36)
  • C++并发线程 - 如何线程间共享数据【详解:如何使用锁操作】

    点击进入系列文章目录 C++技能系列 Linux通信架构系列 C++高性能优化编程系列 深入理解软件架构设计系列 高级C++并发线程编程 期待你的关注哦!!! 快乐在于态度,成功在于细节,命运在于习惯。 Happiness lies in the attitude, success lies in details, fate is a habit. 具体哪个线程按何种

    2024年02月08日
    浏览(41)
  • 【C++】详解std::thread

    2023年9月10日,周日下午开始 2023年9月10日,周日晚上23:35完成 虽然这篇博客我今天花了很多时间去写,但是我对std::thread有了一个完整的认识 不过有些内容还没完善,以后有空再更新.... 目录 头文件 类的成员 类型 方法 (constructor) terminate called without an active exception是什么?

    2024年02月09日
    浏览(45)
  • Linux系统编程5(线程概念详解)

    线程同进程一样都是OS中非常重要的部分,线程的应用场景非常的广泛,试想我们使用的视频软件,在网络不是很好的情况下,通常会采取下载的方式,现在你很想立即观看,又想下载,于是你点击了下载并且在线观看。学过进程的你会不会想,视频软件运行后在OS内形成一个

    2024年02月10日
    浏览(41)
  • 多线程系列(二) -Thread类使用详解

    在之前的文章中,我们简单的介绍了线程诞生的意义和基本概念,采用多线程的编程方式,能充分利用 CPU 资源,显著的提升程序的执行效率。 其中 java.lang.Thread 是 Java 实现多线程编程最核心的类,学习 Thread 类中的方法,是学习多线程的第一步。 下面我们就一起来看看,创

    2024年02月19日
    浏览(45)
  • C++ 多线程编程和线程池

    c++ 多线程需要包含thread头文件 多线程调用函数代码如下 子线程和主线程同时执行,当子线程没执行完,主线程结束时,程序就会报错 join,主线程等待子线程执行完成后再执行,只能使用一次 detach,主程序结束,子线程会在后台执行 joinable,判断线程是否可以调用join和de

    2024年01月20日
    浏览(78)
  • C++线程入门:轻松并发编程

            在现代计算机应用程序中,我们经常需要处理并发任务,这就需要使用多线程来实现。C++是一种功能强大的编程语言,提供了丰富的线程支持,使得并发编程变得相对容易。         C++ 线程是一种多线程编程模型,可以在同一个程序中同时执行多个独立的任务

    2024年02月04日
    浏览(40)
  • C++ 多线程编程(三) 获取线程的返回值——future

    C++11标准库增加了获取线程返回值的方法,头文件为future,主要包括 future 、 promise 、 packaged_task 、 async 四个类。 那么,了解一下各个类的构成以及功能。 future是一个模板类,它是传输线程返回值(也称为 共享状态 )的媒介,也可以理解为线程返回的结果就安置在future中。

    2024年02月02日
    浏览(45)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包