C++并发编程

发布于:2024-04-25 ⋅ 阅读:(22) ⋅ 点赞:(0)

基本介绍

线程

  • C++98标准没有直接提供原生的多线程支持

  • 在C++98中,并没有像后来的C++11标准中那样的<thread>库或其他直接的多线程工具

  • 然而,这并不意味着在C++98中无法实现多线程。开发者通常会使用平台特定的API(如Windows的线程API或POSIX线程(pthreads))来实现多线程。此外,一些第三方库,如Boost.Thread,也提供了在C++98环境中进行多线程编程的功能

  • 需要注意的是,使用平台特定的API或第三方库进行多线程编程可能会增加代码的复杂性和维护成本,因为你需要确保你的代码在所有目标平台上都能正常工作,并处理各种可能的线程同步和互斥问题

  • 从C++11开始,C++标准库开始直接支持多线程编程,通过引入<thread><mutex><condition_variable>等头文件,提供了更为方便和统一的多线程编程接口。因此,如果可能的话,建议使用C++11或更高版本的C++标准进行多线程编程

进程

  • C++标准库本身并没有直接提供创建进程的功能,创建进程通常依赖于操作系统的API或其他库函数

  • 在Unix和Linux系统中,可以使用fork()函数来创建进程。fork()函数会创建一个与当前进程几乎完全相同的子进程,包括代码、数据和堆栈等。在子进程中,可以使用exec()系列函数来执行另一个程序

  • 在Windows系统中,可以使用CreateProcess()函数来创建进程。这个函数会创建一个新的进程,并返回一个进程句柄,可以用于操作该进程

创建线程

#include<iostream>
#include<string>
#include<thread>

void printHelloWorld()
{
	std::cout << "Hello World" << std::endl;
}

void print(std::string text)
{
	std::cout << text << std::endl;
}

int main()
{
	// 当前 main 这里是主线程



	// 创建一个线程 t1,让它执行 printHelloWorld 这个函数
	std::thread t1(printHelloWorld);

	// 等待 t1 线程完成(如果不等待,可能子线程 t1 还没完成的时候,主线程已经结束了,程序会报错)
	t1.join();



	// 创建一个线程 t2,让它执行 print 这个函数,并传入参数
	std::thread t2(print, "This is thread 2.");

	// 等待 t2 线程完成(如果不等待,可能子线程 t2 还没完成的时候,主线程已经结束了,程序会报错)
	t2.join();



	// 创建一个线程 t3
	std::thread t3(print, "This is thread 3.");

	// 分离线程(也可以使用分离线程这个技术,让主线程结束后,子线程依然可以运行)
	t3.detach();



	// 创建一个线程 t4
	std::thread t3(print, "This is thread 3.");

	// 严谨的项目里面,可能用到,先判断该线程是否可以被join()
	bool isJoin = t3.joinable();
	if (isJoin)
	{
		t3.join();
	}

	return 0;
}

线程常见错误

  1. 传递临时变量的问题
#include<iostream>
#include<thread>


void foo(int& x)
{
	x += 1;
}

int main()
{
	int num = 1;		// 局部变量 num
	std::thread t1(foo, std::ref(num));		// std::ref() 传递引用类型
	t1.join();		// 等待t1线程结束

	std::cout << num << std::endl;

	return 0;
}
  1. 传递指针或引用指向局部变量的问题
#include<iostream>
#include<thread>

// 创建一个线程 t (全局变量)
std::thread t;
int a = 1;

void foo(int& x)
{
	std::cout << x << std::endl;		// 1
	x += 1;
	std::cout << x << std::endl;		// 2
}


void test()
{
	t = std::thread(foo, std::ref(a));
}

int main()
{
	test();

	t.join();

	return 0;
}
  1. 入口函数为类的私有成员函数
#include<iostream>
#include<thread>
#include<memory>	// 智能指针,不用的时候,会自动释放

class A
{
private:
	friend void thread_foo();
	void foo()
	{
		std::cout << "hello" << std::endl;
	}
};


void thread_foo()
{
	std::shared_ptr<A> a = std::make_shared<A>();	// 使用智能指针实例化A对象
	std::thread t(&A::foo, a);
	t.join();
}

int main()
{
	thread_foo();
}

互斥量

锁的使用

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

int a = 0;

// 创建互斥锁
std::mutex mtx;

void func()
{
	for (int i = 0; i < 10000; i++)
	{
		mtx.lock();		// 加锁
		a += 1;
		mtx.unlock();	// 解锁
	}
}

int main()
{
	std::thread t1(func);
	std::thread t2(func);

	t1.join();
	t2.join();

	std::cout << a << std::endl;
	return 0;
}

死锁演示

  • 图形演示

在这里插入图片描述

  • 代码演示
#include<iostream>
#include<thread>
#include<mutex>


// 创建互斥锁
std::mutex mtx1;
std::mutex mtx2;


