C++11提供的并发编程

编程入门 行业动态 更新时间:2024-10-27 17:13:12

C++11提供的并发编程

C++11提供的并发编程

C++11 提供的并发编程

在C++11之前,如果想要实现并发编程,就需要使用具体平台提供的并发编程相关的库,非常的不方便,而且还不支持跨平台。在C++11之后,终于在语言层面有了并发编程的支持。包括线程控制、互斥锁、条件变量、区域锁(lockguard)、原子操作、异步等。

线程控制

  • C++11封装了一套线程库,位于thread文件中。

  • 使用std::thread类创建线程,构造thread对象时传入一个可调用对象作为参数,如果有 参数,同时传入参数。构造完成后,新的线程被创建,同时执行该可调用对象。

  • std::thread默认构造函数构造的对象不关联任何线程,判断一个thread对象是否关联线程可以用joinable()接口。

  • joinable的对象析构前,要么用join()等待结束,要么用detach()接触与线程的关联。

  • thread类没有拷贝构造和拷贝赋值,可以move,没有两个thread对象会表示同一线程。

  • c++11创建线程、分离线程、回收线程、获取线程id

#include <thread>
#include <iostream>>
using namespace std;void ThreadFunc(int nThreadNum)
{cout << "INFO: Num is " << nThreadNum << endl;
}int main()
{thread TestThread(ThreadFunc, 1);thread TestThread_detach(ThreadFunc, 2);TestThead_detach.detach();cout << TestThread.get_id() << endl;TestThread.join();return 0;
}

互斥锁

  • 通过mutex可以方便的对临界区加锁,std::mutex定义在mutex头文件,用来避免多个线程同时访问。
  • 提供lock、trylock、unlock
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;void AddCount(mutex &mutexTest, int &nCount)
{for (int i = 0; i < 1000; ++i){mutexTest.lock();++nCount;mutexTest.unlock();}
}int main()
{thread arrThreads[5];mutex mutexTest;int nCount = 0;for (auto& threadIte : arrThreads){threadIte = thread(AddCount, ref(mutexTest), ref(nCount));}for (thread& threadIte : arrThreads){threadIte.join();}cout << nCount << endl;return 0;
}
  • 同时还提供了其他几种熟悉的互斥锁
    • std::mutex,最常用,普遍的互斥量(默认属性),
    • std::recursive_mutex ,允许同一线程使用recursive_mutext多次加锁,然后使用相同次数的解锁操作解锁。mutex多次加锁会造成死锁
    • std::timed_mutex,在mutex上增加了时间的属性。增加了两个成员函数try_lock_for(),try_lock_until(),分别接收一个时间范围,再给定的时间内如果互斥量被锁主了,线程阻塞,超过时间,返回false。
    • std::recursive_timed_mutex,增加递归和时间属性

lock_guard

  • 有作用域的mutex ,让 程序更稳定,防止死锁

  • 类模板std::lock_guard是mutex封装器,通过便利的RAII机制在其作用域内占有mutex。

  • 创建lock_guard对象时,它试图接收给定mutex的所有权。当程序流程离开创建lock_guard对象的作用域时,lock_guard对象被自动销毁并释放mutex,lock_guard类也是不可复制的。

  • 一般,需要加锁的代码段,我们用{}括起来形成一个作用域,括号的开端创建lock_guard对象,把mutex对象作为参数传入lock_guard的构造函数即可,比如上面的例子加锁的部分,我们可以改写如下

std::thread t([&](){for(int i=0;i<10000;i++)std::lock_guard<mutex> guard(mtx);++num;}
})

进入作用域,临时对象guard创建,获取mutex控制权(构造函数里调用了mutex的lock接口),离开作用域,临时对象guard销毁,释放了mutex(析构函数里调用了unlock接口)

条件变量

