C++并发编程

基本介绍

线程

  • 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. 传递临时变量的问题
cpp 复制代码
#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. 传递指针或引用指向局部变量的问题
cpp 复制代码
#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. 入口函数为类的私有成员函数
cpp 复制代码
#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_guardstd::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 进行初始化,并假设该互斥量已经被当前线程成功加锁

文章推荐

相关推荐
ULTRA??6 分钟前
C加加中的结构化绑定(解包,折叠展开)
开发语言·c++
凌云行者41 分钟前
OpenGL入门005——使用Shader类管理着色器
c++·cmake·opengl
凌云行者1 小时前
OpenGL入门006——着色器在纹理混合中的应用
c++·cmake·opengl
~yY…s<#>1 小时前
【刷题17】最小栈、栈的压入弹出、逆波兰表达式
c语言·数据结构·c++·算法·leetcode
可均可可2 小时前
C++之OpenCV入门到提高004:Mat 对象的使用
c++·opencv·mat·imread·imwrite
白子寰2 小时前
【C++打怪之路Lv14】- “多态“篇
开发语言·c++
小芒果_012 小时前
P11229 [CSP-J 2024] 小木棍
c++·算法·信息学奥赛
gkdpjj2 小时前
C++优选算法十 哈希表
c++·算法·散列表
王俊山IT2 小时前
C++学习笔记----10、模块、头文件及各种主题(一)---- 模块(5)
开发语言·c++·笔记·学习
-Even-2 小时前
【第六章】分支语句和逻辑运算符
c++·c++ primer plus