【C++11】线程库 | 互斥量 | 原子性操作 | 条件变量

news2024/11/27 22:23:41

文章目录

  • 一、线程库 - thread
    • 1. 线程对象的构造方式
      • 无参构造
      • 带可变参数包的构造
      • 移动构造
    • 2. thread类的成员函数
      • thread::detach()
      • thread::get_id()
      • thread::join()
      • thread::joinable()
    • 线程函数参数的问题
  • 二、互斥量库 - mutex
    • 标准库提供的四种互斥锁
      • 1. std::mutex
      • 2. std::recursive_mutex
      • 3. std::timed_mutex
      • 4. std::recursive_timed_mutex
      • 5. lock_guard 和 unique_lock
        • lock_guard的定义
        • lock_guard的使用
        • lock_guard的模拟实现
        • unique_lock的说明
  • 三、原子性操作库 - atomic
    • 多线程并发的线程安全问题
    • 方法1:加锁解决线程安全问题
    • 方法2:原子类解决线程安全问题
  • 四、条件变量库 - condition_variable
    • wait系列成员函数
  • 五、实现两个线程交替打印
    • 1. Linux下pthread程库的实现
    • 2. C++线程库的实现
  • 六、并行和并发的区别


一、线程库 - thread

在C++11之前,涉及到多线程问题,都是和平台相关的,比如Windows和Linux下各有自己的接口,这使得代码的可移植性比较差。C++11中最重要的特性就是对线程进行支持了,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念。要使用标准库中的线程,必须包含<thread>头文件。

线程是操作系统中的一个概念,线程对象可以关联一个线程,用来控制线程以及获取线程的状态。

1. 线程对象的构造方式

thread线程库提供了三种构造方式:

构造函数构造函数(中文解释)函数声明
default (1)无参构造、默认构造thread() noexcept;
initialization (2)带可变参数包的构造template <class Fn, class… Args>
explicit thread (Fn&& fn, Args&&… args);
copy [deleted] (3)thread对象无法拷贝构造thread (const thread&) = delete;
move (4)移动构造(传入右值)thread (thread&& x) noexcept;

无参构造

第一种是无参的构造函数,它创建出来的线程对象没有关联任何线程函数,也就是它没有启动任何线程,比如:

thread t1;

t1实际没有对应任何OS中实际的线程。由于thread提供了移动赋值函数,因此当后续需要让该线程对象与线程函数关联时,可以以带参的方式创建一个匿名对象,然后调用移动赋值将该匿名对象关联线程的状态转移给该线程对象:

thread t1;
//... 
t1 = thread(func, 10);
t1.join();

带可变参数包的构造

在【C++】C语言可变函数参数 | C++11可变参数模板 中我们学习到C++支持函数模板的可变参数,这里thread的构造函数就是一个模板函数:

template <class Fn, class... Args>
explicit thread (Fn&& fn, Args&&... args);
  • fn:可调用对象,比如:
    1. 函数指针
    2. 仿函数(函数对象)
    3. lambda表达式
    4. 被bind或functional包装器包装后的可调用对象等
  • args...:调用可调用对象fn时所需要的若干参数。
#include <thread>
#include <iostream>
#include <functional>
#include <Windows.h>
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;
	}
};
My_class my_instance;

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

	Sleep(1);

	//2. 仿函数 (函数对象)
	thread t2(My_class(), 10, 20);

	Sleep(1);

	//3. lambda表达式
	thread t3([](const string& str) ->void {cout << str << endl;}, "I am thread-3");

	Sleep(1);

	//4. 被bind或functional包装器包装后的可调用对象等
	thread t4(std::function<void(int, int)>(func1), 100, 110);

	Sleep(1);

	thread t5(std::bind(&My_class::operator(), &my_instance, std::placeholders::_1, std::placeholders::_2), 220, 230);

	t1.join();
	t2.join();
	t3.join();
	t4.join();
	t5.join();
	return 0;
}

输出:请添加图片描述

移动构造

thread提供了移动构造函数,能够用一个右值线程对象来构造一个线程对象:

thread t3 = thread(func, 4, 20);
thread t4(std::move(thread(func, 10, 20))); // 可以显式move一下

2. thread类的成员函数

thread::detach()

简单来说,若detach在创建线程对象后马上调用,用于把被创建线程与线程对象分离开,分离的线程变为后台线程,创建的线程的"死活"就与主线程无关。

