C++11多线程相关

简介: C++11多线程相关

C++11多线程新特性

创建线程相关注意事项

std::thread线程中,如果需要传递参数的引用类型,那么一定要用std::ref()来转换。

std::thread t5(&func5);
t5.detach();
cout << "pid: " << t5.get_id(); // 调用detach后,t5就不能再管理线程了,即这里就不能获取到正确的线程id了,但是调用joinable()会返回false
thread t6_1(func6);
thread t6_2(std::move(t6_1)); // t6_1线程失去所有权
t6_1.join();  // 因为t6_1以及失去了线程的所有权,所以这里再这样调用会抛出异常
t6_2.join();

std::mutex

为什么最好不要使用std::mutex?

容易忘记解锁

或者因为前面的return返回掉忘记解锁

异常可能会无法释放

std::mutex mtx;
lock_guard<std::mutex> lck(mtx);

lock_guard

如果只是单纯的加锁的话,lock_guard效率比unique_lock效率高但是unique_lock用法比较灵活一些,可以手动的解锁而不必等到析构时自动解锁。

使用lock_guard之后就不能再使用lock()和unlock()了

std::lock():

C++11中的函数模板,它能一次锁住两个或者两个以上的互斥量(至少两个,多了不限,1个不行);

如果互斥量中有一个没锁住,它就会释放锁住的互斥量,并且它就在那里等着,等所有互斥量都锁住,它才能往下走。要么多个互斥量都锁住,要么多个互斥量都没锁住;这样就能够防止死锁,不管锁的顺序如何

使用方法:std::lock(mtx1,mtx2…),后面需要调用mtx1.unlock();mtx2.unlock();

缺陷:还是得需要手工unlock(),因为一旦忘了unlock(),后果不堪设想。

设想:能不能lock_guard和std::lock()结合使用呢?

使用lock_guard的std::adopt_lock参数,std::adopt_lock是一个结构体对象,起一个标记作用:作用就是表示这个互斥量已经lock()过了,不需要在lock_guard的构造函数里对mutex对象进行再次lock()了,这样使用之后后面就不需要再unlock了,因为lock_guard的析构函数会照常unlock,代码演示如下所示:

std::lock(mtx1,mtx2);
// 使用std::adopt_lock参数之前互斥量一定要被锁住,否则会报异常
std::lock_guard<std::mutex> obj1(mtx1, std::adopt_lock);
std::lock_guard<std::mutex> obj1(mtx2, std::adopt_lock);

总结:工程中同时锁住两个互斥量情况不多见,因为每一个互斥量锁的是自己的资源,谨慎使用

当你需要开启另外一个线程,并在当前线程中获取另外一个线程返回的结果,就可以使用std::future和std::async方式

std::unique_lock

unique_lock是个类模板,工作中,一般情况下推荐使用lock_guard,因为它的效率更高一点;unique_lock比lock_guard灵活很多,可以手动lock()和unlock(),效率上差一点,内存占用多一点。

unique_lock的第二个参数

std::adopt_lock

unique_lock的第二个参数也可以使用std::adopt_lock这个参数,含义跟lock_guard相同。

使用前提 :使用std::adopt_lock参数之前互斥量一定要被锁住( 即要先lock ),否则会报异常

std::try_to_lock

含义:我们会尝试用mutex的lock()去锁定这个mutex,但如果锁定不成功,我也会立即返回,并不会阻塞在那里;

使用前提 :用这个try_to_lock的前提是你自己不能先去lock,否则一旦第一个锁成功了,这个也会锁上,相当于锁了两次,连续调用两次lock,会导致程序卡死。

std::unique_lock<std::mutex> obj(mtx1, std::try_to_lock);
if (obj.owns_lock())  // 表示拿到了锁
{
}
else  // 没有拿到锁
{
}
std::defer_lock

含义 :并没有给mutex加锁,即初始化了一个没有加锁的mutex

使用前提 :不能自己先lock,否则会报异常

std::unique_lock<std::mutex> obj(mtx1, std::defer_lock);  // 没有加锁的mtx1
obj.lock();   // 注意,我们不需要自己解锁,因为unique_lock有能力在析构的时候解锁
@todo 共享数据的保护
// 这后面的代码就不需要加上unlock()了,可加可不加,因为unique_lock在析构的时候会判断

