首页 > 编程语言 >C++多线程编程

C++多线程编程

时间:2024-11-18 14:22:03浏览次数:1  
标签:std thread lock 编程 C++ 互斥 线程 mutex 多线程

一、概念

多线程(英语:multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。

传统的C++(C++11标准之前)中并没有引入线程这个概念,在C++11出来之前,如果我们想要在C++中实现多线程,需要借助操作系统平台提供的API,比如Linux的<pthread.h>,或者windows下的<windows.h> 。

1.1 其他相关概念介绍

  • 软件多线程:即便处理器只能运行一个线程,操作系统也可以通过快速的在不同线程之间进行切换,由于时间间隔很小,来给用户造成一种多个线程同时运行的假象;
  • 进程:是指计算机中已运行的程序;
  • 并行:并列运行,多个CPU下支持;
  • 并发:多个任务请求运行,CPU轮流交替运行。

1.2 多线程常见的方式

1.使用标准库提供的thread类:C++11引入了std::thread类,可以用于创建和管理线程。通过包含头文件,并使用std::thread对象来创建新的线程。
2.继承自std::thread的子类:你也可以通过继承std::thread类来创建自己的线程类,以便更好地封装和管理线程。
3.POSIX线程库(pthread):这是一套用于跨平台多线程编程的API,可以在Linux、UNIX等系统上使用。需要包含<pthread.h>头文件,并使用pthread_create函数创建新的线程。
4.使用C++11的std::async和std::future:这种方式可以在一个函数中并发执行多个任务,并获得它们的结果。通过std::async函数创建异步任务,返回一个std::future对象,然后使用std::future来获取结果。
5.OpenMP

二、std::thread类

2.1 创建线程

1.创建线程需要引入头文件 #include;
2.语句"std::thread th1(proc1);"创建了一个名为th1的线程,并且线程th1开始执行;

实例化std::thread类对象时,至少需要传递函数名作为参数。如果函数为有参函数,如"void proc2(int a,int b)",那么实例化std::thread类对象时,则需要传递更多参数,参数顺序依次为函数名、该函数的第一个参数、该函数的第二个参数,···,如"std::thread th2(proc2,a,b);"

3.当线程启动后,一定要在和线程相关联的std::thread对象销毁前,对线程运用join()或者detach()方法。

2.2 回收子线程资源

回收子线程的资源有两种方法——join和detach:

2.2.1 join方法

join的意思是父线程等待子线程结束,在子线程结束时,负责回收子线程的资源。

本质上是使当前线程阻塞,执行加入线程的逻辑。

2.2.2 detach方法

detach的含义是父线程和子线程相互分离,分离的线程由操作系统自动管理。

2.2.3 joinable()

joinable()是一个布尔类型的函数,他会返回一个布尔值来表示当前的线程是否是可执行线程(能被join或者detach)。

因为相同的线程不能join两次,也不能join完再detach,同理也不能detach完再join,所以joinable函数就是用来判断当前这个线程是否可以joinable。

通常有以下几种情况不能被joinable:

  • thread t;:t由缺省构造函数构建,此时线程未传入具体的线程函数。
  • 该thread被move过(包括move构造和move赋值)。【move指的是:线程的所有权将发生转移,原有线程对象的相关标识被清空,失去线程的控制权。其原有线程类对象ID变为0,joinable变为为false。】
  • 该线程已经被join或者detach过。

2.3 构造函数

thread() noexcept  // 默认构造函数,创建一个空thread对象,该对象非joinable	
template <class Fn, class... Args>  // 初始化构造函数,创建一个thread对象,该对象会调用Fn函数,Fn函数的参数由args指定,该对象是joinable的 	
thread (const thread&) = delete  // 拷贝构造函数,被禁用,意味着thread对象不可拷贝构造 	
thread (thread&& x) noexcept  // move构造函数,执行成功之后x失效,即x的执行信息被移动到新产生的thread对象,该对象非joinable	

2.4 std::thread类成员函数

get_id: // 获取线程ID。返回一个类型为std::thread::id的对象。
joinable: // 检查线程是否可被join。检查当前的线程对象是否表示了一个活动的执行线程
join:  // 调用该函数会阻塞当前线程(主调线程)。阻塞调用者(caller)所在的线程(主调线程)直至被join的std::thread对象标识的线程(被调线程)执行结束
detach:  // 将当前线程对象所代表的执行实例与该线程对象分离,使得线程的执行可以单独进行。一旦线程执行完毕,它所分配的资源将会被释放。
native_handle:  // 该函数返回与std::thread具体实现相关的线程句柄。native_handle_type是连接thread类和操作系统SDK API之间的桥梁,如在Linux g++(libstdc++)里,native_handle_type其实就是pthread里面的pthread_t类型,当thread类的功能不能满足我们的要求的时候(比如改变某个线程的优先级),可以通过thread类实例的native_handle()返回值作为参数来调用相关的pthread函数达到目录。
swap: // 交换两个线程对象所代表的底层句柄
operator=:  // 将线程与当前 thread 对象关联
sleep_for: // 线程休眠某个指定的时间片(time span),该线程才被重新唤醒,不过由于线程调度等原因,实际休眠时间可能比 sleep_duration 所表示的时间片更长。
sleep_until:  // 线程休眠至某个指定的时刻(time point),该线程才被重新唤醒。

三、Mutex互斥量

C++ 11中与 Mutex 相关的类(包括锁类型)和函数都声明在 头文件中,所以如果你需要使用 std::mutex,就必须包含 头文件

3.1 mutex 中类的介绍

3.1.1 mutex 系列类(4 种)

std::mutex	           	   // 最基本的Mutex类
std::recursive_mutex	   // 递归Mutex类
std::time_mutex	           // 定时Mutex类
std::recursive_timed_mutex // 定时递归Mutex类

3.1.2 lock 类(2种)

std::lock_guard	   // 与Mutex RAII 相关,方便线程对互斥量上锁
std::unique_lock   // 与 Mutex RAII 相关,方便线程对互斥量上锁,但提供了更好的上锁和解锁控制

3.1.3 函数(3种)

std::try_lock	// 尝试同时对多个互斥量上锁
std::lock		// 可以同时对多个互斥量上锁
std::call_once	// 如果多个线程需要同时调用某个函数,call_once 可以保证多个线程对该函数只调用一次。

3.2 std::mutex成员函数

如何理解互斥量:这样比喻,两个人要去银行的柜台办理业务,且银行只有一个柜台,A要办理业务,B也要办理业务,但是柜台同一时间只能给一个人办理,在办理业务时要坐到柜台位置(lock),用完后再离开柜台位置(unlock)。

那么,这个柜台位置就是互斥量,互斥量保证了使用办理业务这一过程不被打断。

3.2.1 构造函数

std::mutex不允许拷贝构造,也不允许 move 拷贝,最初产生的 mutex 对象是处于 unlocked 状态的。

3.2.2 lock()函数

调用线程将锁住该互斥量。

线程调用该函数会发生下面 3 种情况:
1)如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁。
2)如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住。
3)如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。

