面试版同步异步编程

面试版同步异步编程

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值