主线程创建新线程后,也可以调用detach函数将新线程与主线程进行分离,分离后新线程会在后台运行,其所有权和控制权将会交给C++运行库,此时C++运行库会保证当线程退出时,其相关资源能够被正确回收。

  • 使用detach的方式回收线程的资源,一般在线程对象创建好之后就立即调用detach函数。
  • 否则线程对象可能会因为某些原因,在后续调用detach函数分离线程之前被销毁掉,这时就会导致程序崩溃。
  • 因为当线程对象被销毁时会调用thread的析构函数,而在thread的析构函数中会通过joinable判断这个线程是否需要被join,如果需要那么就会调用terminate终止当前程序(程序崩溃)。

thread::get_id()

作用是获取线程id。下面比较一下Windows下和Linux g++下的线程id的差异

#include <thread>
#include <iostream>
#include <functional>
#include <Windows.h>
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;
	}
};
My_class my_instance;

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

	Sleep(1);

	//2. 仿函数 (函数对象)
	thread t2(My_class(), 10, 20);

	Sleep(1);

	//3. lambda表达式
	thread t3([](const string& str) ->void {cout << str << endl;}, "I am thread-3");

	Sleep(1);

	//4. 被bind或functional包装器包装后的可调用对象等
	thread t4(std::function<void(int, int)>(func1), 100, 110);

	Sleep(1);

	thread t5(std::bind(&My_class::operator(), &my_instance, std::placeholders::_1, std::placeholders::_2), 220, 230);

	Sleep(100);

	cout << "thread-1: " << t1.get_id() << endl;
	cout << "thread-2: " << t2.get_id() << endl;
	cout << "thread-3: " << t3.get_id() << endl;
	cout << "thread-4: " << t4.get_id() << endl;
	cout << "thread-5: " << t5.get_id() << endl;

	t1.join();
	t2.join();
	t3.join();
	t4.join();
	t5.join();
	return 0;
}

运行结果:
请添加图片描述

thread::join()

该函数调用后会阻塞住当前调用join处的线程,当等待的线程结束后,主线程继续执行。

thread::joinable()

线程是否还在执行,joinable代表的是一个正在执行中的线程。

线程函数参数的问题

线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,就算线程函数的参数为引用类型,在线程函数中修改后也不会影响到外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参。比如:

void add(int& num)
{
	num++;
}
int main()
{
	int num = 0;
	thread t(add, num);
	t.join();

	cout << num << endl; //输出:0
	return 0;
}

如果要通过线程函数的形参改变外部的实参,可以参考以下三种方式:

#include <thread>
#include <iostream>

void ThreadFunc1(int& x)
{
	x += 10;
}
void ThreadFunc2(int* x)
{
	*x += 10;
}
int main()
{
	int a = 10;

	// 问题:在线程函数中对a修改,不会影响外部实参
	// 因为:线程函数参数虽然是引用方式,但其实际引用的是线程栈中的拷贝
	//std::thread t1(ThreadFunc1, a); // 这里的a传过去的不是引用哦!只是一份值拷贝
	//t1.join();
	//std::cout << a << std::endl;

	// 解决方法:
	// 1. 如果想要通过形参改变外部实参时,必须借助std::ref()函数
	std::thread t2(ThreadFunc1, std::ref(a));
	t2.join();
	std::cout << a << std::endl;

	// 2. 地址的拷贝
	std::thread t3(ThreadFunc2, &a);
	t3.join();
	std::cout << a << std::endl;

	// 3. lambda表达式,在捕捉列表中添加a的引用
	std::thread t4([&a] {a += 10;});
	t4.join();
	std::cout << a << std::endl;

	return 0;
}

[!Abstract] 对线程的初步总结

  1. 线程是操作系统中的一个概念,线程对象可以关联一个线程,用来控制线程以及获取线程的状态。
  2. 当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程。
  3. thread类是防拷贝的,不允许拷贝构造以及赋值,但是可以移动构造和移动赋值,即将一个线程对象关联线程的状态转移给其他线程对象,转移期间不意向线程的执行。

二、互斥量库 - mutex

标准库提供的四种互斥锁

1. std::mutex

mutex锁是C++11提供的最基本的互斥量,mutex对象之间不能进行拷贝,也不能进行移动。

mutex中常用的成员函数如下:

成员函数功能
lock对互斥量进行加锁
try_lock尝试对互斥量进行加锁
unlock对互斥量进行解锁,释放互斥量的所有权