条件不满足,线程被阻塞。一个线程等待某一变量的成立而阻塞,另一线程使该变量变化而使上一线程成立,同时发送信号(notify)唤醒wait的线程。

  • 接口
    • notify_one通知一个等待的线程
    • notify_all通知所有等待的线程
    • wait 阻塞当前线程,直到条件变量被唤醒
    • wait_for 阻塞当前线程,直到条件变量被唤醒,或到指定时限时长后
    • wait_until 阻塞当前线程,直到条件变量被唤醒,或直到抵达指定时间点
    • native_handle 返回原生句柄
#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()
{// 等待直至 main() 发送数据std::unique_lock<std::mutex> lk(m);cv.wait(lk, []{return ready;});// 等待后,我们占有锁。std::cout << "Worker thread is processing data\n";data += " after processing";// 发送数据回 main()processed = true;std::cout << "Worker thread signals data processing completed\n";// 通知前完成手动解锁,以避免等待线程才被唤醒就阻塞(细节见 notify_one )lk.unlock();cv.notify_one();
}int main()
{std::thread worker(worker_thread);data = "Example data";// 发送数据到 worker 线程{std::lock_guard<std::mutex> lk(m);ready = true;std::cout << "main() signals data ready for processing\n";}cv.notify_one();// 等候 worker{std::unique_lock<std::mutex> lk(m);cv.wait(lk, []{return processed;});}std::cout << "Back in main(), data = " << data << '\n';worker.join();
}

原子操作

C++11起提供了atomic,可以使用它定义一个原子类型。

  • 为什么要定义一个原子类型?
    举个例子,int64_t类型,在32位机器上为非原子操作。更新时该类型的值时,需要进行两步操作(高32位、低32位)。如果多线程操作该类型的变量,且在操作时未加锁,可能会出现读脏数据的情况。

解决该问题的话,加锁,或者提供一种定义原子类型的方法。

