C++多线程函数介绍

1.C++11前没有线程库问题

对于多线程操作,Linux选择使用POSIX标准,而windows没有选择POSIX标准,自己设计了一套API和系统调用,叫Win32 API,就跟Linux存在标准差异,在Linux的代码移植到Windows就可能运行不了。

要保证兼容性,就需要借助条件编译,实现两份代码,根据不同平台执行对应的代码。

// 确保平台兼容性

#ifdef WIN_32

CreateThread // Windows 中创建线程的接口

// ...

#else

pthread_create // Linux 中创建线程的接口

// ...

#endif

C++11后,加入了线程库标准,包含了线程,互斥锁,条件变量等常用线程操作,不用依赖第三方库,使用线程库编写的代码可以在不同环境下运行。

2.C++多线程

并发与并行概念

并发是指两个或者多个事件在同一时间间隔发生,并发是针对单核CPU提出的,在同一CPU上的多个事件。

并行是指多个事件在同一时间发生 ,是针对多核CPU提出的,在不同CPU上的多个事件。

线程库--thread

线程对象构造

1.默认构造函数

thread() noexcept;

创建了一个空的线程对象,不启动任何线程,不会抛出异常

2.模板构造函数

template <class Fn, class... Args>

explicit thread (Fn&& fn, Args&&... args);

Fn是一个可调用对象,可以是函数指针,函数对象或者lambda表达式

Args是传递给Fn的参数列表

explicit关键字表示这个构造函数显式的,防止隐式转换

移动构造函数

thread (thread&& x) noexcept;

用于移动一个线程对象,接收一个右值引用thread&& x,表示从另一个线程对象x窃取线程资源

无参构造

无参数构造出来线程对象,它不会关联任何线程函数,不会启动任何线程。

thread t1;

因为有移动赋值函数,所以创建空线程可以窃取传参匿名线程对象资源。

带可变参数的构造

三种形式创建线程执行任务

需要休眠函数,不然结果会混乱,因为式并发执行任务,往显示器(公共资源)写入信息。

cpp 复制代码
#include<iostream>
#include<algorithm>
#include<mutex>
#include<Windows.h>
#include<thread>
#include<condition_variable>
using namespace std;

void func1(int start, int end)
{
	for (int i = start; i <= end; i++)
	{
		cout << i << " ";

	}
	cout << endl;
}

struct My_class
{
	void operator()(int start, int end)
	{
		for (int i = start; i <= end; i++)
		{
			cout << i << " ";
		}
		cout << endl;
	}
};

int main()
{
	//函数指针
	thread t1(func1, 1, 10);
	Sleep(1);

	//仿函数(函数对象)
	thread t2(My_class(), 10, 20);
	Sleep(1);
	//lambda表达式
	thread t3([](const string& str) -> void {cout << str << endl; },"I am thread-3");

	t1.join();
	t2.join();
	t3.join();
}

注意:thread类是禁止拷贝的,不允许拷贝构造和拷贝赋值,但是可以移动构造和移动赋值,可以将一个线程对象关联状态转移给其它线程对象。

这里传函数名字,实际是传函数指针类型,而thread构造函数有模板参数,虽然是&&但是只有传过去的类型也是&&的才会是右值引用,不然就是左值引用,引用折叠规定。
例:传递函数指针

当你传递一个函数指针给std::thread的构造函数时,例如:

cpp复制

复制代码
void func() {}
std::thread t(func);
  • func 是一个函数名,它是一个左值。

  • 根据引用折叠规则,F&& 在这种情况下会折叠为 F&,因为func是一个左值。

  • 因此,func作为左值被传递给std::thread的构造函数,它被绑定到一个左值引用上。

示例:传递函数对象

如果你传递一个函数对象或lambda表达式,例如:

cpp复制

复制代码
std::thread t([]() { std::cout << "Hello" << std::endl; });
  • 这里的lambda表达式是一个右值。

  • 根据引用折叠规则,F&& 在这种情况下会折叠为 F&&,因为lambda表达式是一个右值。

  • 因此,lambda表达式作为右值被完美转发到std::thread的构造函数。