线程函数调用lock()时,可能会发生以下三种情况:

  • 如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁
  • 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住
  • 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)

线程函数调用try_lock()时,可能会发生以下三种情况:

  • 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock 释放互斥量
  • 如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉
  • 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)

2. std::recursive_mutex

其允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权,释放互斥量时需要调用与该锁层次深度相同次数的 unlock(),除此之外,std::recursive_mutex 的特性和 std::mutex 大致相同。与 std::mutex 类似,std::recursive_mutex 提供了 lock()try_lock()unlock() 方法来管理锁的状态。但是,当同一个线程多次调用 lock() 时,std::recursive_mutex 允许这种行为,而不是导致死锁。

3. std::timed_mutex

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

4. std::recursive_timed_mutex

本质是上面两种锁的结合体。

5. lock_guard 和 unique_lock

C++11采用RAII的方式对锁进行了封装,于是就出现了lock_guard和unique_lock。

lock_guard的定义

lock_guard是C++11中的一个模板类,其定义如下:

template <class Mutex>
class lock_guard;
lock_guard的使用

lock_guard类模板主要是通过RAII的方式,对其管理的互斥锁进行了封装。

  • 在需要加锁的地方,用互斥锁实例化一个lock_guard对象,在lock_guard的构造函数中会调用lock进行加锁。
  • 当lock_guard对象出作用域前会调用析构函数,在lock_guard的析构函数中会调用unlock自动解锁。

可以有效避免死锁问题。

mutex mtx;
void func()
{
	//...
	//匿名局部域
	{
		lock_guard<mutex> lg(mtx); //调用构造函数加锁
		FILE* fout = fopen("data.txt", "r");
		if (fout == nullptr)
		{
			//...
			return; //调用析构函数解锁
		}
	} //调用析构函数解锁
	//...
}
int main()
{
	func();
	return 0;
}
lock_guard的模拟实现
#pragma once
#include <iostream>

namespace chen
{

	template<class Mutex>
	class lock_guard
	{
	public:
		lock_guard(Mutex& mtx)
			:_mtx(mtx)
		{
			std::cout << "lock_guard(Mutex& mtx)" << std::endl;
			_mtx.lock();
		}
		~lock_guard()
		{
			std::cout << "~lock_guard()" << std::endl;
			_mtx.unlock();
		}

		lock_guard& operator=(lock_guard<Mutex>&) = delete;
		lock_guard(lock_guard<Mutex>&) = delete;

	private:
		Mutex& _mtx;
	};
}
unique_lock的说明

但由于lock_guard太单一,用户没有办法对锁进行控制,因此C++11又提供unique_lock。

unique_lock与lock_guard类似,unique_lock类模板也是采用RAII的方式对锁进行了封装。在创建unique_lock对象调用构造函数时也会调用lock进行加锁,在unique_lock对象销毁调用析构函数时也会调用unlock进行解锁。

但lock_guard不同的是,unique_lock更加的灵活,提供了更多的成员函数:

  • 加锁/解锁操作:lock、try_lock、try_lock_for、try_lock_until和unlock。
  • 修改操作:移动赋值、swap、release(返回它所管理的互斥量对象的指针,并释放所有权)。
  • 获取属性:owns_lock(返回当前对象是否上了锁)、operator bool(与owns_lock的功能相同)、mutex(返回当前unique_lock所管理的互斥量的指针)。

std::unique_lockstd::lock_guard 都是 C++ 标准库中提供的互斥锁封装工具,它们都可以帮助简化多线程编程中的互斥操作。但是,它们在使用方式、功能和灵活性上有一些明显的区别。

std::lock_guardstd::unique_lock的区别:
std::lock_guard

  • std::lock_guard 是一个简单的互斥锁包装器,它的设计目标是提供一个简单的、RAII(Resource Acquisition Is Initialization)风格的锁管理机制。在 std::lock_guard 对象构造时,它会自动获取给定的互斥量,并在 std::lock_guard 对象销毁时自动释放该互斥量。这种机制可以确保在异常安全的情况下,互斥量总是会被正确释放。

  • 使用 std::lock_guard 通常是非常简单直接的:你只需要在作用域内定义一个 std::lock_guard 对象,该对象在其生命周期内会自动管理互斥量。但是,std::lock_guard 不提供手动控制锁的能力,一旦构造,它就会立即锁定互斥量,直到对象销毁。