3.2.3 unlock()函数

解锁,释放对互斥量的所有权。

3.2.4 try_lock()函数

尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞。

线程调用该函数也会出现下面 3 种情况,
1)如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock 释放互斥量。
2)如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉。
3)如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。

3.3 std::recursive_mutex

std::recursive_mutex 与 std::mutex 一样,也是一种可以被上锁的对象,但是和 std::mutex 不同的是,std::recursive_mutex 允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权,std::recursive_mutex 释放互斥量时需要调用与该锁层次深度相同次数的 unlock(),可理解为 lock() 次数和 unlock() 次数相同,除此之外,std::recursive_mutex 的特性和 std::mutex 大致相同。

3.4 std::time_mutex

std::time_mutex 比 std::mutex 多了两个成员函数,try_lock_for(),try_lock_until()。

try_lock_for 函数接受一个时间范围,表示在这一段时间范围之内线程如果没有获得锁则被阻塞住(与 std::mutex 的 try_lock() 不同,try_lock 如果被调用时没有获得锁则直接返回 false),如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。

try_lock_until 函数则接受一个时间点作为参数,在指定时间点未到来之前线程如果没有获得锁则被阻塞住,如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。

3.5 std::recursive_timed_mutex

和 std:recursive_mutex 与 std::mutex 的关系一样,std::recursive_timed_mutex 的特性也可以从 std::timed_mutex 推导出来。

3.6 std::lock_guard

内部构造时相当于执行了lock,析构时相当于执行unlock。在其析构函数中进行解锁。最终的结果就是:创建即加锁,作用域结束自动解锁。从而使用lock_guard()就可以替代lock()与unlock()

lock_gurad也可以传入两个参数,第一个参数为adopt_lock标识时,表示不再构造函数中不再进行互斥量锁定,因此此时需要提前手动锁定。

mutex m;//实例化m对象,不要理解为定义变量
void proc1(int a)
{
    m.lock();//手动锁定
    lock_guard<mutex> g1(m, adopt_lock);
    cout << "proc1函数正在改写a" << endl;
    cout << "原始a为" << a << endl;
    cout << "现在a为" << a + 2 << endl;
}//自动解锁

