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

文章目录

  • 一、线程库 - 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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Ubuntu18.04+melodic】抓取环境设置

UR5_gripper_camera_gazebo&#xff08;无moveit&#xff09; 视频讲解 B站-我要一米八了-抓取不止&#xff01;Ubuntu 18.04下UR5机械臂搭建Gazebo环境&#xff5c;开源分享 运行步骤 1.创建工作空间 catkin_make2.激活环境变量 source devel/setup.bash3.1 rviz下查看模…

Oracle 修改数据库的字符集

Oracle 修改数据库的字符集 alter system enable restricted session; alter database "cata" character set ZHS16CGB231280; alter database "cata" national character set ZHS16CGB231280; alter system enable restricted session; alter database…

使用动态种子的DGA:DNS流量中的意外行为

Akamai研究人员最近在域名系统&#xff08;DNS&#xff09;流量数据中观察到&#xff1a;使用动态种子的域名生成算法&#xff08;Domain Generation Algorithm&#xff0c;DGA&#xff09;的实际行为&#xff0c;与对算法进行逆向工程推测的预期行为之间存在一些差异。也就是说…

C++ 基础 输入输出

一 C 的基本IO 系统中的预定义流对象cin和cout: 输入流&#xff1a;cin处理标准输入&#xff0c;即键盘输入&#xff1b; 输出流&#xff1a;cout处理标准输出&#xff0c;即屏幕输出&#xff1b; 流&#xff1a;从某种IO设备上读入或写出的字符系列 使用cin、cout这两个流对…

在Ubuntu上安装Anaconda之后,启动失败

为了方便管理Pythonu环境&#xff0c;在Ubuntu的Docker容器中安装了Anaconda&#xff0c;安装完成&#xff0c;启动时出现如下错误&#xff1a; conda activate xxx usage: conda [-h] [--no-plugins] [-V] COMMAND ... conda: error: argument COMMAND: invalid choice: acti…

Linux的基础IO:文件描述符 重定向本质

目录 前言 文件操作的系统调用接口 open函数 close函数 write函数 read函数 注意事项 文件描述符-fd 小补充 重定向 文件描述符的分配原则 系统调用接口-dup2 缓冲区 缓冲区的刷新策略 对于“2”的理解 小补充 前言 在Linux中一切皆文件&#xff0c;打开文件…

springcloud服务间调用 feign 的使用

引入依赖包 <!-- 服务调用feign --><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency>创建调用外部服务的接口 需要使用的地方注入 使用 启动类增…

CTFHUB-技能树-Web题-RCE(远程代码执行)-eval执行

CTFHUB-技能树-Web题-RCE&#xff08;远程代码执行&#xff09; 文章目录 CTFHUB-技能树-Web题-RCE&#xff08;远程代码执行&#xff09;eval执行解题方法&#xff1a;构造网址&#xff0c;查找当前目录文件并没有发现flag,接着查看上一级目录接着查看上一级接着查看上一级目录…

luceda ipkiss教程 66:金属线的钝角转弯

案例分享&#xff1a;金属线的135度转弯&#xff1a; 所有代码如下&#xff1a; from si_fab import all as pdk import ipkiss3.all as i3 from ipkiss.geometry.shape_modifier import __ShapeModifierAutoOpenClosed__ from numpy import sqrtclass ShapeManhattanStub(__…

一种快速H.264 NALU快速搜索算法

1. 引言 在播放H.264码流的时候,进行NALU的搜索的效率高低影响着系统的性能。有采用普通逐字节搜索的算法,有利用cpu的simd的单指令多数据操作的并行功能进行搜索的算法,今天要介绍的是一个非常简单而且高效的快速搜索算法,而且不需要利用simd指令,搜索的速度甚至快于我之…

Spring-依赖来源

依赖来源 1 Spring BeanDefinition&#xff08;xml,注解&#xff0c;BeanDefinitionBuilder, 还有API实现的单例对象&#xff09; 2 Spring 内建BeanDefinition 3 内建单例对象 依赖注入和依赖查找的区别 Context.refresh() 的时候会调用这个方法&#xff1a;prepareBeanF…