unique的成员函数

lock()和try_lock()一般结合着std::defer_lock使用。

名称 含义
lock() 手工加锁
unlock() 手工解锁,注意互斥量必须被锁住才能解锁,存在的意义 **可能是有些时候你想暂时先处理非共享代码,从而把锁暂时先解开,后面想处理共享代码的时候再加上锁(因为你lock锁住的代码越少,效率越高) **
try_lock() 尝试加锁,加锁成功返回true,失败返回false,成功与否都会立即返回
release() 返回unique_lock所管理的mutex对象指针,并释放所有权;也就是说,unique_lock跟它所管理的mutex没有关系了,返回的是原始的mutex类型的指针
std::unique_lock<std::mutex> obj(mtx1, std::defer_lock);  // 没有加锁的mtx1
if (obj.try_lock()) // 加锁成功
{
}
else    // 加锁失败
{
}

说明

std::unique_lockstd::mutex obj(mtx1, std::defer_lock);或者std::unique_lockstd::mutex obj(mtx1);这两种写法相当于把unique_lock和mutex绑定在一起,而调用release()相当于把两者分开。

unlock()虽然是解锁,但并没有释放unique_lock与mutex之间的关系,还可以继续调用unique_lock的lock();而如果你调用了unique_lock的release(),在有些场景中,你就需要自己unlock()了,比如如下场景:

std::unique_lock<std::mutex> obj(mtx1);
std::mutex *mtx = obj.release();  // 返回原始的mutex类型的指针,其实返回的就是mtx1这个成员变量
// 共享数据代码
mtx->unlock();  // 这里需要自己解锁

unique_lock所有权的传递

std::unique_lock<std::mutex> obj(mtx1);

obj拥有mtx1的所有权,obj可以把自己对mtx1的所有权转移给其它的unique_lock对象,所以,unique_lock对这个mutex的所有权是属于 可以转移,但是不能复制,跟智能指针很像 。实现的方式可以有如下两种:

方式一:

std::unique_lock<std::mutex> obj(mtx1);
// std::unique_lock<std::mutex> obj2(obj);  // 这种复制所有权是非法的,这种方式调用的是拷贝构造函数
std::unique_lock<std::mutex> obj2(std::move(obj));  // 这种可以,调用的是移动构造函数,转移之后obj                            // 管理的对象就为空了

方式二:

std::unique_lock<std::mutex> get_unique_lock()
{
    std::unique_lock<std::mutex> tmp(mtx1);
    return tmp;
}
auto obj = get_unique_lock(); // 这种就会把临时对象构造到obj的预留空间中,这种也是转移的一种方式

std::call_once()

这是一个函数模板,C++11引入的函数,该函数的第二个参数是一个函数名;

功能 :能够保证函数a()只被调用一次,即便是多线程情况下也能保证函数a()只被调用一次,某些情况下具备互斥量这种能力,而且效率上,比互斥量消耗的资源更少。

使用 :需要与一个标记结合使用,这个标记std::once_flag;其实是一种结构,call_once()就是通过这个标记来决定对应的函数a()是否执行,调用call_once()成功后,call_once()就把这个标记设置为一种已调用的状态,后续再次调用call_once(),对应的函数a()就不会执行了。

std::once_flag g_flag;
class singlaton {
public:
  // 注意:只调用一次的东西我们要封装成一个函数
  static void createInstance()
  {
        m_instance = new singlaton();
  }
    static singlaton* instance()
    {
        std::call_once(g_flag, createInstance); // g_flag就像是一个锁一样,两个线程同时执行到这里,其中一个线程要等待另一个线程执行完createInstance返回,第二个线程才决定是否调用createInstance,如果第一个线程已经执行成功了,会将g_flag置位,第二个线程一看g_flag被置位了,就不会调用createInstance了。
        return m_instance;
    }
}

条件变量std::condition_variable、wait()、notify_one()、notify_all()

std::condition_variable