3.7 std::unique_lock

unique_lock类似于lock_guard,只是unique_lock用法更加丰富,同时支持lock_guard()的原有功能。

使用lock_guard后不能手动lock()与手动unlock();

使用unique_lock后可以手动lock()与手动unlock();

unique_lock的第二个参数,除了可以是adopt_lock,还可以是try_to_lock与defer_lock。

try_to_lock:尝试去锁定,得保证锁处于unlock的状态,然后尝试现在能不能获得锁;

尝试用mutx的lock()去锁定这个mutex,但如果没有锁定成功,会立即返回,不会阻塞在那里。

defer_lock: 初始化了一个没有加锁的mutex。

void proc1(int a)
{
    unique_lock<mutex> g1(m, defer_lock);   //始化了一个没有加锁的mutex
 
    g1.lock();  //手动加锁,注意,不是m.lock()
    cout << "proc1函数正在改写a" << endl;
    cout << "proc1函数a为" << a << endl;
    cout << "proc1函数a+2为" << a + 2 << endl;
    g1.unlock();    //临时解锁
    cout << "尝试自动解锁" << endl;
    g1.lock();
    cout << "运行后自动解锁" << endl;
}   //自动解锁
 
void proc2(int a)
{
    unique_lock<mutex> g2(m, try_to_lock);  //尝试加锁,但如果没有锁定成功,会立即返回,不会阻塞在那里
    cout << "proc2函数正在改写a" << endl;
    cout << "proc2函数a为" << a << endl;
    cout << "proc2函数a+1为" << a + 1 << endl;
}   //自动解锁

3.8 condition_variable

std::condition_variable是C++标准库中提供的条件变量,用于线程之间的同步与通信。

它可以配合std::mutex或std::unique_lock一起使用,实现多个线程之间的等待和唤醒机制。

condition_variable头文件有两个variable类,一个是condition_variable,另一个是condition_variable_any

condition_variable必须结合unique_lock使用。condition_variable_any可以使用任何的锁。

condition_variable条件变量可以阻塞(wait、wait_for、wait_until)调用的线程直到使用(notify_one或notify_all)通知恢复为止。

condition_variable是一个类,这个类既有构造函数也有析构函数,使用时需要构造对应的condition_variable对象,调用对象相应的函数来实现上面的功能。

举例如下:

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

std::mutex mtx;  // 互斥锁
std::condition_variable cv;  // 条件变量
bool ready = false;  // 共享变量

void threadFunction()
{
    std::this_thread::sleep_for(std::chrono::seconds(2));  // 模拟耗时操作
    
    {
        std::lock_guard<std::mutex> lock(mtx);  // 获取互斥锁
        
        ready = true;
        cv.notify_one();  // 唤醒等待的线程
    }
}

int main()
{
    std::cout << "Main thread started!" << std::endl;
    
    std::thread t(threadFunction);
    
    {
        std::unique_lock<std::mutex> lock(mtx);  // 获取互斥锁
        
        while (!ready) {  // 当条件不满足时等待
            cv.wait(lock);  // 等待唤醒通知
        }
        
        std::cout << "Main thread resumed!" << std::endl;
    }
    
    t.join();
    
    return 0;
}

在上述示例中,主线程等待子线程完成某个操作后才继续执行。主线程通过std::unique_lock获取互斥锁,并在while循环中检查条件是否满足,如果条件不满足,则调用cv.wait()进入等待状态。当子线程完成操作后,它会获取互斥锁并通知主线程(cv.notify_one()),主线程被唤醒后继续执行。

3.8.1 wait函数

当前线程调用 wait() 后将被阻塞(此时当前线程应该获得了锁(mutex),不妨设获得锁 lck),直到另外某个线程调用 notify_* 唤醒了当前线程。

在线程被阻塞时,该函数会自动调用 lck.unlock() 释放锁,使得其他被阻塞在锁竞争上的线程得以继续执行。

另外,一旦当前线程获得通知(notified,通常是另外某个线程调用 notify_* 唤醒了当前线程),wait()函数也是自动调用 lck.lock(),使得lck的状态和 wait 函数被调用时相同。

3.8.2 wait_for

与std::condition_variable::wait() 类似,不过 wait_for可以指定一个时间段,在当前线程收到通知或者指定的时间 rel_time 超时之前,该线程都会处于阻塞状态。

而一旦超时或者收到了其他线程的通知,wait_for返回,剩下的处理步骤和 wait()类似。

