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


一、线程库 - 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
  • 8
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_宁清

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值