如何通过wifi网络将串口数据发送到多个设备

摘要&#xff1a;当lora电台的速率无法满足高速传输时&#xff0c;可以考虑用“串口服务器”。本文介绍一下如何使用TP-LINK的TL-CPE300D实现一对多的数据发送。 当前也有使用lora电台的&#xff0c;但是lora电台支持的速率有限&#xff0c;可能最大支持到9600&#xff0c;甚至…

[虚拟机+单机]梦幻契约H5修复版_附GM工具

本教程仅限学习使用&#xff0c;禁止商用&#xff0c;一切后果与本人无关&#xff0c;此声明具有法律效应&#xff01;&#xff01;&#xff01;&#xff01; 教程是本人亲自搭建成功的&#xff0c;绝对是完整可运行的&#xff0c;踩过的坑都给你们填上了 视频演示 [虚拟机单…

【电路笔记】-Twin-T振荡器

Twin-T振荡器 文章目录 Twin-T振荡器1、概述2、Twin-T振荡器3、Twin-T放大4、Twin-T 振荡器示例5、总结Twin-T 振荡器是另一种 RC 振荡器电路,它使用两个并联的 RC 网络来产生单一频率的正弦输出波形。 1、概述 Twin-T 振荡器是另一种类型的 RC 振荡器,它产生正弦波输出,用…

PTP 对时协议 IEEE1588 网络对时 硬件基础

前言 在很多应用场景有精确对时的需求&#xff0c;例如车载网络&#xff0c;音视频流&#xff0c;工业网络。本文档将会阐述对时的硬件需求。 协议 流行的协议为 IEEE1588 标准指定的对时方法&#xff0c;名为 PTP 对时协议。 网卡硬件要求 找到某型网卡的特性描述&#x…

Antd Table组件,state改变,但是render并不会重新渲染

背景 在table上面&#xff0c;当鼠标放在cell上面的时候&#xff0c;需要去请求接口拉取数据&#xff0c;然后setList(res.result)后&#xff0c;希望render中的traceIds也能够实时更新渲染。 const [traceIds, setTraceIds] useState() // 需要展示在popover上面的数据&…

基于STM32F401RET6智能锁项目(环境搭建)

工程搭建 MDK&#xff0c;固件库&#xff0c;芯片包下载 下载keil5&#xff0c;stm32f4xx的固件库以及stm32f4的芯片包 keil官网&#xff1a;https://www2.keil.com/mdk5/ stm32中国官网&#xff1a;https://www.stmcu.com.cn/ 创建工程 1、新建一个工程文件夹&#xff0c;…

漫威争锋Marvel Rivals怎么搜索 锁区怎么搜 游戏搜不到怎么办

即将问世的《漫威争锋》&#xff08;Marvel Rivals&#xff09;作为一款万众期待的PvP射击游戏新星&#xff0c;荣耀携手漫威官方网站共同推出。定档5月11日清晨9时&#xff0c;封闭Alpha测试阶段将正式揭开序幕&#xff0c;持续时间长达十天之久。在此首轮测试窗口&#xff0c…

加速科技突破2.7G高速数据接口测试技术

随着显示面板分辨率的不断提升&#xff0c;显示驱动芯片&#xff08;DDIC&#xff09;的数据接口传输速率越来越高&#xff0c;MIPI、LVDS/mLVDS、HDMI等高速数据接口在DDIC上广泛应用。为满足高速数据接口的ATE测试需求&#xff0c;作为国内少数拥有完全自研的LCD Driver测试解…

Facebook消息群发脚本的制作思路!

在数字化社交日益盛行的今天&#xff0c;Facebook作为全球最大的社交平台之一&#xff0c;为企业和个人提供了广阔的交流与合作空间。 然而&#xff0c;手动向大量用户发送消息既耗时又低效&#xff0c;因此&#xff0c;开发一款能够自动群发消息的脚本成为了许多人的需求&…