std::condition_variable本身就是一个类,wait()、notify_one()、notify_all()都是它的成员函数

wait()

wait()用来等待一个东西,

如果第二个参数lambda表达式返回值是true,那wait()直接返回;

如果第二个参数lambda表达式返回值是false,那么wait()将解锁互斥量,并阻塞到本行,一直阻塞到其它某个线程调用notify_one()或者notify_all()成员函数为止;

如果wait没有第二个参数:比如my_cond.wait(obj);,那么就跟第二个参数为lambda表达式并且返回值为false效果一样

其它线程调用notify_one()将原本wait(原来是阻塞)的状态唤醒后,wait就开始恢复干活了,恢复后干什么活?

a) wait将不断的尝试重新获取互斥量锁,如果获取不到,那么流程就卡在wait这里等着获取,如果获取到了(就等于加了锁),那么wait就继续执行b;

b)

b.1)如果wait有第二个参数(lambda或者其它可调用对象),就判断这个lambda表达式的返回值,如果lambda表达式返回值为false,那wait又对互斥量解锁,然后又休眠等待再次被notify_one唤醒;

b.2)如果表达式为true,则wait返回,流程走下来,此时互斥量是一个上锁的状态

b.3)如果wait没有第二个参数,则wait返回,流程走下来, 此时互斥量也是一个上锁的状态

std::condition_variable my_cond;

// 处理任务的线程
while (true) 
{
    std::unique_lock<std::mutex> obj(m_mtx);
    my_cond.wait(obj, [this] {
        if (!msgRecvList.empty())
            return true;
        return false
    });
    // 流程只要走到这里来,这个互斥量一定是锁着的,并且消息队列中一定有数据
    。。。
    // 因为unique_lock的灵活性,我们可以根据实际情况随时解锁
}

notify_one()

尝试把wait()的线程唤醒,但并不会一定有效果,就比如下面的情况,假如说push任务的线程notify_one()的时候,处理任务的线程走到了如下“//处理业务流程,执行时间很长”的代码中,因为处理任务的线程没有被wait(),所以此时唤醒的效果是无效的

// push任务的线程
for (int i = 0; i < 10000; i++)
{
    std::unique_lock<std::mutex> obj(m_mtx);
    msgRecvList.push_back(i);
    // 尝试把wait()的线程唤醒,但并不会一定有效果,就比如下面的情况,假如说push任务的线程
    // notify_one()的时候,处理任务的线程走到了如下“//处理业务流程,执行时间很长”的代码中
    // ,因为处理任务的线程没有被wait(),所以此时唤醒的效果是无效的
    my_cond.notify_one();
}
// 处理任务的线程
while (true) 
{
    std::unique_lock<std::mutex> obj(m_mtx);
    my_cond.wait(obj, [this] {
        if (!msgRecvList.empty())
            return true;
        return false
    });
    // 流程只要走到这里来,这个互斥量一定是锁着的,并且消息队列中一定有数据
    。。。
    // 因为unique_lock的灵活性,我们可以根据实际情况随时解锁
    obj.unlock();
    //处理业务流程,执行时间很长
}

说明:

仔细琢磨一下我们发现,程序可能并不会像我们想象的那样,push任务的线程把任务发送到消息队列中唤醒处理任务线程然后处理任务的线程从消息队列中取出一个任务执行,还可能出现处理任务的线程中积压了很多个任务,原因是wait()只是尝试获取锁,但是不一定能获取成功,因为push任务的线程是一个for循环,可能notify_one()之后,push任务的线程的std::unique_lock<std::mutex> obj(m_mtx);和wait()那里争抢锁并争抢成功的情况,这种情况下会出现消息队列 积压消息的情况。

notify_all()

用于同时唤醒多个线程。

async、future、packaged_task、promise

async、future创建后台任务并返回值

希望线程返回一个结果

std::async是个函数模板,用来启动一个异步任务,它返回一个std::future对象,std::future是一个类模板,可以调用std::future对象的get()函数获取异步任务的结果,

std::future:将来的意思,有人也称std::future提供了一种访问异步操作结果的机制,就是说这个结果你可能不能马上拿到,但是在不久的将来,在线程执行完毕的时候,你就能够拿到结果了。这个std::future类模板的对象里会保存一个值,在将来的某个时刻你可以拿到这个值。