template <class Rep, class Period>
  cv_status wait_for (unique_lock<mutex>& lck,
					  const chrono::duration<Rep,Period>& rel_time);

另外,wait_for 的重载版本的最后一个参数pred表示 wait_for的预测条件,只有当 pred条件为false时调用 wait()才会阻塞当前线程,并且在收到其他线程的通知后只有当 pred为 true时才会被解除阻塞。

template <class Rep, class Period, class Predicate>
    bool wait_for (unique_lock<mutex>& lck,
         const chrono::duration<Rep,Period>& rel_time, Predicate pred);

四、异步线程

4.1 std::future异步线程

需要 #include,async是一个函数模板,用来启动一个异步任务,它返回一个future类模板对象,future对象起到了占位的作用,刚实例化的future是没有储存值的,但在调用future对象的get()成员函数时,主线程会被阻塞直到异步线程执行结束,并把返回结果传递给future,即通过FutureObject.get()获取函数返回值。

如何理解:相当于你去银行业务(主线程),把资料交给了柜台,柜台人员去给你办理(async创建子线程),柜台人员给了你一个单据(future对象),说你的业务正在给你办(子线程正在运行),等段时间你再过来凭这个单据取结果。过了段时间,你去柜台取结果,但是结果还没出来(子线程还没return),你就在柜台人员等着(阻塞),直到你拿到结果(get())你才离开(不再阻塞)。

#include <iostream>
#include <thread>
#include <mutex>
#include<future>
#include<Windows.h>
using namespace std;
double t1(const double a, const double b)
{
	double c = a + b;
	Sleep(3000); //假设t1函数是个复杂的计算过程,需要消耗3秒
	return c;
}
 
int main()
{
	double a = 2.3;
	double b = 6.7;
	future<double> fu = async(t1, a, b); //创建异步线程线程,并将线程的执行结果用fu占位;
	cout << "正在办理业务" << endl;
	cout << "马上为您办理好,请您耐心等待" << endl;
	cout << "计算结果:" << fu.get() << endl;//阻塞主线程,直至异步线程return,future对象的get()方法只能调用一次。
	return 0;
}

4.2 shared_future

future与shard_future的用途都是为了占位,但是两者有些许差别。future的get()成员函数是转移数据所有权;

shared_future的get()成员函数是复制数据。

future对象的get()只能调用一次;无法实现多个线程等待同一个异步线程,一旦其中一个线程获取了异步线程的返回值,其他线程就无法再次获取。

shared_future对象的get()可以调用多次;可以实现多个线程等待同一个异步线程,每个线程都可以获取异步线程的返回值。

五、原子类型automic

互斥量的加锁一般是针对一个代码段,而原子操作针对的一般都是一个变量。

automic是一个模板类,使用该模板类实例化的对象,提供了一些保证原子性的成员函数来实现共享数据的常用操作。

在以前,定义了一个共享的变量(int i=0),多个线程会操作这个变量,那么每次操作这个变量时,都是用lock加锁,操作完毕使用unlock解锁,以保证线程之间不会冲突;

现在,实例化了一个类对象(automic i=0)来代替以前的那个变量,每次操作这个对象时,就不用lock与unlock,这个对象自身就具有原子性,以保证线程之间不会冲突。

automic对象提供了常见的原子操作(通过调用成员函数实现对数据的原子操作):

  • store是原子写操作。
  • load是原子读操作。
  • exchange是于两个数值进行交换的原子操作。
  • 即使使用了automic,也要注意执行的操作是否支持原子性。一般atomic原子操作,针对++,–,+=,-=,&=,|=,^=是支持的。
#include <atomic>
#include <thread>
#include <iostream>
using namespace std;
 
atomic_int64_t total = 0; //atomic_int64_t相当于int64_t,但是本身就拥有原子性
 
//线程函数,用于累加
void threadFunc(int64_t endNum) {
	for (int64_t i = 1; i <= endNum; ++i)
	{
		total += i;
	}
}
 
int main() {
	int64_t endNum = 100;
	thread t1(threadFunc, endNum);
	thread t2(threadFunc, endNum);
 
	t1.join();
	t2.join();
 
	cout << "total=" << total << endl; //10100
}

六、线程池

在一个程序中,如果我们需要多次使用线程,这就意味着,需要多次的创建并销毁线程。

而创建并销毁线程的过程势必会消耗内存,线程过多会带来调动的开销,进而影响缓存局部性和整体性能。

线程的创建并销毁有以下一些缺点:

  • 创建太多线程,将会浪费一定的资源,有些线程未被充分使用。
  • 销毁太多线程,将导致之后浪费时间再次创建它们。
  • 创建线程太慢,将会导致长时间的等待,性能变差。
  • 销毁线程太慢,导致其它线程资源饥饿。

