- 一个非常简单的hello world级别的例子。
#include <thread>
using namespace std;
int func()
{
cout<<"int func(int)"<<endl;
return 100;
}
int main()
{
std::thread t(func);
t.join();
return 0;
}
# g++ main.cc -std=c++11 -lpthread
- 带参数的方式启动线程
int func(int val, string str)
{
cout<<"int func(int) , str : "<<str<<endl;
sleep(100);
return 100;
}
int main()
{
string str = "hello";
std::thread t(func,100,str);
t.join(); //join 表示主线程在这个地方等待和回收子线程t1
}
int main()
{
string str = "hello";
std::thread t(func,100,str);
t.detach();
// 一旦选择了detach模式,就不会执行joinable
if(t.joinable())
{
t.join();
}
}
- 获得返回值
void func(int & res)
{
res = 100;
}
int main()
{
int res = 0;
thread t1(func,ref(res));
sleep(1); //主线程等待一下子线程的计算
cout<<res<<endl;
t1.join();
}
- 通过调用类型构造,将带有函数调用符类型的实例传入
class background
{
public:
void operator()() //这个是重载了() 的运算符,调用的时候是 b()
{
cout << "operator ()" << endl;
}
};
int main()
{
background b; //b()调用operator()
std::thread t1(b);
// std::thread t1((background())); // 或写成这样
t1.join();
return 0;
}
- 给线程参数传递引用的
ref()
// 其他
void func(string & msg)
{
cout << msg << endl;
}
string str = "hello world";
thread t1(func , str); //虽然传递的是引用,但是一个线程修改,另一个线程是看不见的
thread t1(func , ref(str)); //修改成这样就可以了
- 并发度
cout << thread::hardware_concurrency() << endl;
- 观察多线程的命令:
ps -T -p 进程pid
pidstat -t -p 进程pid -u 1
top -H -p pid
- gdb调试
1. 查看线程
info threads
2. 跳转到某个线程
thread 2/3/4
3.
set scheduler-locking off|on|step
估计是实际使用过多线程调试的人都可以发现,在使用step或者continue命令调试当前被调试线程的时候,其他线程也是同时执行的,怎么只让被调试程序执行呢?
通过这个命令就可以实现这个需求。
off 不锁定任何线程,也就是所有线程都执行,这是默认值。
on 只有当前被调试程序会执行。
step 在单步的时候,除了next过一个函数的情况(熟悉情况的人可能知道,这其实是一个设置断点然后continue的行为)以外,只有当前线程会执行。
-
使用mutex,作为资源互斥。
#include <thread> #include <string> #include <mutex> using namespace std; mutex mu; void share_print(string str, int id) { mu.lock(); // lock , 这样确保cout的这段代码不被打断 cout <<"str "<< str << " " << id << endl; mu.unlock(); } void thread_func1() { for (int i = 0; i < 10; i++) { share_print("thread t1", i); } } void thread_func2() { for (int i = 0; i < 10; i++) { share_print("thread t2", i); } } int main() { thread t1(thread_func1); thread t2(thread_func2); t1.join(); t2.join(); }
-
使用guard
这样写的风格更加符合软件工程的要求
void share_print(string str, int id) { lock_guard<mutex> guard(mu); cout <<"str "<< str << " " << id << endl; }
但是,可以顺着这个思路去优化。比如,我们希望cout的资源,永远是线程安全的,这个可以设计一个类,把cout封装起来。让它以后使用的时候,就不必要考虑是否加锁了。在这个类的内存设计加锁的代码。
-
使用类封装希望线程安全的资源
#include <string> #include <mutex> #include <fstream> using namespace std; // 使用类封装线程安全的资源 class LogFile { private: mutex mu; ofstream f; public: LogFile() { f.open("log.txt"); } void shared_print(string str, int id) { lock_guard<mutex> locker(mu); f << "thread " << id << " str " << str << endl; } }; void thread_func1(LogFile & logfile) { for (int i = 0; i < 10; i++) { logfile.shared_print("thread t1", i); } } void thread_func2(LogFile & logfile) { for (int i = 0; i < 10; i++) { logfile.shared_print("thread t2", i); } } int main() { LogFile logfile; thread t1(thread_func1,ref(logfile)); //这个ref很重要,表示传递的是引用 thread t2(thread_func2,ref(logfile)); t1.join(); t2.join(); return 0; }
死锁出现的情况就是操作系统课本上面讲的基本那些东西。
还有一个问题就是,如何发现死锁?
可以attach或gdb core文件,查看bt,函数调用堆栈。如果发现,阻塞在lock的地方,就基本上是可以发生程序死锁了。
如果不使用条件变量的话,只是用锁,会写出这样很低效率的代码。
deque<int> q;
mutex mu;
void thread_func1()
{
int count = 10;
while (count > 0)
{
mu.lock();
q.push_front(count); //互斥区
cout<<"t1 push a value from t1 "<<count<<endl;
mu.unlock();
sleep(1);
count--;
}
}
void thread_func2()
{
int data = 0;
while (data != 1)
{
mu.lock();
if(!q.empty())
{
data = q.back();
q.pop_back();
mu.unlock();
cout<<"t2 got a value from t2 "<<data<<endl;
}
else
{
cout<<"proc"<<endl;
mu.unlock();
}
}
}
int main()
{
thread t1(thread_func1);
thread t2(thread_func2);
t1.join();
t2.join();
return 0;
}
加入条件变量之后,可以避免很多的无效的循环
#include <condition_variable>
#include <deque>
using namespace std;
deque<int> q;
condition_variable cond;
mutex mu;
void thread_func1()
{
int count = 10;
while (count > 0)
{
mu.lock();
q.push_front(count); //互斥区
cout<<"t1 push a value from t1 "<<count<<endl;
mu.unlock();
cond.notify_one();
sleep(1);
count--;
}
}
void thread_func2()
{
int data = 0;
while (data != 1)
{
unique_lock<mutex> locker(mu);
cond.wait(locker); // cond.wait(locker,[]() {return !q.empty(); }); //防止被为唤醒,只有满足 !q.empty(); 的时候,才会被唤醒v
// if(!q.empty()) //这些是可以去掉的代码,因为此时q一定不是空的,因为wait是被生产者唤醒的
// {
data = q.back();
q.pop_back();
mu.unlock();
cout<<"t2 got a value from t2 "<<data<<endl;
// }
// else
// {
// cout<<"proc"<<endl;
// mu.unlock();
// }
}
}
int main()
{
thread t1(thread_func1);
thread t2(thread_func2);
t1.join();
t2.join();
return 0;
}
如果不使用future,如果获得子线程的返回值?
-
方式1:
可以将返回值存放在引用里面,但是但是另外一个问题是,并不知道什么时候,可以计算出结果。
void cal_fib(int n,int &result) { int arr[n]; arr[0] = 0; arr[1] = 1; for(int i=2;i<=n;i++) { arr[i]=arr[i-1]+arr[i-2]; usleep(40000); } int res = arr[n]; result = res; } int main() { int n = 30; int result = 0; thread t1(cal_fib,n,ref(result)); // 讲返回值保存在引用里面,但是另外一个问题是,并不知道什么时候,可以计算出结果 for(int i=0;i<5;i++) { cout<<result<<endl; sleep(1); } t1.join(); return 0; }
-
方式2:
使用condition避免无效的等待。
#include <condition_variable> using namespace std; condition_variable cond; mutex mu; void cal_fib(int n,int &result) { int arr[n]; arr[0] = 0; arr[1] = 1; for(int i=2;i<=n;i++) { arr[i]=arr[i-1]+arr[i-2]; usleep(40000); } mu.lock(); int res = arr[n]; result = res; mu.unlock(); cond.notify_one(); } int main() { int n = 30; int result = 0; thread t1(cal_fib,n,ref(result)); unique_lock<mutex> locker(mu); cond.wait(locker); // void wait(std::unqiue_lock<std::mutex> & lock); condition_variable::wait 定义 cout<<result<<endl; // for(int i=0;i<5;i++) // { // cout<<result<<endl; // sleep(1); // } t1.join(); return 0; }
-
future 终于登场了。future这里体现出了异步的感觉。
#include <future>
int cal_fib(int n)
{
int arr[n];
arr[0] = 0;
arr[1] = 1;
for(int i=2;i<=n;i++)
{
arr[i]=arr[i-1]+arr[i-2];
usleep(50000);
}
return arr[n];
}
int main()
{
int n = 20;
future<int> fu = async(cal_fib,n);
int result = fu.get(); //主线程会阻塞在这里,直到计算出结果
cout<<result<<endl;
return 0;
}
-
promise
上面提到了future,这是一个未来的值。如果获取的时候,通过 fu.get() 来计算出来。
promise 是保证我未来会给一个值,然后通过future现在拿到这个值。 举一个简单的例子:
int cal_fib(int n, promise<int> &p) { int arr[n]; arr[0] = 0; arr[1] = 1; for(int i=2;i<=n;i++) { arr[i]=arr[i-1]+arr[i-2]; usleep(50000); } p.set_value(arr[n]); // 设置一个值 return 0; } int main() { promise<int> p; // 保证未来会传递一个值 future<int> futureObj = p.get_future(); future<int> fu = async(cal_fib, 30, ref(p)); cout << "wait result ... " << endl; int result = futureObj.get(); cout<<result<<endl; return 0; }
-
packaged_task
-
信号量 wait_until
condition_variable::wait_until 判断信号量是否等到了某个时间
std::condition_variable cv; bool done; std::mutex m; bool wait_loop() { std::cout << "enter wait_loop ... " << std::endl; auto const timeout= std::chrono::steady_clock::now()+ std::chrono::milliseconds(5000); std::unique_lock<std::mutex> lk(m); while(!done) { std::cout << "enter while ... " << std::endl; if(cv.wait_until(lk,timeout)==std::cv_status::timeout) { std::cout << "break" << std::endl; break; } } return done; }
-
内存模型
c++11中引入了内存模型的相关操作,本质上是对两个问题的解决。
-
cpu cache 多个核心修改不可见的问题
-
reorder 编译器交换代码顺序的问题
这两个问题导致一些在单线程环境下视乎不可能出现的问题,成为了可能。
写的很好的一篇文章 :
https://www.codedump.info/post/20191214-cxx11-memory-model-1/
https://www.codedump.info/post/20191214-cxx11-memory-model-2/#acquire-release
防止走丢,保存了一个pdf的快照:
-
-
C++并发编程(中文版)
https://legacy.gitbook.com/book/chenxiaowei/cpp_concurrency_in_action/details
-
cppreference
-
现代 C++ 教程:高速上手 C++ 11/14/17/20