std::unique_lock

  • std::unique_lock 提供了比 std::lock_guard 更高级的功能和更大的灵活性。它允许你延迟锁定、尝试锁定、手动解锁以及更复杂的锁定策略。此外,std::unique_lock支持条件变量,这是 std::lock_guard 所不具备的。

  • 使用 std::unique_lock,你可以选择在何时锁定和解锁互斥量,这对于某些复杂的同步需求是非常有用的。例如,你可能需要在某个条件满足时才锁定互斥量,或者在某个操作完成后立即解锁。


总的来说,std::lock_guard 是一个简单且易于使用的工具,适用于大多数基本的同步需求。而 std::unique_lock 则提供了更多的控制和灵活性,适用于更复杂的同步场景。

三、原子性操作库 - atomic

多线程并发的线程安全问题

多线程最主要的问题是共享数据带来的问题(即线程安全)。如果共享数据都是只读的,那么没问题,因为只读操作不会影响到数据,更不会涉及对数据的修改,所以所有线程都会获得同样的数据。但是,当一个或多个线程要修改共享数据时,就会产生很多潜在的麻烦。比如:

#include <iostream>
#include <thread>
using namespace std;

unsigned long sum = 0L;

void fun(size_t num)
{
	for (size_t i = 0; i < num; ++i)
		sum++;
}

int main()
{
	cout << "Before joining,sum = " << sum << std::endl;
	thread t1(fun, 10000000);
	thread t2(fun, 10000000);
	t1.join();
	t2.join();
	cout << "After joining,sum = " << sum << std::endl;
	return 0;
}

上述代码中分别让两个线程对同一个变量n进行了100000次++操作,理论上最终n的值应该是200000,但最终打印出n的值却是小于200000的:请添加图片描述

根本原因就是++-- 操作并不是一个原子操作,而是对应三条汇编指令:
load :将共享变量ticket从内存加载到寄存器中
update : 更新寄存器里面的值,执行-1操作
store :将新值,从寄存器写回共享变量ticket的内存地址

请添加图片描述

因此可能当线程1刚将n的值加载到寄存器中就被切走了,也就是只完成了++操作的第一步,而线程2可能顺利完成了一次完整的++操作才被切走,而这时线程1继续用之前加载到寄存器中的值完成剩余的两步操作,最终就会导致两个线程分别对共享变量n进行了一次++操作,但最终n的值却只被++了一次。

如何解决这个问题?

方法1:加锁解决线程安全问题

C++98中对于这里出现的线程安全的问题,会选择对共享修改的数据进行加锁保护。比如:

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

using namespace std;

unsigned long sum = 0L;
std::mutex mtx;

void fun(size_t num)
{
	for (size_t i = 0; i < num; ++i)
	{
		mtx.lock();
		sum++;
		mtx.unlock();
	}
}

int main()
{
	cout << "Before joining,sum = " << sum << std::endl;
	thread t1(fun, 10000000);
	thread t2(fun, 10000000);
	t1.join();
	t2.join();
	cout << "After joining,sum = " << sum << std::endl;
	return 0;
}

这里可以选择在for循环体里面进行加锁解锁,也可以选择在for循环体外进行加锁解锁。但效果终究是不尽人意的,在for循环体里面进行加锁解锁会导致线程的频繁进行加锁解锁操作,在for循环体外面进行加锁解锁会导致两个线程的执行逻辑变为串行,而且如果锁控制得不好,还容易造成死锁。

方法2:原子类解决线程安全问题

因此C++11中引入了原子操作。所谓原子操作:即不可被中断的一个或一系列操作,C++11引入的原子操作类型,使得线程间数据的同步变得非常高效。

原子类型名称对应的内置类型名称
atomic_boolbool
atomic_charchar
atomic_scharsigned char
atomic_ucharunsigned char
atomic_intint
atomic_uintunsigned int
atomic_shortshort
atomic_ushortunsigned short
atomic_longlong
atomic_ulongunsigned long
atomic_llonglong long
atomic_ullongunsigned long long
atomic_char16_tchar16_t
atomic_char32_tchar32_t
atomic_wchar_twchar_t

我们可以使用chrono库中的high_resolution_clock来测量两种方法的时间:

#include <iostream>  
#include <mutex>  
#include <thread>  
#include <atomic>  
#include <chrono>  

using namespace std;
using namespace chrono;

std::mutex mtx;
long sum = 0;
atomic_long sum_atomic{ 0 };