#include <future>
int mythread()  // 线程入口函数
{
    cout << "mythread start" << "thread id= " << std::this_thread::get_id() << endl;
    std::chrono::miliseconds dura(5000); 
    // 休息5秒
    std::this_thread::sleep_for(dura);
    cout << "mythread end" << "thread id= " << std::this_thread::get_id() << endl;
    return 5;
}
int main()
{
    cout << "main" << "thread id= " << std::this_thread::get_id() <<endl;
    std::future<int> result = std::async(mythread);
    cout << "continue.....!" << endl; // async后,get()之前的代码是会继续执行的,不会卡住
    int def;
    def = 0;
    cout << result.get() << endl; // 阻塞获取,结果是5,程序会在get()这里卡着等待结果
}

总结:

上述程序通过std::future对象的get()成员函数等待线程执行结束并返回结果,这个get()函数很牛,不拿到将来的返回值就卡在这里等待拿值。但是get()只能调用一次,调用多次会报异常。

std::future还有一个**wait()成员函数,它只是等待线程返回,本身并不返回结果。**有点像线程的join()。

// 使用类成员函数的方式
class A
{
public:
  int mythread(int a)
  {
        return a+1;
  }
}
int main()
{
    A a;
    int tmp = 12;
    std::future<int> result = std::async(&A::mythread, &a, tmp);  // 注意a要使用&a的这种方式
    int ret = result.get();
}

std::packaged_task

是个类模板,它的模板参数是各种可调用对象;通过std::packaged_task来把各种可调用对象包装起来,方便将来作为线程入口函数来调用。

int mythread(int mypar) // 线程入口函数
{
    cout << "mythread start" << "thread id= " << std::this_thread::get_id() << endl;
    std::chrono::miliseconds dura(5000); 
    // 休息5秒
    std::this_thread::sleep_for(dura);
    cout << "mythread end" << "thread id= " << std::this_thread::get_id() << endl;
    return 5;
}
int main()
{
    std::packaged_task<int(int)> mypt(mythread);  // 把函数mythread通过packaged_task包装起来
    std::thread t1(std::ref(mypt), 1);  // 线程直接开始执行,第二个参数作为线程入口函数的参数
    t1.join();
    std::future<int> result = mypt.get_future();  // 这么一绑定,future里保存的就可以是mypt包装的                            // 任务的返回值
    cout << result.get() << endl;
}

说明

std::packaged_task包装完可调用对象之后也可以直接调用,所以说std::packaged_task本身也是个可调用对象。

std::packaged_task<int(int)> mypt(mythread);
mypt(105);      // 直接调用,相当于函数调用
std::future<int> result = mypt.get_future();  // 这么一绑定,future里保存的就可以是mypt包装的                            // 任务的返回值
cout << result.get() << endl;

还有一些其它的使用方法:

myTaskVec.push_back(std::move(mypt)); // 入容器,使用了移动语义,入进去之后mypt就为空
std::packaged_task<int(int)> mypt2;
auto iter = mytasks.begin();
mypt2 = std::move(*iter); // 移动语义,也就是说你把迭代器这一项中的内容折腾到mypt2中去了,即容器中这             // 一项内容为空了,但是容器中的这一项还在,需要移除
myTaskVec.erase(iter);  // 迭代器已经失效了,所以后续代码不可以再使用iter
mypt2(123);
std::future<int> result = mypt2.get_future(); // 这么一绑定,future里保存的就可以是mypt包装的                            // 任务的返回值 
cout << result.get() << endl;

std::promise

类模板,作用是我们能够在某个线程中给它赋值,然后我们可以在其它线程中,把这个值取出来用