线程池维护着多个线程,这避免了在处理短时间任务时,创建与销毁线程的代价。

标签:std,thread,lock,编程,C++,互斥,线程,mutex,多线程
From: https://www.cnblogs.com/Terrypython/p/18552520

相关文章

  • 极限编程在项目管理中的应用,你了解吗?
    极限编程(ExtremeProgramming,XP)是一种敏捷软件开发方法论,强调通过持续的反馈、快速的迭代和强烈的团队协作来提升软件开发的效率和质量。虽然XP最初是针对软件开发过程设计的,但它的核心原则和实践也可以在项目管理中得到广泛应用。以下是极限编程在项目管理中的一些关键应用和作......
  • C++ 中的线程、锁和条件变量
    C++中的线程、锁和条件变量Created:2024-06-19T17:17+08:00Published:2024-11-18T10:39+08:00Categories:C-CPP目录线程创建与执行锁lockguardexamplemutex底层实现解释byGPT条件变量(conditionvariable)线程从cv.wait(lock)被唤醒后会自动抢锁虚假唤醒生产者消费......
  • 编程之路,从0开始:结构体详解
    目录前言正文1、结构体引入2、结构体的声明3、typedef4、结构体的匿名声明5、结构的自引用(1)链表(2)自引用6、结构体内存对齐(1)对齐规则(2)题目(3)为什么存在内存对齐?(4)默认对齐数7、结构体实现位段(1)什么是位段(2)位段的跨平台问题(3)位段的应用总结 前言     ......
  • C++二级:数字字符求和
    数字字符求和请编写一个程序实现以下功能:从一个字符串中,提取出所有的数字字符即0-9,并作为数求和。输入一行字符串,长度不超过100,字符串中不含空格。输出字符串中所有数字字符作为数的和样例输入Lsd2f02k3ja3sdf223样例输出171、数字字符求和请编写一个程序实现以下功......
  • C++刷题第十题——求奇数的乘积
    ProblemDescription给你n个整数,求他们中所有奇数的乘积。Input输入数据包含多个测试实例,每个测试实例占一行,每行的第一个数为n,表示本组数据一共有n个,接着是n个整数,你可以假设每组数据必定至少存在一个奇数。Output输出每组数中的所有奇数的乘积,对于测试实例,输出一行。Sa......
  • 《 C++ 修炼全景指南:二十 》不止是链表升级!跳表的核心原理与超强性能解析
    摘要这篇博客全面解析了跳表(SkipList)作为一种高效的链表数据结构的特性和应用。跳表以多层链表和随机化策略实现O(logn)的查找、插入和删除性能,简化了平衡树结构中常见的复杂旋转操作。通过剖析跳表的结构设计和核心操作,我们探讨了其在范围查询和动态更新中的优势,......
  • c++ 后端
    基础知识1.指针、引用2.数组3.缺省参数4.函数重载5.内联函数6.宏7.auto8.const9.类和对象10.类的6个默认成员函数11.初始化列表12.this指针13.C/C++的区别14.C++三大特性15.结构体内存对齐规则16.explicit17.static18.友元类、友元函数19.内部类20.......
  • wincc 7.5SP2下VBA编程学习练习15:批量删除变量
    在前面练习的基础上学习批量删除变量。新建下面的脚本:SubDeleteTags()'批量删除变量DimhmigoAshmigoDimstrTagNameAsStringDimiAsIntegerSethmigo=NewhmigoFori=1To5strTagName="Real"&CStr(i)hmigo.DeleteTagstrTagNameNextSethmigo=Nothin......
  • 易基因:华中农大周道绣团队利用BS-seq揭示水稻雄性配子发生过程DNA甲基化重编程机|Genom
    大家好,这里是专注表观组学十余年,领跑多组学科研服务的易基因。在开花植物中,DNA甲基化在配子和合子中部分重编程,但其在植物配子发生过程中的时机和功能意义尚不清楚。DNA甲基化在抑制转座元件(TE)和相关序列中起着关键作用,对于基因表达调控至关重要。近日,华中农业大学周道绣教授团......
  • C++ 编程基础(8)模版 | 8.2、函数模版
    文章目录一、函数模版1、声明与定义2、模版参数3、模板的实例化3.1、隐式实例化3.2、显示实例化4、模版的特化5、注意事项6、总结前言:C++函数模板是一种强大的特性,它允许程序员编写与类型无关的代码。通过使用模板,函数或类可以处理不同的数据类型,而无需重复编写......