// 方法1:加锁  
void fuc(size_t num)
{
    for (size_t i = 0; i < num; ++i)
    {
        mtx.lock();
        sum++; // 非原子操作  
        mtx.unlock();
    }
}

// 方法2:原子性操作  
void fuc_atomic(size_t num)
{
    for (size_t i = 0; i < num; ++i)
        sum_atomic++; // 原子操作  
}

int main()
{
    // 测量方法1的时间  
    auto start = high_resolution_clock::now();

    thread t1(fuc, 1000000);
    thread t2(fuc, 1000000);

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

    auto end = high_resolution_clock::now();
    auto duration = duration_cast<milliseconds>(end - start);

    cout << "Method 1 (mutex) took " << duration.count() << " milliseconds." << endl;
    cout << "After joining, sum = " << sum << endl;

    // 重置sum以便进行下一次测量  
    sum = 0;

    // 测量方法2的时间  
    start = high_resolution_clock::now();

    thread t3(fuc_atomic, 1000000);
    thread t4(fuc_atomic, 1000000);

    t3.join();
    t4.join();

    end = high_resolution_clock::now();
    duration = duration_cast<milliseconds>(end - start);

    cout << "Method 2 (atomic) took " << duration.count() << " milliseconds." << endl;
    cout << "After joining, sum_atomic = " << sum_atomic << endl;

    return 0;
}

会发现使用原子性操作库中的原子类型,运行时间更短:
请添加图片描述

在C++11中,程序员不需要对原子类型变量进行加锁解锁操作,线程能够对原子类型变量互斥的访问。
更为普遍的,程序员可以使用atomic类模板,定义出需要的任意原子类型。

atmoic<T> t; // 声明一个类型为T的原子类型变量t

注意:原子类型通常属于"资源型"数据,多个线程只能访问单个原子类型的拷贝,因此在C++11中,原子类型只能从其模板参数中进行构造,不允许原子类型进行拷贝构造、移动构造以及operator=等,为了防止意外,标准库已经将atmoic模板类中的拷贝构造、移动构造、赋值运算符重载默认删除掉了:

atomic(const atomic&)            = delete;
atomic& operator=(const atomic&) = delete;

四、条件变量库 - condition_variable

wait系列成员函数

wait系列成员函数的作用就是让调用线程进行阻塞等待,包括waitwait_forwait_until

下面先以wait为例进行介绍,wait函数提供了两个不同版本的接口:

//版本一
void wait(unique_lock<mutex>& lck);
//版本二
template<class Predicate>
void wait(unique_lock<mutex>& lck, Predicate pred);

函数说明:

  • 调用第一个版本的wait函数时只需要传入一个互斥锁,线程调用wait后会立即被阻塞,直到被唤醒。
  • 调用第二个版本的wait函数时除了需要传入一个互斥锁,还需要传入一个返回值类型为bool的可调用对象,与第一个版本的wait不同的是,当线程被唤醒后还需要调用传入的可调用对象,如果可调用对象的返回值为false,那么该线程还需要继续被阻塞。

为什么调用wait系列函数时需要传入一个互斥锁?

  • 因为wait系列函数一般是在临界区中调用的,为了让当前线程调用wait阻塞时其他线程能够获取到锁,因此调用wait系列函数时需要传入一个互斥锁,当线程被阻塞时这个互斥锁会被自动解锁,而当这个线程被唤醒时,又会自动获得这个互斥锁。
  • 因此wait系列函数实际上有两个功能,一个是让线程在条件不满足时进行阻塞等待,另一个是让线程将对应的互斥锁进行解锁。

wait_for和wait_until函数的使用方式与wait函数类似:

  • wait_for函数也提供了两个版本的接口,只不过这两个版本的接口都比wait函数对应的接口多了一个参数,这个参数是一个时间段,表示让线程在该时间段内进行阻塞等待,如果超过这个时间段则线程被自动唤醒。
  • wait_until函数也提供了两个版本的接口,只不过这两个版本的接口都比wait函数对应的接口多了一个参数,这个参数是一个具体的时间点,表示让线程在该时间点之前进行阻塞等待,如果超过这个时间点则线程被自动唤醒。
  • 线程调用wait_for或wait_until函数在阻塞等待期间,其他线程调用notify系列函数也可以将其唤醒。此外,如果调用的是wait_for或wait_until函数的第二个版本的接口,那么当线程被唤醒后还需要调用传入的可调用对象,如果可调用对象的返回值为false,那么当前线程还需要继续被阻塞。
    注意: 调用wait系列函数时,传入互斥锁的类型必须是unique_lock