#include<atomic>
#include<iostream>
int main(){std::atomic<int64_t> value;//定义一个原子对象int64_t x=10;value.store(x);//将x存入该原子对象中value++;//使用原子对象特化的自增int64_t y=value.load(std::memory_order_relaxed);//取出原子对象中的值std::cout<<y<<std::endl;}
  • 其他原子类型相关操作
特化成员函数说明
fetch_add原子地将参数加到存储于原子对象的值,并返回先前保有的值
fetch_sub原子地进行参数和原子对象的值的逐位与,并获得先前保有的值
fetch_or原子地进行参数和原子对象的值的逐位或,并获得先前保有的值
fetch_xor原子地进行参数和原子对象的值的逐位异或,并获得先前保有的值
operator++令原子值增加一
operator++(int)令原子值增加一
operator–令原子值减少一
operator–(int)令原子值减少一

异步

  • 异步操作的主要目的是让调用方法的主线程不需要同步等待调用函数,从而可以让主线程继续执行它下面的代码。因此异步操作无须额外的线程负担,使用回调的方式进行处理。在设计良好的情况下,处理函数可以不必或者减少使用共享变量,减少了死锁的可能。当需要执行I/O操作时,使用异步操作比使用线程+同步 I/O操作更合适。
  • C++11中的异步操作主要有std::future、std::async、std::promise、std::packaged_task。
    • std::async最简洁,返回值就是future对象,任务可以在调用std::async结束后立即执行,也可以延后执行,使用future来控制
    • std::packaged_task把future对象的创建和任务的执行分割开来,提升了自由度,任务可以放到指定线程中去运行
    • std::promise灵活性最高,std::async和std::packaged_task都必须等到任务执行完毕后才能在future对象里拿到返回值,std::promise则可以在任务执行的任意时候都能拿到返回值,不必等到任务执行结束。

async

  • async 异步可以让耗时的操作不影响当前主进程的执行,而是单独的启动一个新的线程来运行任务.

  • std::async执行的任务可以运行在单独线程里,也可以不运行在单独线程里,如果是立即执行则会开一个线程去执行,如果是延后执行,则会在调用任务的线程里阻塞执行。

  • 立即执行,开一个线程去执行

  • 如果是立即执行,那么还需要注意future对象的析构问题。
    如果要析构std::async返回的future对象,那么future对象的析构函数会阻塞当前线程,直到任务执行结束

#include <future>
#include <iostream>bool myfunc(int data1, int data2)
{std::cout << "myfunc id: " << std::this_thread::get_id() << "\n";if (data1+data2 > 100){return true;}else{return false;}
}int main(void)
{std::cout << "main id: " << std::this_thread::get_id() << "\n";std::future<bool> fu = std::async(myfunc, 100, 200); // 100和200是myfunc需要的参数// 阻塞等待fu.get();return 0;
}
  • 延后执行,在调用get后在本线程中阻塞执行。
#include <future>
#include <iostream>bool myfunc(int data1, int data2)
{std::cout << "myfunc id: " << std::this_thread::get_id() << "\n";if (data1+data2 > 100){return true;}else{return false;}
}int main(void)
{std::cout << "main id: " << std::this_thread::get_id() << "\n";std::future<bool> fu = std::async(std::launch::deferred, myfunc, 100, 200); // 100和200是myfunc需要的参数// 等待1秒std::this_thread::sleep_for(std::chrono::seconds(1));// 阻塞等待fu.get();return 0;
}

package_task

  • std::packaged_task,相比于std::async,std::packaged_task自由度更高,而且也没有future对象析构阻塞的问题。分割了future对象的创建和任务的执行,执行任务可以显示地指定某个线程去做,不像std::async是背后做的
#include <future>
#include <iostream>bool myfunc(int data1, int data2)
{    // 等待1秒std::this_thread::sleep_for(std::chrono::seconds(1));if (data1+data2 > 100){return true;}else{return false;}
}int main(void)
{std::packaged_task<bool(int, int)> task(myfunc);std::future<bool> fu = task.get_future();// 必须要使用std::move把task转成右值std::thread t(std::move(task), 100, 200); // 100和200是myfunc需要的参数// 调用get进行阻塞等待std::cout << fu.get() << "\n";t.join();return 0;
}

promise

  • std::promise,相比于前2个,std::promise不需要等到任务结束就可以拿到future对象里的值,而且自由度最高,可以根据需要来决定返回值类型,不必是任务结束时的返回值。另外,std::promise只是个异步结果的提供者,不像前2者可以传递任务进来。

  • 在主线程中初始化一个可以传递string的promise,获取他的future,开启一个新线程,把promise传进去,这样就可以在这个线程函数中给promise放置信息,在主线程中,可以不用等这个线程函数结束就用future的get获取它放置的信息。另外可以将promise的类型设为void,只起到通知作用。

#include <future>
#include <iostream>
#include <string>bool myfunc(std::promise<std::string> pro, int data1, int data2)
{    // 等待1秒std::this_thread::sleep_for(std::chrono::seconds(1));pro.set_value("hello");// 等待2秒std::this_thread::sleep_for(std::chrono::seconds(2));if (data1+data2 > 100){return true;}else{return false;}
}int main(void)
{std::promise<std::string> pro;std::future<std::string> fu = pro.get_future();// 必须使用std::move把pro转成右值,之后pro就不能再被使用std::thread t(myfunc, std::move(pro), 100, 200);std::cout << fu.get() << "\n";t.join();return 0;
}
  • 利用主线程控制线程函数,可以把promise的future传给要执行的线程函数,在主线程中设置promise的值,在线程函数中的future get
#include <future>
#include <iostream>
#include <string>bool myfunc(std::future<void> fu, int data1, int data2)
{    fu.get();std::cout << "DDD\n";if (data1+data2 > 100){return true;}else{return false;}
}int main(void)
{std::promise<void> pro;std::future<void> fu = pro.get_future();std::thread t(myfunc, std::move(fu), 100, 200);// 等待1sstd::this_thread::sleep_for(std::chrono::seconds(1));// 进行通知pro.set_value();t.join();return 0;
}

参考文档

C++异步操作三种方式的区别
C++11中的异步操作
C++多线程编程

后记

日暮乡关走歧途,朝朝暮暮滂沱路
寒山远方望金台,伶仃之余谈风生

更多推荐

C++11提供的并发编程

本文发布于:2024-03-10 03:42:09,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1726915.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!