3.thread类的成员函数

1.join

  • 功能:等待一个线程完成。如果该线程还未执行完毕,则当前线程(一般是主线程)将被阻塞,直到该线程执行完成,主线程才会继续执行。

2.joinable

  • 功能 :判断线程是否可以执行join()函数,返回truefalse

  • 用法if (t.joinable()) { t.join(); } 其中tstd::thread对象。

3.detach

  • 功能 :将当前线程与创建的线程分离,使它们分别运行,当分离的线程执行完毕后,系统会自动回收其资源。如果一个线程被分离了,就不能再使用join()函数了,因为线程已经无法被联接了。

  • 用法t.detach(); 其中tstd::thread对象。

4.get_id

  • 功能 :获取该线程的id

  • 用法std::thread::id t_id = t.get_id(); 其中tstd::thread对象。

5.swap

  • 功能:将两个线程对象关联线程的状态进行交换。

  • 用法t1.swap(t2); 其中t1t2std::thread对象。

join和joinable

joinable函数可以用于判断线程是否有效。

线程无效:采用无参构造的线程对象;线程对象的状态已经转移给其它线程对象;线程调用join或者detach。

代码示例

cpp 复制代码
#include<iostream>
#include<algorithm>
#include<mutex>
#include<Windows.h>
#include<thread>
#include<condition_variable>
using namespace std;


void Print()
{
	cout << "Hello from thread" << endl;

}

int main()
{
	thread t1(Print);
	Sleep(1);
	if (t1.joinable())
	{
		cout << "Thread is joinable" << endl;
	}
	
	t1.join();

	if (t1.joinable())
	{
		cout << "Thread is joinable" << endl;
	}
	else
	{
		cout << "Thread isn't joinable" << endl;
	}
	return 0;
}

代码解释

上面代码,先创建线程执行,延时为了让打印执行完,然后用joinable函数判断线程是否结束,没有执行join函数就还是存在,join函数执行后,线程结束资源回收,再去判断线程的状态就是结束了。

代码示例

cpp 复制代码
#include<iostream>
#include<algorithm>
#include<mutex>
#include<Windows.h>
#include<thread>
#include<condition_variable>
using namespace std;


void func()
{
	for (int i = 0; i <= 5; i++)
	{
		cout << "线程执行中" << endl;
		this_thread::sleep_for(chrono::seconds(1));
	}
	cout << "线程结束" << endl;
}

int main()
{
	thread t1(func);
	t1.detach();

	cout << "主线程继续执行" << endl;

	this_thread::sleep_for(chrono::seconds(7));
	cout << "主线程结束" << endl;

	return 0;
}

代码解释

先执行线程函数,打印六次每次间隔1秒,因为执行了分离函数detach,所以主线程就会执行自己的代码,因为主线程结束了,线程也会跟着结束,所以主函数延时等待线程执行完才结束。

4.this_thread类

  1. get_id

    • 功能:获取当前线程的唯一标识符(ID)。

    • 用法std::thread::id id = std::this_thread::get_id();

    • 说明:每个线程都有一个唯一的ID,这个ID可以用来识别和区分不同的线程。

  2. sleep_for

    • 功能:使当前线程休眠一个指定的时间段。

    • 用法std::this_thread::sleep_for(std::chrono::milliseconds(500));

    • 说明:这个函数接受一个时间长度作为参数,单位可以是秒、毫秒等。线程会休眠指定的时间长度。

  3. sleep_until

    • 功能:使当前线程休眠直到指定的具体时间点。

    • 用法std::this_thread::sleep_until(std::chrono::system_clock::now() + std::chrono::seconds(5));

    • 说明:这个函数接受一个时间点作为参数,线程会休眠直到达到这个时间点。

  4. yield

    • 功能:使当前线程"放弃"执行,让操作系统调度另一个线程继续执行。

    • 用法std::this_thread::yield();

    • 说明:这个函数不会使线程休眠,而是让操作系统有机会调度其他线程。当前线程可能会在下一次调度时再次获得执行机会。