void mythread(std::promise<int> &tmpp, int calc)// promise中模板类型为int,表明要返回给其它线程中用                          // 的是int类型的值
{
    // 做一些复杂的运算
    // 做其它运算,比如整整花费了5秒
    int result = calc;
    tmpp.set_value(result);
}
void mythread2(std::future<int> &tmpf)
{
   auto result = tmpf.get();
}
int main()
{
    std::promise<int> myprom; //声明一个std::promise对象myprom,保存的值类型为int;
    std::thread t1(mythread, std::ref(myprom), 180);  // 这里传的是真正的引用,能把其它线程中
                              // set_value的值带回来
    t1.join();
    // 获取结果值
    std::future<int> fu1 = myprom.get_future(); // promise和future绑定,用于获取promise设置的值
    // 也可以把fu1对象传到其它线程中去,其它线程去调用get()获取结果值,但需要注意,get()函数只能调用一次。
    std::thread t2(mythread2, std::ref(fu1));
    t2.join();
    // auto result = fu1.get();
}

总结:

通过promise保存一个值,在将来某个时刻我们通过把一个future对象绑定到这个promise上来得到这个绑定的值。

小结

到底怎么用,什么时候用?

我们学习这些东西的目的并不是要把他们都用在咱们自己的实际开发中。相反,如果我们能够用最少的东西写一个稳定、高效的多线程程序,更值得赞赏,我们为了成长,必须要阅读一些高手写的代码,,从而快速实现自己代码的积累,我们的技术就会有一个大幅度的提升。

std::future的其它成员函数

int mythread()  // 线程入口函数
{
    cout << "mythread start" << "thread id= " << std::this_thread::get_id() << endl;
    std::chrono::miliseconds dura(5000); 
    // 休息5秒
    std::this_thread::sleep_for(dura);
    cout << "mythread end" << "thread id= " << std::this_thread::get_id() << endl;
    return 5;
}
int main()
{
    cout << "main" << "thread id= " << std::this_thread::get_id() <<endl;
    std::future<int> result = std::async(mythread);
    cout << "continue.....!" << endl; // async后,get()之前的代码是会继续执行的,不会卡住
    // wait_for等待一段时间
    std::future_status status = result.wait_for(std::chrono::seconds(1)); // 等待1秒
    if  (status == std::future_status::timeout)// 超时
    {
        // 表示线程还没执行完
    }
    else if (status == std::future_status::ready)
    {
        // 表示线程成功返回
        cout << result.get() << endl;
    }
    else if (status == std::future_status::deferred)  // 延迟
    {
        // 如果async的第一个参数设置为std::launch::defer,则本条件成立
        // 如果async的第一个参数设置为std::launch::defer,则上面的wait_for函数就不会等待
        // 因为mythread()函数没有被执行,只有调用get的时候才会执行,并且会在get所在的
        // 线程执行
        cout << "线程被延迟执行" << endl;
        cout << result.get() << endl;
    }
}

std::shared_future

为什么future对象的get函数不能多次调用?

主要是因为get函数的设计是一个移动语义,也就是说你已调用get,相当于把结果移动到了上述代码的ret里面,一移动,result里面的东西就为空了,所以get多次会出问题。

这样就带来一个问题,如果有多个线程都想获取到另一个线程执行的结果,那该怎么办呢?

std::shared_future也是个类模板,它的get()函数的效果是复制数据,从而多个线程就可以获取到另一个线程执行的结果了。

int mythread(int mypar) // 线程入口函数
{
    cout << "mythread start" << "thread id= " << std::this_thread::get_id() << endl;
    std::chrono::miliseconds dura(5000); 
    // 休息5秒
    std::this_thread::sleep_for(dura);
    cout << "mythread end" << "thread id= " << std::this_thread::get_id() << endl;
    return 5;
}
int main()
{
    std::packaged_task<int(int)> mypt(mythread);  // 把函数mythread通过packaged_task包装起来
    std::thread t1(std::ref(mypt), 1);  // 线程直接开始执行,第二个参数作为线程入口函数的参数
    t1.join();
    std::future<int> result = mypt.get_future();  // 这么一绑定,future里保存的就可以是mypt包装的                            // 任务的返回值
    std::shared_future<int> result_s(std::move(result));  // 这里传右值,不然会报错
    //std::shared_future<int> result_s(result.share()); //这种写法也是可以的
}

std::async

异步创建一个任务,至于是否会创建一个线程,取决于你给的参数,这个参数类型为std::launch类型,总体可以分为如下几种类型:

