C/C++程序员最苦恼的是自己跨平台能力不是一半弱。如果想跨平台,有一大波函数库等着你来深入研究。你再反观java。。。。
一、原子操作
所谓原子操作,就是多线程中“最小的且不可并行化的操作”。通常原子操作都是互斥访问保证的。但是互斥一般靠平台相关汇编指令,这也是为什么C++11之前一直没有做的原因。
#include <atomic> //原子操作需要的头文件
#include <thread> //线程头文件
#include <iostream>
using namespace std;
atomic_llong total{ 0 };//原子数据类型long long
//这样的构词法还有atomic_int等等
//下面的东西不是这一节的内容
void func(int)
{
for (long long i = 0; i < 100000000LL; ++i)
total = total + i;
}
int main()
{
thread t1(func, 0);
thread t2(func, 0);
t1.join();
t2.join();
cout << total << endl;
return 0;
}
可以通过<cstdatomic>中查看内置的原子操作。这就出现一个问题。非内置类型始终怎么实现原子操作的。这就是atom模板类,std::atomic<T> t; 如:
atomic<float> ad{ 12.7f };//这种写法是C++11推荐的
原子操作通常属于“资源型”的数据。这意味着多个线程通常只能访问单个原子类型的拷贝。因此C++11中,原子类型只能从其模板参数中进行构造,标准不允许原子类型进行拷贝构造,移动构造,以及使用operator=等 ,以防止出现意外。这样无法编译。
atomic<float> ad1{ad };/./错误
为了避免线程间关于a的竞争。模板改了很多地方。。
atomic<int> a;
int b =a;//相当于b = a.load();
int a =1;//相当于a.store(1);
二、线程
1.线程对象的创建
#include <thread>
#include <iostream>
using namespace std;
void func(int)
{}
int main()
{
thread t1(func, 0);
t1.join();
return 0;
}
线程的构造函数参数,可以视为,(要执行的函数名,该函数参数1,该函数参数2,。。。。)
在这里,join是阻塞函数。意义为等上面跑完才开始执行下一个操作。 我们如果没有他会怎么样呢?主线程继续往下跑,跑到return 0;但此时t1线程可能没有跑完,线程对象却要被强制释放。如下面代码;
#include <thread>
#include <iostream>
using namespace std;
void func(int a)
{
for (int i = 0; i < 10; ++i)
std::cout << a << endl;
}
int main()
{
int a;//停机变量无意义
{
thread t1(func, 1);
thread t2(func, 2);
}//运行到这里,t1,t1没有了
std::cin >> a;
return 0;
}
如果改成这样,可以正常执行:
int main()
{
int a;
{
thread t1(func, 1);
thread t2(func, 2);
t1.join();//主线程被阻塞了,停在这里,等t1线程对象的线程执行完再运行
t2.join();
}
std::cin >> a;
return 0;
}
如果不希望线程被阻塞吗,将线程与线程对象分离可以用t1.detach();将线程与线程对象分离。这里要说明下,线程与线程对象是两码事(这个很重要)。我们仅是依托线程对象来创建线程。
如下;
int main()
{
int a;
{
thread t1(func, 1);
thread t2(func, 2);
t1.detach();//主线程没有被阻塞,将线程与线程对象分离
t2.detach();
}//运行到这一步,线程对象依然会析构,但是线程却可以继续运行。
std::cin >> a;
return 0;
}
这也从侧面描述了,用detach将线程与线程对象分开后,就不能合并了。
2.线程的特点
线程不能复制,但可以移动(即使用std::move())。线程移动后,线程对象t不再代表任何线程。。
另外还可以用std::bind或lambda表达式创建。
thread t1(std::bind(func, 1));
thread t2([](int, int) {},1,2);
t1.join();
t2.join();
三、互斥量(实质就是锁的变量)
互斥量是一种同步原语,线程同步手段,用于保护多线程同时访问共享数据。“互斥量”的翻译十分有迷惑性。它就是“锁类”。以至于如果不这样理解,将会对后面的条件变量混淆。C++11提供了4种互斥量。
1.独占互斥量std::mutex
互斥量接口都很相似,一般用法是通过lock()方法来阻塞线程,直到获得互斥量所有权为止。线程获得互斥量并完成任务之后,就必须使用unlock()来解除对互斥量的占用,lock()和unlock()必须成对出现。try_lock()尝试锁定互斥量,如成功返回true如失败返回false,他是非阻塞的,看来可以用来检查当前互斥量的状态。
改动上面的程序将函数变成加锁的:
#include <thread>
#include <iostream>
#include <mutex>
using namespace std;
std::mutex uni_lock;
void func(int a)
{
uni_lock.lock();
for (int i = 0; i < 10; ++i)
std::cout << a << endl;
uni_lock.unlock();
}
int main()
{
int a;
{
thread t1(func, 1);
thread t2(func, 2);
t1.join();
t2.join();
}
std::cin >> a;
return 0;
}
这显示这就友好了
官方建议尽量使用更安全的lock_guard。因为他在构造时自动加锁,析构时自动解锁,防止忘解锁的事情发生。lock_guard是个 类模板,形如其名托管互斥量。后面的几个互斥量基本都用这种方法。
std::mutex u_lock;
void func(int a)
{
std::lock_guard<std::mutex> locker(u_lock);
for (int i = 0; i < 10; ++i)
std::cout << a << endl;
}
2.递归互斥量std::recursive_mutex
需要说明的是,还是尽量不要使用递归互斥量的好
(1)需要用到递归锁定的多线程,往往可以简化为迭代。允许递归容易放纵复杂逻辑产生。
(2)递归锁效率一般低一些。
(3)递归超过一定数目再lock进行调用会抛出std::system错误
3.带超时的互斥量std::timed_mutex与std::recursive_timed_mutex
std::timed_mutex u_lock;
void func(int a)
{
std::chrono::milliseconds timeout(100);
while (1)
{
if (u_lock.try_lock_for(timeout))
{
///...///
}
}
}
4.给互斥量上的的两种区域锁
上面我们介绍了lock_guard,这其实是一种区域锁,内部用实现机制是类模板。四、条件变量
条件变量是C++11提供的另一种用于等待的同步机制,它能阻塞一个或多个线程。直到收到另一个线程发出的通知或者超时,才会唤醒当前阻塞的线程。条件变量需要和互斥的量配合起来用。C++11提供两种条件变量。- std::condition_variable:必须与std::unique_lock配合使用(上文提到一种区间锁)
- std::condition_variable_any:更加通用的条件变量,可以与任意类型的锁配合使用,相比前者使用时会有额外的开销。
他们的成员函数相同。
成员函数 | 说明 |
---|---|
notify_one | 通知一个等待线程 |
notify_all | 通知全部等待线程 |
wait | 阻塞当前线程直到被唤醒 |
wait_for | 阻塞当前线程直到被唤醒或超过指定的等待时间(长度) |
wait_until | 阻塞当前线程直到被唤醒或到达指定的时间(点) |
#include <iostream> // std::cout
#include <thread> // std::thread
#include <mutex> // std::mutex, std::unique_lock
#include <condition_variable> // std::condition_variable
std::mutex mtx; // 全局互斥锁.
std::condition_variable cv; // 全局条件变量.
bool ready = false; // 全局标志位.
//下面是重点
void do_print_id(int id)
{
std::unique_lock <std::mutex> lck(mtx); //独占锁
while (!ready) // 如果标志位不为 true, 则等待...
cv.wait(lck); // 当前线程被阻塞, 当全局标志位变为 true 之后,
// 线程被唤醒, 继续往下执行打印线程编号id.
std::cout << "thread " << id << '\n';
}
void go()
{
std::unique_lock <std::mutex> lck(mtx);
ready = true; // 设置全局标志位为 true.
cv.notify_all(); // 通知唤醒所有线程.与上面额wait函数有关。
}
//上面是重点。main函数就是为了生成10个线程。每个线程先死循环,之后突然运行go()打开死循环。
int main()
{
std::thread threads[10];
//下面开10个线程:
for (int i = 0; i < 10; ++i)
threads[i] = std::thread(do_print_id, i);
go(); // go!
for (auto & th : threads) //直接诶是
th.join();
return 0;
}