get_id与上面的thread类的不一样,thread类的get_id需要对象.get_id(),而这个可以直接this_thread::get_id()

代码示例

cpp 复制代码
#include<iostream>
#include<algorithm>
#include<mutex>
#include<Windows.h>
#include<thread>
#include<condition_variable>
using namespace std;


void func()
{
	cout << "Thread ID" << std::this_thread::get_id() << endl;
}

int main()
{
	thread t1(func);
	Sleep(1);
	thread t2(func);
	Sleep(1);
	cout << "Main thread ID" << std::this_thread::get_id() << endl;
	
	
	t1.join();
	t2.join();
	return 0;
}

sleep_for和sleep_until

sleep_until表示休眠一个绝对时间,比如线程运行结束后,休眠到某个具体的时间点(明天八点)继续运行,sleep_for是让线程休眠一个相对时间(休眠3秒)

相对时间:时,分,秒,毫秒等,这些单位包含在chrono类中。

代码示例

cpp 复制代码
#include<mutex>
#include<Windows.h>
#include<thread>
#include<condition_variable>
#include<vector>
using namespace std;


int main()
{

	vector<thread> v(5);
	for (int i = 0; i < 5; i++)
	{
		v[i] = thread([]()->void
			{
				for (int i = 0; i < 10; i++)
				{
					auto id = this_thread::get_id();
					cout << "我是线程" << id << "正在运行" << endl;

					this_thread::sleep_for(chrono::milliseconds(200));
				}

			});
		
	}

	for (auto& x : v)
	{
		x.join();
	}
	return 0;

}

线程函数传参数问题

线程函数的参数使拷贝的方式拷贝到线程栈空间中,使用引用传参数,在线程内部修改这个引用值也不会改变(在传过去的线程打印值还是原来的),因为实际引用的是线程栈中的拷贝,而不是线程函数的形参。

cpp 复制代码
void add(int& num)
{
	num++;
}
int main()
{
	int num = 0;
	thread t(add, num);
	t.join();
 
	cout << num << endl; //0
	return 0;
}

通过线程函数形参改变外部的实参

cpp 复制代码
#include<iostream>
#include<algorithm>
#include<mutex>
#include<Windows.h>
#include<thread>
#include<condition_variable>
#include<vector>
using namespace std;


void func1(int& x)
{
	x += 10;
}

void func2(int* x)
{
	*x += 10;
}

int main()
{
	int a = 10;

	//方案一
    //如果想要通过形参改变外部实参时,必须借助std::ref()函数

	std::thread t1(func1, std::ref(a));
	t1.join();
	std::cout << a << std::endl;


	//方案二
	std::thread t2(func2, &a);
	t2.join();
	cout << a << endl;
	//方案三
	std::thread t3([&a]() {a += 10; });
	t3.join();
	cout << a << endl;
}

5.互斥量库-mutex

锁是一种机制,用来确保同一时刻只有一个线程可以访问共享资源。通过使用锁,可以防止多个线程同时修改共享资源,从而保证数据的一致性和正确性。

线程拥有自己独立的栈结构,但对于全局变量等临界资源,是直接被多个线程共享的。

cpp 复制代码
int g_val = 0;
 
void Func(int n)
{
	cout << "&g_val: " << &g_val << " &n: " << &n << endl << endl;
}
 
int main()
{
	int n = 10;
	thread t1(Func, n);
	thread t2(Func, n);
 
	t1.join();
	t2.join();
	return 0;
}

g_val的地址一样,而局部变量的地址不一样,就说明栈区不是同一个,处于线程的独立栈。

6.标准库提供的四种互斥锁

1.std::mutex