// people1 先抢占 mtx1,再快速抢占 mtx2
void people1()
{
	for (int i = 0; i < 1000; i++)
	{
		mtx1.lock();
		std::cout << "people1 上锁mtx1成功\n";
		mtx2.lock();
		std::cout << "people1 上锁mtx2成功\n";
		mtx1.unlock();
		mtx2.unlock();
	}
}

// people2 先抢占 mtx2,再快速抢占 mtx1
void people2()
{
	for (int i = 0; i < 1000; i++)
	{
		mtx2.lock();
		std::cout << "people2 上锁mtx2成功\n";
		mtx1.lock();
		std::cout << "people2 上锁mtx1成功\n";
		mtx1.unlock();
		mtx2.unlock();
	}
}


int main()
{
	std::thread t1(people1);
	std::thread t2(people2);

	t1.join();
	t2.join();

	return 0;
}

解决死锁

  • 解决办法:所有人都要严格按照顺序抢占资源,都要先抢占完A资源,才能继续抢占B资源,继续抢占C资源…
#include<iostream>
#include<thread>
#include<mutex>


// 创建互斥锁
std::mutex mtx1;
std::mutex mtx2;


// people1 要先抢占 mtx1,才能抢占 mtx2
void people1()
{
	for (int i = 0; i < 1000; i++)
	{
		mtx1.lock();
		std::cout << "people1 上锁mtx1成功\n";
		mtx2.lock();
		std::cout << "people1 上锁mtx2成功\n";
		mtx1.unlock();
		std::cout << "people1 解锁mtx1成功\n";
		mtx2.unlock();
		std::cout << "people1 解锁mtx2成功\n";
	}
}

// people2 要先抢占 mtx1,才能抢占 mtx2
void people2()
{
	for (int i = 0; i < 1000; i++)
	{
		mtx1.lock();
		std::cout << "people2 上锁mtx1成功\n";
		mtx2.lock();
		std::cout << "people2 上锁mtx2成功\n";
		mtx1.unlock();
		std::cout << "people2 解锁mtx1成功\n";
		mtx2.unlock();
		std::cout << "people2 解锁mtx2成功\n";
	}
}


int main()
{
	std::thread t1(people1);
	std::thread t2(people2);

	t1.join();
	t2.join();

	return 0;
}

lock_guard

  • std::lock_guard 是 C++ 标准库中的一种互斥量封装类,用于保护共享数据,防止多个线程同时访问同一资源而导致的数据竞争问题
  • 当构造函数被调用时,该互斥量会被自动锁定
  • 当析构函数被调用时,该互斥量会被自动解锁
  • std::lock_guard 对象不能复制或移动,因此它只能在局部作用域中使用
#include<iostream>
#include<thread>
#include<mutex>

// 共享变量
int shared_data = 0;

// 创建互斥锁
std::mutex mtx;

void func()
{
	for (int i = 0; i < 1000; i++)
	{
		std::lock_guard<std::mutex> obj(mtx);	// 构造时自动加锁,析构时自动解锁
		shared_data++;
	}
}

int main()
{
	std::thread t1(func);
	std::thread t2(func);

	t1.join();
	t2.join();

	std::cout << shared_data << std::endl;		// 2000

	return 0;
}

unique_lock

  • std::unique_lock 是 C++ 标准库中提供的一个互斥量封装类,用于在多线程程序中对互斥量进行加锁和解锁操作
  • 它的主要特点是可以对互斥量进行更加灵活的管理,包括延迟加锁、条件变量、超时等

std::unique_lock 提供了以下几个成员函数

lock():尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被成功加锁。

try_lock():尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则函数立即返回 false,否则返回 true

try_lock_for(const std::chrono::duration<Rep, Period>& rel_time):尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被成功加锁,或者超过了指定的时间

try_lock_until(const std::chrono::time_point<Clock, Duration>& abs_time):尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被成功加锁,或者超过了指定的时间点

unlock():对互斥量进行解锁操作

std::unique_lock 还提供了以下几个构造函数

unique_lock() noexcept = default:默认构造函数,创建一个未关联任何互斥量的 std::unique_lock 对象

explicit unique_lock(mutex_type& m):构造函数,使用给定的互斥量 m 进行初始化,并对该互斥量进行加锁操作

unique_lock(mutex_type& m, defer_lock_t) noexcept:构造函数,使用给定的互斥量 m 进行初始化,但不对该互斥量进行加锁操作

unique_lock(mutex_type& m, try_to_lock_t) noexcept:构造函数,使用给定的互斥量 m 进行初始化,并尝试对该互斥量进行加锁操作。如果加锁失败,则创建的 std::unique_lock 对象不与任何互斥量关联

unique_lock(mutex_type& m, adopt_lock_t) noexcept:构造函数,使用给定的互斥量 m 进行初始化,并假设该互斥量已经被当前线程成功加锁

文章推荐