每日面试经验2-20
一、std tread的构造函数问题。
#include <thread>
#include <iostream>
#include <stdexcept>
class ScropeThread
{
public:
ScropeThread(std::thread t) :m_pThead(std::move(t))
{
if (!m_pThead.joinable())
{
throw std::logic_error("no thread");
}
}
~ScropeThread()
{
m_pThead.join();
}
ScropeThread(const ScropeThread &) = delete;
ScropeThread& operator=(const ScropeThread &) = delete;
private:
std::thread m_pThead;
};
void fun(void){}
int main()
{
std::thread t1(fun);
ScropeThread(t1);
return 0;
}
这样的构造函数是错的,因为c++的thread只支持右值作为参数的构造函数。
正确的做法:
std::thread t1(fun);
ScropeThread(std::move(t1));
或者
ScropeThread(std::thread(fun));
二、绑定类函数的方法。一般会把类别定义成这样。否则要传入传入成员函数地址、 类对象地址、参数。
#include <thread>
#include <iostream>
#include <stdexcept>
#include <string>
class A{
public:
void print1(std::string s){
std::cout << s << std::endl;
}
std::thread call_print1(std::string s){
return std::thread(&A::print1, this, s);
}
};
int main()
{
A* p = new A();
std::thread t1 = p->call_print1("Hello!");
t1.join();
return 0;
}
三、互斥量的使用。
mutex,独占形互斥量:有两个操作成员函数lock(), unlock()。这两个函数是成对使用的。非对称,非有序使用是不合法的。
原则上,对临界资源的使用,任何时候只能有一个。
为了防止不unlock,所以有std::lock_guard()这种东西,lock_guard直接取代的是lock和unlock两个东西。原理是,构造函数里有lock,析构函数里有unlock(超出作用域)。代价:只有在析构的时候才会放开锁,不太灵活。
四、死锁是什么?产生的原因以及解决办法:
至少有两个互斥量才能产生。一个线程未拿到所有资源,就被调度算法打断,并且没有释放资源。另一个线程也需要这些资源,并且也占用量上一个线程需要的资源未释放。这就死锁了。
一般来说,需要把 加锁的顺序弄成一致,并且把放开锁的顺序反过来。
std::lock()函数模版,它不存在多个锁上锁的顺序导致的死锁问题。这个函数模版只会同时锁住互斥量,或者同时不锁住所有互斥量。可以结合std::lock&&lock_guard<>,多使用一个std::adopt_lock,使用std::adopt_lock可以在构造函数里不加锁,当然,前提是你已经lock了。
五、介绍一下lock_guard和unique_lock。
unique_lock和lock_guard的区别:灵活一点,但是效率低一些,内存占用多一些。
unique_lock第二个参数是也可以是 std::adopt_lock和std::try_to_lock,尝试用mutex的lock,这个参数前提是不能先lock。在没有拿到锁的时候也不会阻塞,而会立即返回并且去做别的事情。owns_lock()可以判断拿没拿到锁。
defer_lock也不能先lock,并且没有给mutex加锁。 意思就是返回了一个没有加锁的mutex。
unique_lock的成员函数:lock(), unlock()(之所以使用unlock就是想执行一些非共享代码),try_lock()会返回拿没拿到锁。这个与unique_to_lock参数有点相似。release,就是把mutex和unique_lock解除了 关系,并且返回一个mutex的指针。 如果现在处于lock的状态,你有责任接管过来并且负责解锁。
unique_lock锁住的代码段多,叫粒度粗,锁住的代码少,那就是粒度细。
unique_lock的所有权转换。unique_lock就是可以拥有一个mutex互斥量,还可以进行转移(但是不能复制)。直接用std::move。 还有一种做法是return,因为返回局部的变量值会生成临时的对象,并且调用移动构造函数。
五、介绍一下call_once():可以保证函数只会被调用一次。once_flag来决定函数是否被执行(这是call_once的第一个参数)。
五、介绍一下条件变量。
std::condition_varaible、wait(), notify_one()。条件变量的设计能让我们提升效率,是一个条件相关的类。与互斥量进行配合。wait使用之后,wait将会解锁互斥量,并且堵塞到本行,直到别的线程调用notidy_one()为止。如果在堵塞中被唤醒,还需要重新去拿锁。当然,可以使用一个lambda函数作为第二个参数。返回值是true,wait直接就解锁互斥量并且返回。notify_all就是唤醒一些都卡在wait那儿的线程。
六、介绍一下async、future。
async是一个函数模版,起来一个异步任务。返回的是一个std::future对象。并且用get来获得值,get只能调用一次。async有额外的参数,比如launnch类型。std::launnch::deferred,表示get的时候,才会调用async调用,并且没有创建新线程。async的默认值是std::launch::deferred|std::launch::async,也就是系统自己决定用不用一个新线程。好处是什么,如果系统资源紧张的时候,用async不会崩溃。
std::future_status, future还有一个wait_for函数,会返回:std::future_status。future_status有几个值:std::future_status::timeout, std::future_status::ready, std::future_status::deferred。
七、std::packaged_task,将函数包装起来,将来作为线程的入口。
八、std::promise, 是一个类模版。我们可以在某个线程中对他进行赋值,并且在其他线程中把这个值拿出来用。
九、关于std::ref, std::bind的时候,使用的是参数的拷贝,而不是参数的引用。所以当希望调用函数的参数是引用的时候,需要用std::ref。std::thread中,可调用对象希望入参味引用的时候,也要用std::ref进行绑定。
十、shared_future,也是个类模版,get函数是复制数据。
十一、介绍一下原子操作。
以c++的类模版std::atomic来举例,定义的方法是std::atomic
十二、介绍一下其他的互斥量?
第一,临界区。critical section,进入临界区就类似于加锁。如果同一个进程重复进入临界区,需要多次出临界区。recursive_mutex,解决了同一个互斥量多次被lock。如果用了,那需要做的事情是考虑有没有优化空间。timed_mutex:带超时功能的互斥量。timed_mutex带有 try_lock_for 和try_lock_until这两个参数。try_lock_for代表花费多少时间进行加锁,try_lock_until代表的是到什么时间为止。
十三、介绍一下线程池
一、对所有的任务创建一个线程不太现实,二、偶尔创建一个线程的这种做法会有不稳定性的问题。线程池:把一堆线程放在一起,统一管理。2000个基本就是极限,采用某些api开发程序,按照专业技术来,在业务上也有分析。