notify系列成员函数

notify系列成员函数的作用就是唤醒等待的线程,包括notify_onenotify_all

  • notify_one:唤醒等待队列中的首个线程,如果等待队列为空则什么也不做。
  • notify_all:唤醒等待队列中的所有线程,如果等待队列为空则什么也不做。

五、实现两个线程交替打印

面试题: 让两个线程交替打印,一个打印奇数,一个打印偶数

1. Linux下pthread程库的实现

// linux pthread version
#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>

pthread_cond_t condA = PTHREAD_COND_INITIALIZER;
pthread_cond_t condB = PTHREAD_COND_INITIALIZER;
pthread_mutex_t g_mtx = PTHREAD_MUTEX_INITIALIZER;
bool flag = true; // true代表A可以打印,false表示B可以打印

class ThreadData
{
public:
    ThreadData(const char* str)
        :_threadname(str)
    {}
    ~ThreadData() = default;
    const std::string& GetThreadName()
    {
        return _threadname;
    }
public:
    std::string _threadname;
};

void* RoutineA(void* argv)
{
    ThreadData td = *static_cast<ThreadData*>(argv);
    // 临界区 加锁

    while (true)
    {
        pthread_mutex_lock(&g_mtx);

        if (flag == false)
        {
            pthread_cond_wait(&condA, &g_mtx);
        }

        if (flag == true)
        {
            std::cout << "I am " << td.GetThreadName() << std::endl;
            sleep(1);
            flag = false;
            pthread_cond_signal(&condB);
        }

        pthread_mutex_unlock(&g_mtx);
    }
}

void* RoutineB(void* argv)
{
    ThreadData td = *static_cast<ThreadData*>(argv);
    // 临界区 加锁

    while (true)
    {
        pthread_mutex_lock(&g_mtx);

        if (flag == true)
        {
            pthread_cond_wait(&condB, &g_mtx);
        }

        if (flag == false)
        {
            std::cout << "I am " << td.GetThreadName() << std::endl;
            sleep(1);
            flag = true;
            pthread_cond_signal(&condA);
        }

        pthread_mutex_unlock(&g_mtx);
    }

}

int main()
{
    pthread_t tidA, tidB;
    ThreadData tdA("thread-A");
    ThreadData tdB("thread-B");

    pthread_create(&tidA, nullptr, RoutineA, (void*)&tdA);
    pthread_create(&tidB, nullptr, RoutineB, (void*)&tdB);

    pthread_join(tidA, nullptr);
    pthread_join(tidB, nullptr);
    return 0;
}

2. C++线程库的实现

// C++11 thread version

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

int main()
{
    std::mutex mtx;
    std::condition_variable cv;
    bool flag = true;

    std::thread t1([&mtx, &flag, &cv](int count = 100) {
        for (int i = 0; i < count; i += 2)
        {
            std::unique_lock<std::mutex> lock(mtx);
            if (flag == false)
            {
                cv.wait(lock, [&]()->bool {return flag;});
            }
            std::cout << i << std::endl;
            flag = false;
            cv.notify_one();
        }
        });

    std::thread t2([&mtx, &flag, &cv](int count = 100) {
        for (int i = 1; i < count; i += 2)
        {
            std::unique_lock<std::mutex> lock(mtx);
            if (flag == true)
            {
                cv.wait(lock, [&]()->bool {return !flag;});
            }
            std::cout << i << std::endl;
            flag = true;
            cv.notify_one();
        }
        });

    t1.join();
    t2.join();
    return 0;
}

六、并行和并发的区别

在操作系统中,"并行"和"并发"是两个相关但又不同的概念:

  1. 并行(Parallelism)

    • 并行指的是系统中同时执行多个任务的能力。这些任务可以在同一时刻发生,通过利用多个处理单元(比如多核处理器或者分布式系统中的多个计算节点)来实现。在并行中,多个任务同时进行,它们之间可能是独立的,也可能是相关联的。并行通常用于提高系统的性能和效率。
  2. 并发(Concurrency)

    • 并发指的是系统中同时具有多个活动实体(比如进程、线程或任务),它们在一段时间内可能重叠执行,但不一定同时执行。这意味着在同一时间点上,系统中可能存在多个活跃的实体,但它们的执行可能交错进行。并发通常用于提高系统的响应性、资源利用率和结构简洁性。

