一.线程执行体:
Lambda表达式的多线程
#include<iostream>
#include<thread>
#include<vector>
#include<algorithm>
using namespace std;
int main() {
thread td([](int a, int b) {
cout << a << "+" << b << "=" << a + b << endl;
},1,2);
td.join();
system("pause");
}
对象的多线程
struct functor
{
void operator()(int a, int b) {
cout << a << "+" << b << "=" << a + b << endl;
}
};
int main() {
thread td(functor(),1,2);
td.join();
system("pause");
}
使用std::bind表达式绑定对象和其非静态成员函数
using namespace std;
class C {
int data_;
public:
C(int data) :data_(data) {}
void member_fun(int c) {
cout << "this->data=" << this->data_ << "; extend c=" << c << endl;
}
};
int main() {
C obj(10);
thread td(bind(&C::member_fun, &obj,3));
td.join();
system("pause");
}
使用Lambda表达式调用对象的非静态成员函数
class C {
public:
int data_;
C(int data) :data_(data) {}
void member_fun(int c) {
cout << "this->data=" << this->data_ << "; extend c=" << c << endl;
}
};
int main() {
C obj(10);
auto a = [obj]()mutable {obj.member_fun(3); };
obj.data_ = 11;
thread td(a);
td.join();
thread td2([&obj]() {obj.member_fun(4); });
td2.join();
system("pause");
}
注意结果的输出,两种lambda策略,上面一种是复制obj,下面是引用。所以打印时一个是10,一个是11
二.线程管理函数
1.
#include <iostream>
#include <thread>
#include <iomanip>
int main()
{
std::thread td([](){});
std::cout << "td.joinable() = " << std::boolalpha << td.joinable() << std::endl;
td.detach();
std::cout << "td.joinable() = " << std::boolalpha << td.joinable() << std::endl;
}
2.
#include <iostream>
#include <thread>
#include <iomanip>
int main()
{
std::thread td([](){});
std::cout << "td.joinable() = " << std::boolalpha << td.joinable() << std::endl;
td.join();
std::cout << "td.joinable() = " << std::boolalpha << td.joinable() << std::endl;
}
3.RAII
class thread_guard {
std::thread& t_;
public:
explicit thread_guard(std::thread& t) : t_(t) { }
thread_guard(const thread_guard&) =delete;
thread_guard& operator=(const thread_guard&) =delete;
~thread_guard() { if (t_.joinable()) t_.join(); }
};
三.互斥Mutex
std::mutex 互斥对象
std::timed_mutex 带有超时的互斥,超时后直接放弃
std::recusive_mutex 允许被同一个程序递归的lock unlock
std::recusive_timed_mutex 带了超时的xx
std::shared_timed_mutex(c++14) 允许多个线程共享所有权的互斥对象,比如读写锁
用mutex对set的insert操作进行保护,实现安全的并发访问
#include<iostream>
#include<thread>
#include<vector>
#include<algorithm>
#include "ThreadGuard.h"
#include <set>
#include <mutex>
#include<random>
int main() {
std::set<int> int_set;
std::mutex mt;
auto f = [&int_set, &mt]() {
try {
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<> dis(1, 1000);
for (std::size_t i = 0; i != 100000; ++i) {
mt.lock();
int_set.insert(dis(gen));
mt.unlock();
}
}
catch (...) {}
};
std::thread td1(f), td2(f);
td1.join();
td2.join();
system("pause");
}
四.使用RAII管理互斥对象
std::lock_guard严格基于作用域的锁管理类模板,构造时是否加锁是可选的,析构时自动释放锁,所有权不可转移,对象生存期内不允许手动加锁和释放锁。。lock_guard 对象不可被拷贝构造或移动构造
std::unique_lock 更加灵活的锁管理模板,构造时是否加锁可选,在对象析构时如果持有锁会自动释放锁,所有权可以转移。对象生命周期允许手动加锁和释放锁。构造(或移动(move)赋值)时,unique_lock 对象需要传递一个 Mutex 对象作为它的参数,新创建的 unique_lock 对象负责传入的 Mutex 对象的上锁和解锁操作。
unique_lock(const unique_lock&) = delete;
unique_lock(unique_lock&& x);
std::shared——lock(c++14)
#include <iostream> // std::cout
#include <thread> // std::thread
#include <mutex> // std::mutex, std::lock_guard, std::adopt_lock
#include <chrono>
#include <stdexcept>
std::mutex mtx; // mutex for critical section
void print_thread_id(int id) {
try {
for (int i = 0; i < 10; i++) {
//mtx.lock();
//std::lock_guard<std::mutex> lck(mtx, std::adopt_lock
//std::lock_guard<std::mutex> lck(mtx);
std::this_thread::sleep_for(std::chrono::milliseconds(1));
std::cout << "thread #" << id << ">>" << i <<'\n';
if (i == 7) throw (std::logic_error("fake error"));
//mtx.unlock();
}
}
catch (...) {
std::cout << "exception" << std::endl;
}
}
int main()
{
std::thread threads[10];
// spawn 10 threads:
for (int i = 0; i<10; ++i)
threads[i] = std::thread(print_thread_id, i + 1);
for (auto& th : threads) th.join();
system("pause");
return 0;
}
加锁策略:
1.默认 请求锁,阻塞当前线程直到成功获得锁 三种都支撑
2.std::defer_lock 不请求锁 unique_lock,shared_lock
3.std::try_to_lock 尝试请求锁,但不阻塞线程,锁不可用时也会立即返回 unique_lock,shared_lock
4.std::adopt_lock 假定当前线程已经获得互斥对象的所有权,所以不再请求锁 lock_guard,unique_lock,shared_lock
{
std::unique_lock<std::mutex> lock1(mutex1, std::defer_lock);
std::unique_lock<std::mutex> lock2(mutex2, std::defer_lock);
std::lock(mtx1, mtx2);
do_sth();
}
{
std::unique_lock<std::mutex> lock1(mutex1, std::try_to_lock);
if(lock1.owns_lock()){
do_sth1();
} esle {
do_sth2();
}
}
std::unique_lock 与std::lock_guard都能实现自动加锁与解锁功能,但是std::unique_lock提供了 lock(), unlock() 和 try_lock() 函数,要比std::lock_guard更灵活控制锁的范围,减小锁的粒度,但是更灵活的代价是占用空间相对更大一点且相对更慢一点。
五.条件变量
条件变量:一种同步原语(Synchronization Primitive)用于多线程之间的通信,它可以阻塞一个或同时阻塞多个线程直到,收到来至其他线程的通知;超时;发送虚假唤醒(Spurious Wakeup)。
C++11的条件变量有两个类
std::condition_variable:必须与std::unique_lock配合使用
std::condition_variable_any:更加通用的条件变量,可以与任意类型的锁配合使用,相比前者使用时会有额外的开销
两者在线程要等待条件变量前,都必须要获取相应的锁
二者相同的成员函数:
notify_one
notify_all
wait
wait_for >>超时设置为时间长度
wait_until >>超时设置为时间点
遗留说明:
condition_variable_any的额外开销是什么?虚假唤醒是啥?
#include <iostream> // std::cout
#include <thread> // std::thread
#include <mutex> // std::mutex, std::lock_guard, std::adopt_lock
#include <chrono>
#include <stdexcept>
#include <queue>
#include <string>
#include <ctime>
#include <sys/timeb.h>
std::mutex mtx; // mutex for critical section
std::queue<std::string> dataQueue;
std::condition_variable dataCond;
bool isStop = false;
std::string getSystemTime()
{
std::this_thread::sleep_for(std::chrono::milliseconds(10));
struct timeb t;
ftime(&t);
return std::to_string(1000 * t.time + t.millitm);
}
void DataProduceThread() {
while (true) {
std::string data = getSystemTime();
std::lock_guard<std::mutex> lock(mtx);
dataQueue.push(data);
dataCond.notify_one();//尝试注释该行,执行下,有助于理解条件变量有什么用
if (isStop) break;
}
}
void DataConsumeThread(int consumerId) {
while (true)
{
std::unique_lock<std::mutex> lock(mtx);
dataCond.wait(lock, [] {return !dataQueue.empty(); });
std::string data = dataQueue.front();
dataQueue.pop();
lock.unlock();//wait返回时mutex处于locked状态,为了提高并发应用效率,应立即显示解锁,后继续处理数据
//handle data....
std::cout << "[consumerId:" << consumerId << "] handle data: " << data << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
if (isStop) break;
}
}
int main()
{
std::thread pd(DataProduceThread);
std::thread consumers[10];
for (int i = 0; i < 10; i++) {
consumers[i] = std::thread(DataConsumeThread, i + 1);
}
std::this_thread::sleep_for(std::chrono::seconds(10));
isStop = true;
pd.join();
for (auto& t : consumers) t.join();
system("pause");
return 0;
}
结合这个例子,大家可以再试着将consumer减到3,在lock获取,cond通过后的地方加上打印,结合具体大家,个人得出下面心得:
1.虽然打印会有乱序,原因是lock.unlock后才进行的数据处理。导致cout输出流程,存在并发冲突调用的情况。如果将lock.unlock移到线程等待的sleep_for前面,就不会有这个问题了。但如果移到sleep_for后面程序会没法跑结束,为什么呢?
这个是因为,dataCond.wait(lock,[]{})。条件变量的wait是阻塞等待,当produce线程先停止后,经notify_one(),导致总会有线程没有被唤醒,出现阻塞卡死等待。
这时候可以这样验证下,将producer里的notify_one()改成notify_all()。恩,结果发现还不行?!!
那个这个时候就要在看下dataCond.wait(lock,[]{})这个了。可以看到wait有两个参数,后面那个是lambda表达式,啥式不是关键。关键是这个参数的作用,简单看下定义,它是条件判断。换句话说,condition被唤醒了还不算真被唤醒,他还可以通过这个参数进行判断,到底是否满足条件,如果不行它还是会进行阻塞,等待下一次唤醒。
所以如果要验证,可以这样再改下,把判断条件去了dataCond.wait(lock);然后就可以跑完,正常退出了。
最后这个问题到底应该怎么正确修复了,个人觉得可以将wait改为wait_for,超时则退出,进行下一次循环,不要一直死等。当然判断条件函数还是有意义的,可以防止虚假唤醒,提高整体的运作效率。有兴趣的同学,可以想办法构造验证下如果producer是notify_one,加入第一个被notify的线程A不满足条件没被唤醒,是否会有其他的线程B继续被notify然后判断是否满足条件。恩这个个人感觉是这样的,但毕竟没实际验证,可能有出入。dataCond.wait_for(lock, std::chrono::milliseconds(2), [] {return !dataQueue.empty(); });
2.通过打压分析,多线程之间,mtx是生效的,lock锁可以保障同时只有一个可以进入,算是实现了并发冲突的解决。
3.既然mtx+lock已经实现了并发冲突,condition的意义到底是什么?我如今的理解是阻塞,防止线程无意义的空转。条件变量,不满足条件就别xx嘛。mtx是互斥锁,本质上他的作用是解决并发冲突的;lock只是对mutex的封装,本质上解决lock和unlock分开写,过程异常导致未正常释放的问题;所以这么看mutex和lock都是没有阻塞等待的作用。so如果不想空转,又不想没轮等个xx时间再来一轮,就有了condition。