mutex锁是基本的互斥量,mutex之间不能进行拷贝,也不能进行移动。

  1. lock

    • 功能:对互斥量进行加锁。

    • 用法mutex.lock();

    • 说明 :调用lock函数会阻塞当前线程,直到互斥量被成功锁定。如果互斥量已经被其他线程锁定,调用lock的线程将会等待,直到互斥量被解锁。

  2. try_lock

    • 功能:尝试对互斥量进行加锁。

    • 用法bool success = mutex.try_lock();

    • 说明 :与lock不同,try_lock不会阻塞当前线程。如果互斥量已经被其他线程锁定,try_lock会立即返回false,否则返回true表示成功锁定互斥量。

  3. unlock

    • 功能:对互斥量进行解锁,释放互斥量的所有权。

    • 用法mutex.unlock();

    • 说明 :调用unlock函数会释放当前线程持有的互斥量锁。之后,其他线程可以尝试通过locktry_lock来获取互斥量的锁。需要注意的是,只有拥有互斥量锁的线程才能调用unlock函数。

代码示例

cpp 复制代码
#include<iostream>
#include<algorithm>
#include<mutex>
#include<Windows.h>
#include<thread>
#include<condition_variable>
#include<vector>
#include<mutex>
using namespace std;


int g_val = 0;
mutex mtx;

void func(int n)
{
	mtx.lock();
	while (n--)
	{
		g_val++;

	}
	mtx.unlock();
}

int main()
{
	int n = 20000;
	thread t1(func, n);
	thread t2(func, n);
	t1.join();
	t2.join();

	cout << "g_val:" <<g_val<< endl;
	return 0;
}

并行与串行

互斥锁的加锁,解锁位置不同,就会有不同的效果,锁加在while的外面,则先拿到锁的线程就会先执行完任务,就是串行操作。锁加在while里面,则是两个线程一起执行任务,就是并行操作。

而在这个代码中,把锁放在外面是快的,因为锁在里面,临界区很小,就会频繁申请锁和释放锁,效率低下,最好把临界区的大小适当。

2.std::recursive_mutex

recursive_mutex递归互斥锁,这把锁主要用来递归加锁的场景中。

代码示例

这个代码就会造成死锁,因为申请锁后,执行递归函数,有重新申请锁,而锁并没有释放,就会阻塞在这里,造成死锁。把锁换成递归时用的锁就可以解决。(递归的锁会判断当前的函数是否跟递归前的函数是否一样,一样就可以访问临界区,而不会阻塞)

cpp 复制代码
// 普通互斥锁
mutex mtx;
 
void func(int n)
{
	if (n == 0)
		return;
 
	mtx.lock();
	n--;
 
	func(n);
	mtx.unlock();
}
 
int main()
{
	int n = 1000;
	thread t1(func, n);
	thread t2(func, n);
	
	t1.join();
	t2.join();
	return 0;
}

3.std::timed_mutex

timed_mutex时间互斥锁,这把锁新增了定时解锁的功能,可以把程序运行指定时间后,自动解锁,如果锁没有解开。

try_lock_for是按照相对时间自动解锁,而try_lock_until则是按照绝对时间解锁

4.std::recursive_timed_mutex

这个锁是递归时间互斥锁,可以解决递归以及定时解锁。

7.RAII风格的锁

手动加锁,解锁可能会出现死锁问题,引进的异常处理,临界区遇到异常就跳出去执行catch了,锁就没有被解开,就导致死锁问题。

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
 
std::mutex mtx;
 
void dangerousFunction(int id) 
{
    // 手动加锁
    mtx.lock();
 
    std::cout << "Thread " << id << " is running." << std::endl;
 
    // 模拟一个异常情况,没有解锁就退出
    if (id == 1) {
        throw std::runtime_error("Thread 1 encountered an error!");
    }
 
    // 手动解锁(如果有异常发生,这行代码不会执行)
    mtx.unlock();
}
 
int main() {
    try {
        std::thread t1(dangerousFunction, 1);
        std::thread t2(dangerousFunction, 2);
 
        t1.join();
        t2.join();
    } catch (const std::exception &e) {
        std::cerr << "Exception caught: " << e.what() << std::endl;
    }
 
    return 0;
}

RAII风格编写代码

cpp 复制代码
#include<iostream>
#include<algorithm>
#include<mutex>
#include<Windows.h>
#include<thread>
#include<condition_variable>
#include<vector>
#include<mutex>
using namespace std;