总的来说,可以这样理解:

  • 并行是同时做多件事情,着重于同时性。
  • 并发是指系统在同一时间段内能够处理多个任务,着重于交替性。

在实际应用中,这两个概念经常会同时存在,因为在多任务系统中,通常会使用并发来处理多个任务,同时也会利用并行来加速单个任务的执行。

并发Concurrency
并行Parallelism
任务 B
任务 A
任务 C
任务 D
任务 B
任务 A
任务 C
任务 D

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1668666.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

报告!Golang冲上来啦!

今天又来讲Go语言&#xff0c;根据全球知名的编程语言排行榜TIOBE在4月份公布的最新的编程语言排名&#xff0c;令人瞩目的是&#xff0c;Go语言已经跃升至历史最高位&#xff0c;位列排行榜第七名&#xff0c;并且Go语言是前十榜单中最年轻的编程语言。这一成绩不仅彰显了Go语…

java1.8 的 client runtime compiler和server runtime compiler

你好&#xff0c;我是 shengjk1&#xff0c;多年大厂经验&#xff0c;努力构建 通俗易懂的、好玩的编程语言教程。 欢迎关注&#xff01;你会有如下收益&#xff1a; 了解大厂经验拥有和大厂相匹配的技术等 希望看什么&#xff0c;评论或者私信告诉我&#xff01; 文章目录 一…

MySQL数据查询优化

MySQL调优是开发中必不可少的内容&#xff0c;以下是对MySQL查询性能优化的部分总结 1. explain关键字的使用 explain关键字可以模拟优化器执行sql查询语句&#xff0c;获取sql的执行信息&#xff0c;使用方法&#xff1a; explainsql语句 1.1 字段详解 id&#xff08;select …

Linux---在Xshell上配置软件包管理器yum

一、什么是yum 在Linux下安装软件, 一个通常的办法是下载到程序的源代码, 并进行编译, 得到可执行程序. 但是这样太麻烦了, 于是有些人把一些常用的软件提前编译好, 做成软件包(可以理解成windows上的安装程序)放在一个服务器上, 通过包管理器可以很方便的获取到这个编译好的软…

练习题(2024/5/12)

1二分查找 给定一个 n 个元素有序的&#xff08;升序&#xff09;整型数组 nums 和一个目标值 target &#xff0c;写一个函数搜索 nums 中的 target&#xff0c;如果目标值存在返回下标&#xff0c;否则返回 -1。 示例 1: 输入: nums [-1,0,3,5,9,12], target 9 输出: 4…

解决vue3项目打包后部署后某些静态资源图片不加载问题

目录 问题 原因 解决方案 问题 开发完项目打包并部署 然后访问时发现导航栏背景图片没加载 打开浏览器控制台发现这张图片报错404 原因 可能是因为在部署后的服务器环境中对中文文件名的支持不完善。服务器在解析 URL 时可能无法正确识别或编码中文字符&#xff0c;导致无…

数据结构(四)——二叉树和堆(下)

制作不易&#xff0c;三连支持一下呗&#xff01;&#xff01;&#xff01; 文章目录 前言一、二叉树链式结构的实现总结 前言 这篇博客我们将来了解普通二叉树的实现和应用&#xff0c;对大家之前分治和递归的理解有所挑战。 一、二叉树链式结构的实现 1.前置说明 在学习二叉…

网络编程套接字详解

目录 1. 预备介绍 2.网络字节序 3.udp网络程序 4.地址转换函数 5.udp网络编程 1.预备介绍 1.1源IP地址和目标IP地址 举个例子: 从北京出发到上海旅游, 那么源IP地址就是北京, 目标IP地址就是上海. 1.2 端口号 作用: 标识一个进程, 告诉OS这个数据交给那个进程来处理; (1)…

爱分析基于杭州云器Lakehouse实现成本最优的一体化管理,新一代数据平台的建设方式

导读 1.当前&#xff0c;企业在大数据和数据中台建设上取得成果&#xff0c;但数据开发管理仍具挑战性&#xff08;成本、效率、复杂度&#xff09;。 2.随数据平台领域成熟&#xff0c;厂商应结合自身需求&#xff0c;重新思考“基于开源自建数据平台”的重资产模式与“购买…

【AMBA Bus ACE 总线 7.1 -- ACE Domains 介绍 2】