std::launch::defered表明该函数会被延迟调用,直到future上调用get()或者wait()为止,此种情况是不会启动一个线程的。这种是同步运行

std::launch::async表示强制创建一个新线程并立即执行。这种是异步运行

std::launch::any = std::launch::defered | std::launch::async

说明:这个参数意味着async的行为可能是“创建新线程并立即执行”或者没有创建新线程并且延迟到调用    get() 才开始执行任务,两者居其一,系统会根据一定的因素去自行选择。这种可能是同步也可能是异步运 行

不带参数的情形即默认情况下和“std::launch::any = std::launch::defered | std::launch::async”这种效果是一样的

std::launch::sync的效果跟std::launch::defered效果一样

使用方式举例如下:

std::future<int> result = std::async(std::launch::defered, mythread);

std::async与std::thread区别

std::thread创建线程的方式,如果线程返回,你想拿到这个返回值不容易

std::async创建异步任务,可能创建也可能不创建线程。并且async方法很容易拿到线程入口函数的返回值

由于资源限制:

如果用std::thread创建的线程太多,则可能创建失败,系统报告异常,崩溃

如果用async,一般不会报异常不会崩溃,因为系统资源紧张导致无法创建新线程的时候,std::async这种不加额外参数的调用就不会创建新线程。而是后续谁调用了get()来请求结果,那么这个异步任务就运行在执行这条get()语句所在的线程上。

目录
相关文章
|
1月前
|
缓存 安全 C++
C++无锁队列:解锁多线程编程新境界
【10月更文挑战第27天】
55 7
|
1月前
|
消息中间件 存储 安全
|
2月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
58 1
|
2月前
|
存储 并行计算 安全
C++多线程应用
【10月更文挑战第29天】C++ 中的多线程应用广泛,常见场景包括并行计算、网络编程中的并发服务器和图形用户界面(GUI)应用。通过多线程可以显著提升计算速度和响应能力。示例代码展示了如何使用 `pthread` 库创建和管理线程。注意事项包括数据同步与互斥、线程间通信和线程安全的类设计,以确保程序的正确性和稳定性。
|
2月前
|
存储 前端开发 C++
C++ 多线程之带返回值的线程处理函数
这篇文章介绍了在C++中使用`async`函数、`packaged_task`和`promise`三种方法来创建带返回值的线程处理函数。
82 6
|
2月前
|
缓存 负载均衡 Java
c++写高性能的任务流线程池(万字详解!)
本文介绍了一种高性能的任务流线程池设计,涵盖多种优化机制。首先介绍了Work Steal机制,通过任务偷窃提高资源利用率。接着讨论了优先级任务,使不同优先级的任务得到合理调度。然后提出了缓存机制,通过环形缓存队列提升程序负载能力。Local Thread机制则通过预先创建线程减少创建和销毁线程的开销。Lock Free机制进一步减少了锁的竞争。容量动态调整机制根据任务负载动态调整线程数量。批量处理机制提高了任务处理效率。此外,还介绍了负载均衡、避免等待、预测优化、减少复制等策略。最后,任务组的设计便于管理和复用多任务。整体设计旨在提升线程池的性能和稳定性。
86 5
|
2月前
|
C++
C++ 多线程之线程管理函数
这篇文章介绍了C++中多线程编程的几个关键函数,包括获取线程ID的`get_id()`,延时函数`sleep_for()`,线程让步函数`yield()`,以及阻塞线程直到指定时间的`sleep_until()`。
42 0
|
2月前
|
资源调度 Linux 调度
Linux C/C++之线程基础
这篇文章详细介绍了Linux下C/C++线程的基本概念、创建和管理线程的方法,以及线程同步的各种机制,并通过实例代码展示了线程同步技术的应用。
34 0
Linux C/C++之线程基础
|
4月前
|
Java 调度
基于C++11的线程池
基于C++11的线程池
|
4月前
|
Dart 编译器 API
Dart ffi 使用问题之在C++线程中无法直接调用Dart函数的问题如何解决
Dart ffi 使用问题之在C++线程中无法直接调用Dart函数的问题如何解决