文章目录
- 一、线程库 - 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
:可调用对象,比如:- 函数指针
- 仿函数(函数对象)
- lambda表达式
- 被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] 对线程的初步总结
- 线程是操作系统中的一个概念,线程对象可以关联一个线程,用来控制线程以及获取线程的状态。
- 当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程。
- 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_lock
和 std::lock_guard
都是 C++ 标准库中提供的互斥锁封装工具,它们都可以帮助简化多线程编程中的互斥操作。但是,它们在使用方式、功能和灵活性上有一些明显的区别。
std::lock_guard和std::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_bool | bool |
atomic_char | char |
atomic_schar | signed char |
atomic_uchar | unsigned char |
atomic_int | int |
atomic_uint | unsigned int |
atomic_short | short |
atomic_ushort | unsigned short |
atomic_long | long |
atomic_ulong | unsigned long |
atomic_llong | long long |
atomic_ullong | unsigned long long |
atomic_char16_t | char16_t |
atomic_char32_t | char32_t |
atomic_wchar_t | wchar_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系列成员函数的作用就是让调用线程进行阻塞等待,包括wait
、wait_for
和wait_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_one
和notify_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;
}
六、并行和并发的区别
在操作系统中,"并行"和"并发"是两个相关但又不同的概念:
-
并行(Parallelism):
- 并行指的是系统中同时执行多个任务的能力。这些任务可以在同一时刻发生,通过利用多个处理单元(比如多核处理器或者分布式系统中的多个计算节点)来实现。在并行中,多个任务同时进行,它们之间可能是独立的,也可能是相关联的。并行通常用于提高系统的性能和效率。
-
并发(Concurrency):
- 并发指的是系统中同时具有多个活动实体(比如进程、线程或任务),它们在一段时间内可能重叠执行,但不一定同时执行。这意味着在同一时间点上,系统中可能存在多个活跃的实体,但它们的执行可能交错进行。并发通常用于提高系统的响应性、资源利用率和结构简洁性。
总的来说,可以这样理解:
- 并行是同时做多件事情,着重于同时性。
- 并发是指系统在同一时间段内能够处理多个任务,着重于交替性。
在实际应用中,这两个概念经常会同时存在,因为在多任务系统中,通常会使用并发来处理多个任务,同时也会利用并行来加速单个任务的执行。