请阅读【AMBA Bus ACE 总线与Cache 专栏 】 欢迎学习:【嵌入式开发学习必备专栏】 文章目录 AxDOMAINAxDOMAIN[1:0]的值及含义AxDOMAIN 在ARM的AXI Coherency Extensions (ACE) 协议中,AxDOMAIN[1:0]是一个重要的信号字段,用于指示传输的域类型。这个字段影响了传输对系统…

Spring Boot支持发送邮件

GitHub&#xff1a;SpringBootDemo Gitee&#xff1a;SpringBootDemo 微信公众号&#xff1a; 通过Spring Boot整合邮件任务&#xff0c;支持发送邮件&#xff0c;可以实现服务故障时向指定邮箱发送邮件。 0 开发环境 JDK&#xff1a;1.8Spring Boot&#xff1a;2.7.18 1…

花了24小时做的采购、库存、进销存excel模板,真心好用,免费分享

花了24小时做的采购、库存、进销存excel模板&#xff0c;真心好用 在企业的日常运营中&#xff0c;进销存管理是一项至关重要的任务。它不仅涉及到商品的采购、销售和库存管理&#xff0c;还直接影响到企业的财务状况和市场竞争力。为了提高管理效率&#xff0c;许多企业选择使…

传感网应用开发教程--AT指令访问新大陆云平台(ESP8266模块+物联网云+TCP)

实现目标 1、熟悉AT指令 2、熟悉新大陆云平台新建项目 3、具体目标&#xff1a;&#xff08;1&#xff09;注册新大陆云平台&#xff1b;&#xff08;2&#xff09;新建一个联网方案为WIFI的项目&#xff1b;&#xff08;3&#xff09;ESP8266模块&#xff0c;通过AT指令访问…

经典笔试题:快速排序 计数排序

Problem: 912. 排序数组 思路 &#x1f468;‍&#x1f3eb; 三叶题解 &#x1f496; AC&#xff1a;计数排序 时间复杂度: O ( n ) O(n) O(n) 空间复杂度: O ( n ) O(n) O(n) class Solution {public int[] sortArray(int[] nums) {int max -50001, min 50001;for (…

HCIP的学习(15)

第六章&#xff0c;BGP—边界网关协议 自治系统—AS ​ 定义&#xff1a;由一个单一的机构或组织所管理的一系列IP网络及其设备所构成的集合。 ​ AS的来源&#xff1a; 整个网络规模过大&#xff0c;会导致路由信息收敛速度过慢&#xff0c;设备对相同目标认知不同。AS之间…

MATLAB实现遗传算法优化选址-路径LRP问题(Location-Routing Problem)

MATLAB实现遗传算法优化选址-路径LRP问题(Location-Routing Problem) 一、模型 选址车辆路径问题&#xff08;Location-Routing Problem, LRP&#xff09;是一个组合优化问题&#xff0c;旨在同时优化设施位置的选择和车辆的配送路径。在这个问题中&#xff0c;我们考虑一个由…

栈与队列的实现

前言 本次博客将要实现一下栈和队列&#xff0c;好吧 他们两个既可以使用动态数组也可以使用链表来实现 本次会有详细的讲解 栈的实现 栈的基础知识 什么是栈呢&#xff1f; 栈的性质是后进先出 来画个图来理解 当然可不可以出一个进一个呢&#xff0c;当然可以了 比如…

C++笔记(STL标准库)

1.STL六大部件 容器 Containers分配器 Allocators&#xff1a;帮容器分配内存算法 Algorithms迭代器 Iterators&#xff1a;算法通过迭代器操作容器里的数据&#xff0c;是一种泛化的指针适配器 Adapters&#xff1a;修改或扩展已有类或函数的接口以满足特定的需求仿函数 Func…

Python深度学习基于Tensorflow(9)注意力机制

文章目录 注意力机制是怎么工作的注意力机制的类型 构建Transformer模型Embedding层注意力机制的实现Encoder实现Decoder实现Transformer实现 注意力机制的主要思想是将注意力集中在信息的重要部分&#xff0c;对重要部分投入更多的资源&#xff0c;以获取更多所关注目标的细节…

2024最新商业视频打赏系统源码 多套模板 有代理后台 已对接支付

简介&#xff1a; 2024最新商业视频打赏系统源码 多套模板 有代理后台 已对接支付 图片&#xff1a; 源码下载