面试版同步异步编程
面试版同步异步编程
https://mp.weixin.qq.com/s/zy4WbvUP_ByZfSmMyDcU_Q
1,什么情况下会用到多线程?
并行化业务逻辑:经常需要频繁的发送,等待,接收其他业务线程的数据,信息交换是常见且高频的行为,这个时候就要开发高效的异步编程了。
2,什么是异步编程?同步编程又是什么?
将不同任务分配到不同线程执行,并且通过手动/自动方式,解决不同Task之间资源依赖关系。
通俗解释如下:
同步:程序发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。
异步:程序在发出调用之后,这个调用就直接返回了,所有没有返回结果。而是在调用发出后,被调用者通过状态,通知调用者,或通过回调函数处理这个调用。
3,线程同步和异步主要解决了什么问题?
同步:为了解决对共享数据的竞争访问问题,访问同步化,也就是按照既定的先后次序,一个访问需要阻塞等待前一个访问完成后才能开始。
异步:主要针对任务或线程的执行顺序,也即一个任务不需要阻塞等待上一个任务执行完成后再开始执行,程序的执行顺序与任务的排列顺序是不一致的。
4,行了,看你说了那么多,先写个简单的多线程例子吧,写完给我解释下什么是互斥锁,如何实现?
#include <vector>
#include <numeric>
#include <iostream>
#include <chrono>
#include <thread>
#include <mutex>
#include <string>
int main(int argc, char *argv[])
{
std::mutex g_display_mutex;
//定义一个lambda函数fun 输出线程id和一个传入的字符串 分别在新起的线程和当前线程中调用他
auto fun = [](std::string a, std::mutex & lock) {
//使用mutex的原因 两个线程都需要通过全局对象 std::cout来输出到屏幕 如果没有mutex 会打乱id和字符串的输出顺序
std::unique_lock<std::mutex> lock_guard(lock);
std::cout << "thread id = " << std::this_thread::get_id() << "\t " << a << "\n";
};
std::thread t(fun,"hello", std::ref(g_display_mutex));
fun("world", g_display_mutex);
t.join();
return 0;
}
互斥锁:在线程之间访问共享数据需要通过互斥锁来同步,保证同一时刻只要一个线程可以访问,或者只要一个线程进行写操作。
实现:手动使用lock/unlock来加减锁不是个好方法,容易漏写unlock,尤其在复杂的逻辑判断中。因此标准库提供了lock_guard,unique_lock,shared_lock等
lock_guard
https://blog.csdn.net/guotianqing/article/details/104002449
lock_guard
lock_guard是一个互斥量包装程序,它提供了一种方便的RAII(Resource acquisition is initialization )风格的机制来在作用域块的持续时间内拥有一个互斥量。
创建lock_guard对象时,它将尝试获取提供给它的互斥锁的所有权。当控制流离开lock_guard对象的作用域时,lock_guard析构并释放互斥量。
它的特点如下:
- 创建即加锁,作用域结束自动析构并解锁,无需手工解锁
- 不能中途解锁,必须等作用域结束才解锁
- 不能复制
示例
#include <thread>
#include <mutex>
#include <iostream>
int g_i = 0;
std::mutex g_i_mutex; // protects g_i
void safe_increment()
{
const std::lock_guard<std::mutex> lock(g_i_mutex);
++g_i;
std::cout << std::this_thread::get_id() << ": " << g_i << '\n';
// g_i_mutex is automatically released when lock
// goes out of scope
}
int main()
{
std::cout << "main: " << g_i << '\n';
std::thread t1(safe_increment);
std::thread t2(safe_increment);
t1.join();
t2.join();
std::cout << "main: " << g_i << '\n';
}
unique_lock
unique_lock是一个通用的互斥量锁定包装器,它允许延迟锁定,限时深度锁定,递归锁定,锁定所有权的转移以及与条件变量一起使用。
简单地讲,unique_lock 是 lock_guard 的升级加强版,它具有 lock_guard 的所有功能,同时又具有其他很多方法,使用起来更强灵活方便,能够应对更复杂的锁定需要。
特点如下:
- 创建时可以不锁定(通过指定第二个参数为std::defer_lock),而在需要时再锁定
- 可以随时加锁解锁
- 作用域规则同 lock_grard,析构时自动释放锁
- 不可复制,可移动
- 条件变量需要该类型的锁作为参数(此时必须使用unique_lock)
#include <mutex>
#include <thread>
#include <chrono>
struct Box {
explicit Box(int num) : num_things{num} {}
int num_things;
std::mutex m;
};
void transfer(Box &from, Box &to, int num)
{
// don't actually take the locks yet
std::unique_lock<std::mutex> lock1(from.m, std::defer_lock);
std::unique_lock<std::mutex> lock2(to.m, std::defer_lock);
// lock both unique_locks without deadlock
std::lock(lock1, lock2);
from.num_things -= num;
to.num_things += num;
// 'from.m' and 'to.m' mutexes unlocked in 'unique_lock' dtors
}
int main()
{
Box acc1(100);
Box acc2(50);
std::thread t1(transfer, std::ref(acc1), std::ref(acc2), 10);
std::thread t2(transfer, std::ref(acc2), std::ref(acc1), 5);
t1.join();
t2.join();
}
Difference between std::lock_guard and std::unique_lock
5,说说什么是条件变量,干嘛用的,并且给我写一段应用的代码吧。
条件变量condition_variable实现线程同步,他可以同时阻塞一个或多个线程,直到其他线程更改了共享变量,并通知了当前条件变量。
实现流程:需要捆绑mutex来使用
a,获取std::mutex,一般通过std::lock_guard或std::unique_lock
b,修改共享变量
c,执行条件变量的notify_one或者notify_all方法,等候其他线程通知
d,获取std::mutex,只能通过std::unique_lock,注意必须是同一个mutex,因为需要保护共享变量
e,执行wait函数
f,当条件变量被通知时,当前线程会被唤起,自动获取mutex。注意一旦发生虚假唤醒,线程将继续wait
看一下代码
#include <iostream>
#include <string>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex m;
std::condition_variable cv;
std::string data;
bool ready = false;
bool processed = false;
void worker_thread()
{
// 等待 直到主线程发送来数据
std::unique_lock<std::mutex> lk(m);//之所以换成了unique_lock 因为在notify_once之前需要手动unlock
cv.wait(lk, []{return ready;});//
// after the wait, we own the lock.
std::cout << "Worker thread is processing data\n";
data += " after processing";
//发生数据到主线程
processed = true;
std::cout << "Worker thread signals data processing completed\n";
lk.unlock();//如果notify的时候还不释放mutex 会导致被通知的线程马上被再次 block 为避免唤醒虚假线程
cv.notify_one();
}
int main()
{
std::thread worker(worker_thread);//主线程中首先启动worker子线程。
data = "Example data";
// 发送数据到 worker_thread线程
{
std::lock_guard<std::mutex> lk(m);//通过mutex保护共享变量ready
ready = true;
std::cout << "main() signals data ready for processing\n";
}
cv.notify_one();//主线程修改ready后 通过cv.notify_one发出通知
// wait for the worker
{
std::unique_lock<std::mutex> lk(m);
cv.wait(lk, []{return processed;});//然后通过cv.wait等待processed变量被置为ture
}
std::cout << "Back in main(), data = " << data << '\n';
worker.join();
}
6,能面到现在,说明你功底不错。那你就再说说有哪些方式可以实现跨线程取值?有没有更高效的方式?
C++11获取线程返回值的实现代码
共享变量,函数参数,更高效的那就 future 和 promise 吧
7,既然你提到了 future 和 promise,你能说说他们是如何工作的吗?并给我写个相关的代码?
有线程1和2,线程1希望从线程2中获取特定值,步骤如下:
线程1:创建promise对象,并从该对象中获得对应的future对象->
->线程1将promise对象传递给线程2,完成其他工作
->通过future::get()方法等待从线程2中取值,此时线程1被阻塞。完成 取值后继续剩下的工作。
线程2:接受传入的promise对象,通过promise的set_XXX等方法设置特定值,然后继续线程2自身的工作。
核心思想:promise与future是成对出现的。生产者通过promise赋值,消费者通过future取值。
#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>
#include <future>
#include <string>
#include <mutex>
//1
void accumulate(std::vector<int>::iterator first,
std::vector<int>::iterator last,
std::promise<int> &accumulate_promise)
{
int sum = std::accumulate(first, last, 0);
accumulate_promise.set_value(sum); // 生成者通过promise的set_value方法赋值 在set_value方法内获取promise内部存储的mutex并更新对象 通知 future
}
//2
void task_thread()
{
std::packaged_task<int(int,int)> task([](int a, int b) {
return std::pow(a, b);
}); //可以传入函数对象 函数指针 lambda函数等 这些函数并不是在 package_task构造时被执行 需要手动 invoke才能执行
std::future<int> result = task.get_future();//从中获得future 借此可以获取绑定函数的返回值
std::thread task_td(std::move(task), 2, 10);
task_td.join();
std::cout << "task_thread:\t" << result.get() << '\n';
}
int main(int argc, char *argv[])
{
try {
std::vector<int> numbers = { 1, 2, 3, 4, 5, 6 };
std::promise<int> accumulate_promise;
std::future<int> accumulate_future = accumulate_promise.get_future();//消费者通过future的get来阻塞线程获取对应的值 只能被调用一次
std::thread work_thread(accumulate, numbers.begin(), numbers.end(),
std::ref(accumulate_promise));
//accumulate_future.wait(); // wait for result
std::cout << "result=" << accumulate_future.get() << '\n';
work_thread.join(); // wait for thread completion
}
catch (std::exception &e)
{
std::cerr << e.what();
}
task_thread();
return 0;
}
看个小例子:
#include <iostream>
#include <functional>
#include <thread>
#include <future>
void print_int (std::future<int>& fut) {
int x = fut.get();
std::cout << "value: " << x << '\n';
}
int main ()
{
//提供一个不同线程之间的数据同步机制 他可以存储一个某种类型的值 并将其传递给对应的future 即使这个future不在同一个线程中也可以安全的访问到这个值
std::promise<int> prom; // create promise
std::future<int> fut = prom.get_future(); // engagement with future
std::thread th1(print_int, std::ref(fut)); // send future to new thread
prom.set_value (10); // fulfill promise
// (synchronizes with getting the future)
th1.join();
return 0;
}
继续看:
#include <iostream>
#include <future>
#include <chrono>
#include <thread>
// count down taking a second for each value:
int countdown(int from, int to) {
for (int i=from; i!=to; --i) {
std::cout << i << '\n';
std::this_thread::sleep_for(std::chrono::seconds(1));
}
std::cout << "Lift off!\n";
return from-to;
}
int main ()
{
//提供一个不同线程之间的数据同步机制 他可以存储一个函数操作 并将其返回值传递给对应的future 而这个future在另外一个线程中也可以安全的访问到这个值
std::packaged_task<int(int,int)> tsk(countdown); // set up packaged_task
std::future<int> ret = tsk.get_future(); // get future
std::thread th(std::move(tsk),10,0); // spawn thread to count down from 10 to 0
int value = ret.get(); // wait for the task to finish and get result
std::cout << "The countdown lasted for " << value << " seconds.\n";
th.join();
return 0;
}
再来一个:
// 使用promise传递被调用线程返回结果,通过共享状态变化通知调用线程已获得结果
#include <vector>
#include <thread>
#include <future>
#include <numeric>
#include <iostream>
#include <chrono>
void accumulate(std::vector<int>::iterator first,
std::vector<int>::iterator last,
std::promise<int> accumulate_promise)
{
int sum = std::accumulate(first, last, 0);
accumulate_promise.set_value(sum); // 将结果存入,并让共享状态变为就绪以提醒future
}
int main()
{
// 演示用 promise<int> 在线程间传递结果。
std::vector<int> numbers = { 1, 2, 3, 4, 5, 6 };
std::promise<int> accumulate_promise;
std::future<int> accumulate_future = accumulate_promise.get_future();
std::thread work_thread(accumulate, numbers.begin(), numbers.end(),
std::move(accumulate_promise));
accumulate_future.wait(); //等待结果
std::cout << "result=" << accumulate_future.get() << '\n';
work_thread.join(); //阻塞等待线程执行完成
getchar();
return 0;
}
改进版:
//使用async传递被调用线程返回结果
#include <vector>
#include <thread>
#include <future>
#include <numeric>
#include <iostream>
#include <chrono>
int accumulate(std::vector<int>::iterator first,
std::vector<int>::iterator last)
{
int sum = std::accumulate(first, last, 0);
return sum;
}
int main()
{
// 演示用 async 在线程间传递结果。
std::vector<int> numbers = { 1, 2, 3, 4, 5, 6 };
auto accumulate_future = std::async(std::launch::async, accumulate, numbers.begin(), numbers.end()); //auto可以自动推断变量的类型
std::cout << "result=" << accumulate_future.get() << '\n';
//std::async能在很大程度上简少编程工作量,使我们不用关注线程创建内部细节,就能方便的获取异步执行状态和结果,还可以指定线程创建策略
getchar();
return 0;
}
8,你也提到了async,说说吧,为何他就简便了?
std::promise< T >与std::packaged_task< Func >在使用时既需要创建提供共享状态的对象(promise与packaged_task),又需要创建访问共享状态的对象(future与shared_future),std::async能在很大程度上简少编程工作量,使我们不用关注线程创建内部细节,就能方便的获取异步执行状态和结果,还可以指定线程创建策略。所以,我们可以使用std::async替代线程的创建,让它成为我们做异步操作的首选。
看一看代码:
#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>
#include <future>
#include <string>
#include <mutex>
std::mutex m;
struct X {
void foo(int i, const std::string& str) {
std::lock_guard<std::mutex> lk(m);
std::cout << str << ' ' << i << '\n';
}
void bar(const std::string& str) {
std::lock_guard<std::mutex> lk(m);
std::cout << str << '\n';
}
int operator()(int i) {
std::lock_guard<std::mutex> lk(m);
std::cout << i << '\n';
return i + 10;
}
};
template <typename RandomIt>
int parallel_sum(RandomIt beg, RandomIt end)
{
auto len = end - beg;
if (len < 1000)
return std::accumulate(beg, end, 0);
RandomIt mid = beg + len/2;
auto handle = std::async(std::launch::async,
parallel_sum<RandomIt>, mid, end);
int sum = parallel_sum(beg, mid);
return sum + handle.get();
}
int main()
{
std::vector<int> v(10000, 1);//这10000个元素全是1
std::cout << "The sum is " << parallel_sum(v.begin(), v.end()) << '\n';
X x;
// Calls (&x)->foo(42, "Hello") with default policy:
// may print "Hello 42" concurrently or defer execution
auto a1 = std::async(&X::foo, &x, 42, "Hello");//async是在新线程中异步执行 通过返回的future的get函数来取值
// Calls x.bar("world!") with deferred policy
// prints "world!" when a2.get() or a2.wait() is called
auto a2 = std::async(std::launch::deferred, &X::bar, x, "world!");//defered是在当前线程中同步执行 请注意 并不是立即执行 而是延后到get函数被调用的时候才执行
// Calls X()(43); with async policy
// prints "43" concurrently
auto a3 = std::async(std::launch::async, X(), 43);
a2.wait(); // prints "world!"
std::cout << a3.get() << '\n'; // prints "53"
}
async很好用呢?在写一个吧:
#include<iostream>
#include<future>
#include<cmath>
using namespace std;
bool is_prime(int x){
cout<<"i am come in."<<endl;
for(int i=2;i<x;i++){
if(x%i==0){
cout<<"\nfactor: "<<i<<endl;
return false;
}
//this_thread::sleep_for(chrono::milliseconds(1));
}
return true;
}
int main(){
//首先创建线程 is_prime(65656813) 任务创建后 立即返回一个std::future对象
future<bool> fut(async(launch::async,is_prime,96271313));
cout<<"please wait\n";
while(fut.wait_for(chrono::milliseconds(1))!=future_status::ready)
cout<<".";
//主线程通过他来获取结果 如果调用过程中 任务没完成 则主线程阻塞至任务完成
int ret=fut.get();
cout<<"\nfinal result: "<<ret<<endl;
system("pause");
return 0;
}
参考链接:
1,https://zhuanlan.zhihu.com/p/77999255
2,https://zhuanlan.zhihu.com/p/78612487
3,https://www.cnblogs.com/moodlxs/p/10111601.html
4,https://www.cnblogs.com/lx17746071609/p/11128255.html