std::mutex mtx;

void func(int id)
{
	try
	{
		std::lock_guard<std::mutex> lock(mtx);
		std::cout << "Thread" << id << " is running" << std::endl;
		if (id == 1)
		{
			throw std::runtime_error("Thread error");
		}
	}
	catch (const std::exception& e)
	{
		std::cerr << "Exception caught in thread" << id << ":" << e.what() << std::endl;
	}
}

int main()
{
	try
	{
		std::thread t1(func, 1);
		std::thread t2(func, 2);

		t1.join();
		t2.join();
	}
	catch(const std::exception& e)
	{
		std::cerr << "Exception caught:" << e.what() << std::endl;
	}
	return 0;
}

lock_guard

std::lock_guard是C++是标准库的一个模板类,用于实现资源的自动加锁和解锁。基于RALL设计理念,能够确保在作用域结束时自动释放锁资源。

std::lock_guard的主要特点

1.自动加锁: 在创建std::lock_guard对象时,会立即对指定的互斥量进行加锁操作。这样可以确保在进入作用域后,互斥量已经被锁定,避免了并发访问资源的竞争条件。

2.自动解锁:std::lock_guard对象在作用域结束时,会自动释放互斥量。无论作用域是通过正常的流程结束、异常抛出还是使用return语句提前返回,std::lock_guard都能保证互斥量被正确解锁,避免了资源泄漏和死锁的风险。

3.适用于局部锁定: 由于std::lock_guard是通过栈上的对象实现的,因此适用于在局部范围内锁定互斥量。当超出std::lock_guard对象的作用域时,互斥量会自动解锁,释放控制权。

unique_lock

std::unique_lock是C++标准库中的一个模板类,用于实现更加灵活的互斥量的加锁和解锁操作。

std::unqiue_lock特点

1.可以自动加锁和解锁,在创建对象时立即对指定的互斥量进行加锁操作,确保互斥量被锁定。

2.更加灵活加锁和解锁,可以自动加锁和解锁,也可以手动解锁和解锁。

3.可以延迟加锁和条件变量,std::unqiue_lock支持延迟加锁的功能,可以在锁没申请下创建对象,加锁后就直接管理这个锁。

代码示例

cpp 复制代码
#include<iostream>
#include<algorithm>
#include<mutex>
#include<Windows.h>
#include<thread>
#include<condition_variable>
#include<vector>
#include<mutex>
using namespace std;

std::mutex mtx;

void func1()
{
	std::unique_lock<std::mutex> lock(mtx);
	std::cout << "Thread running" << std::endl;

	//手动解锁加锁
	lock.unlock();
	lock.lock();

}//结束时自动释放锁

int main()
{
	std::thread t1(func1);
	t1.join();
	return 0;
}
相关推荐
点云SLAM7 分钟前
C++20新增内容
c++·算法·c++20·c++ 标准库
照书抄代码10 分钟前
C++11可变参数模板单例模式
开发语言·c++·单例模式·c++11
No0d1es15 分钟前
CCF GESP C++编程 四级认证真题 2025年3月
开发语言·c++·青少年编程·gesp·ccf·四级·202503
No0d1es39 分钟前
CCF GESP C++编程 五级认证真题 2025年3月
开发语言·c++·青少年编程·gesp·ccf·五级·2025年3月
shuaixio1 小时前
【C++代码整洁之道】第九章 设计模式和习惯用法
c++·设计模式·设计原则·常见设计模式·习惯用法
the_nov2 小时前
25.Reactor
linux·c++
神里流~霜灭3 小时前
数据结构:二叉树(三)·(重点)
c语言·数据结构·c++·算法·二叉树·红黑树·完全二叉树
小王努力学编程3 小时前
【Linux系统编程】进程概念,进程状态
linux·运维·服务器·c++
2401_853448233 小时前
C嘎嘎类里面的额函数
c语言·开发语言·c++
莫有杯子的龙潭峡谷3 小时前
4.4 代码随想录第三十五天打卡
c++·算法