C++多线程

C++多线程,第1张

C++多线程

课程地址:c++11并发与多线程视频课程
第一节 并发基本概念及实现,进程、线程基本概念
(1)并发、进程和线程的基本概念和综述
(1.1) 并发:多个任务同时发生;一个程序同时执行多个任务。
(1.2)可执行程序:硬盘上存储的可运行的程序。
(1.3)进程:运行起来的可执行程序。
(1.4)线程:线程是 *** 作系统能够进行运算调度的最小单位,被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。每个进程运行起来后,有且只有一个主线程。
(1.5) 异步:发起请求后,不等待这个发起的请求返回任何响应就去先干别的事,当然最后是等待到这个返回呢还是不等呢?关键就是要看,是否真的返回,如果返回了,则接受,不返回,也不会一直等待,遇到main函数结束时, *** 作系统会结束并清理这个进程的所有资源和痕迹。
(1.6)同步:相对于异步就是必须等到发起请求后返回的响应后,该请求后才能做别的事,简单说就是单线程。
(2)并发的实现方法
(2.1)多进程并发:通过运行多个可执行程序来运行多个任务的方法。多线程之间可通过文件、共享内存、消息队列、管道、socket等机制进行进程之间的通信。
(2.2)多线程并发:在一个进程中通过开辟多个线程实现并发的方法。线程不是越多越好,每个线程都需要独立的栈空间,但堆共享,线程之间的切换需要保存很多的中间状态,切换会耗费时间。一个进程中的所有线程共享地址空间(共享内存),全局变量、指针、引用都可以在线程之间传递,使用多进程开销远远小于多进程。共享内存会带来数据一致性问题,需要在共享内存上进行一定的保护处理。
和进程相比,线程启动速度更快,更轻量级;系统资源开销更少,执行速度更快,比如共享内存这种同信方式快于任何其他通信方式。但实现起来有一定难度,要小心处理一致性问题。
(3)c++11新标准线程库
一些概念:互斥量,信号量,临界区。C++ 11 新标准,增加了对多线程的支持,可移植性(跨平台),大大减少开发人员的工作量。
第二节 线程启动、结束,创建线程多法,join detach
(1)线程运行的开始和结束
(1.1) 程序运行起来的时候,主线程(main函数)会自动运行,主线程结束,则代表整个程序运行结束,此时其他线程若没有运行结束,则会被 *** 作系统强行终止;
(1.2)std:thread用于创建一个线程,join函数阻塞主线程,让主线程等待子线程结束后再汇合,detach函数不会阻塞主线程,即主线程不需要等待该线程执行完毕而继续运行,前者使用更安全,也更普遍。detach和join不能连续被使用,否则会报异常。joinable用与判断线程是否可以执行join或者detach。

#include 
#include 
void test(int i){
std::cout <<"test thread got input:"<< i << std::endl;
}

int main(){
   int tmp = 10;
   std::thread testThread(test, tmp);
   testThread.join();
   //testThread.detach();
   std::cout <<"Main function finished"<< std::endl;
   return 0;
}

(2)创建线程的其他方法
(2.1)用类创建进程:须在类内重载public类型的不带参数()函数,thread传入实例化的类对象作为参数即可:

#include 
class Test{
public:
void operate()(){
   //task
}
};

int main(){
   Test test;
   std::thread testThread(test);//test对象被复制到testThread子线程了
   testThread.join();
   return 0;
}

(2.2) 用lambda表达式

#include 
int main(){
   auto testFunction = [] {
   //task
   }
   std::thread testThread(testFunction);
   testThread.join();
   return 0;
}

第三节线程传参详解,detach()大坑,成员函数做线程函数
(1)传递临时对象作为线程参数
(1.1)避免的坑
没有使用std::ref,即使线程函数参数为引用,实际也是值传递,如

#include 
#include 
void test(int& i){
std::cout <<"test thread got input:"<< i << std::endl;
}

int main(){
   int tmp = 10;
   std::thread testThread(test, tmp);//tmp值传递,线程里会对其进行一次拷贝
   testThread.join();
   //testThread.detach();
   std::cout <<"Main function finished"<< std::endl;
   return 0;
}

传数组的时候实际是值传递了一个指针,若该数组内存被释放掉,将产生不可预料的后果,在线程函数中使用string代替char[],在线程中会执行隐式转换和复制,可解决此问题。若在创建线程的时候将其显式转换成string,该转换过程将在线程创建的时候被转化。在创建线程的同时,构建临时对象传参是可行的。

#include 
#include 
#include 
using namespace std;
void test(int& i, string buffer){
std::cout <<"test thread got input:"<< i << std::endl;
}
int main(){
   int tmp = 10;
   char[] test_buff="test buff";
   std::thread testThread(test, tmp, test_buff);//线程创建后,test_buff会在test函数(testThread子线程中)中被隐式转换成string类
   //  std::thread testThread(test, tmp, (string)test_buff);//线程创建前test_buff会被显式转换成string类(在主线程中)
   testThread.join();
   //testThread.detach();
   std::cout <<"Main function finished"<< std::endl;
   return 0;
}

(1.2)线程id
std::this_thread::get_id()可获得所在线程的id号

(1.3)小总结:a) 传递int等简单类型,建议使用值传递,不要用引用,以免节外生枝;
b) 若传递类对象,避免隐式类型转换,全部在线程创建时或之前进行显式类型转换,然后在线程函数中使用引用&传参(但实质上也是值传递),减少一次复制 *** 作;
c) 尽量不要用detach,避免临时变量被销毁后留下的非法引用问题。

(2)传递类对象和智能指针作为线程参数

#include 
#include 
#include 
using namespace std;
class A{
mutable int m_i;
};

void test(const A& a){
a.m_i = 10;
}

int main(){
   A a;
   std::thread testThread(test, a);//直传递,线程的行为不会对a造成修改
   //std::thread testThread(test, std::ref(a));//引用传递,线程的行为将会修改a
   testThread.join();
   //testThread.detach();
   std::cout <<"Main function finished"<< std::endl;
   return 0;
}

独占式智能指针不能被复制,而创建线程传参时会进行一次隐式复制,所以不能直接传参独占式智能指针,需要在实参中使用std::move对独占式智能指针进行修饰。

(3)用成员函数指针做线程函数

#include 
#include 
#include 
using namespace std;
class A{
void thread_work(int param){
//task 1
}

void operator()(int param){
//task 2
}
};

int main(){
   A a;
   int num = 10;
   std::thread testThread(&A::thread_work, a, num);//a会在主线程被拷贝构造,子线程调用拷贝构造后的对象方法,最后拷贝构造对象在子线程里被析构。
   //std::thread testThread(&A::thread_work, std::ref(a), num);//a不会被拷贝构造,子线程直接调用对象a方法。
   testThread.join();
   //testThread.detach();
   std::thread testThreadOperator(a, num);//a会在主线程被拷贝构造,子线程调用拷贝构造后的对象方法,最后拷贝构造对象在子线程里被析构。调用重载的带参()函数作线程函数;
   //std::thread testThreadOperator(std::ref(a), num);//a不会被拷贝构造,子线程直接调用对象a方法。调用重载的带参()函数作线程函数。
   std::cout <<"Main function finished"<< std::endl;
   return 0;
}

第四节 创建多个线程、数据共享问题分析
(1)创建和等待多个线程
同时创建多个线程的时候,多个线程的执行顺序是乱的,跟 *** 作系统内部对线程的运行调度机制有关;
(2)数据共享问题分析
(2.1) 只读的数据: 只读的数据时安全稳定的,不需要做任何特别处理;
(2.2) 有读有写:如果不做任何处理,肯定会崩溃;最简单的处理:读的时候不能写,写的时候不能读,也不能有其他写。 处理方法:使用互斥量mutex和锁机制。

第五节 互斥量概念及用法、死锁
(1) 互斥量(mutex)的基本概念:互斥量是一个类对象,可理解成一把锁,多个线程尝试用lock()成员函数来加锁,只有一个线程能锁定成功(成功的标志是lock函数返回),如果没有锁成功,那么线程会卡在lock()这里,并不断尝试去加锁。互斥量多了会影响效率,少了则会达不到保护效果。
(2)互斥量的用法
(2.1) lock(), unlock()
先lock() *** 作共享数据,之后再进行unlock(),两者必须成对存在
(2.2) std::lock_guard类模板:类似智能指针原理,在析构的时候可以自动解锁
(3) 死锁
(3.1) 造成死锁的原因:两个锁在不同的线程里被以不同的顺序加锁
(3.2) 解决方法:保证加锁顺序一样;
(3.3)std::lock()函数模板:用于处理多个互斥量。一次锁定至少两个锁,不会产生因为加锁顺序而导致的死锁问题。如果互斥量中一个没被锁住,它就在那里等所有互斥量都锁住,然后往下走。要么两个互斥量都锁住,要么都没锁,如果只锁一个,另一个没成功,则它立即把已锁住的锁解锁。
(3.4) std::lock_guard的std::adopt_lock参数:起一个标志作用,表示互斥量已经被锁住(必须已经被锁住),不需要在std::lock_guard里对互斥量再进行加锁。
第六节 unique_lock详解
(1.1) unique_lock是一个模板类,具有自动解锁功能,一般推荐使用lock_guard。unique_lock的使用比lock_guard更灵活,效率上差一点,内存占用多一些。
(2) 第二个参数:
(2.1 ) std::unique_lock(my_mutex, std::adopt_lock),std::adopt_lock起一个标记作用,与std::lock_guard里的用法类似。
(2.2) std::try_to_lock:尝试给互斥量上锁,但如果没有上锁成功,会立即返回,而不会阻塞。使用try_to_lock的前提是之前不能先lock;
(2.3) std::defer_lock:使用前提是先前不能进行lock,否则会报异常。初始化了一个没有加锁的互斥量,方便灵活调用unique_lock;
(3) 成员函数
(3.1) lock()
(3.2) unlock()
(3.3) try_lock() 尝试加锁,如果拿不到锁,则返回true,否则返回false,不会阻塞
(3.4) release(),返回它所管理的mutex对象指针,并释放所有权,也就是说,这个unique_lock和mutex不再有关系
其他:lock的代码行数越少,代码的效率越高,执行越快。有人把锁住的代码量称为锁的粒度,用粗细来表示:
a,锁住的代码量越少,粒度越细,执行效率越高;
b,锁住的代码越多,粒度越粗,执行效率越低。
选择合适的粒度,在保护数据的同时提高效率。
(4) unique_lock所有权的传递:可以转移(std::move(std::unique_lock())或return std::unique_lock),但不能复制。

第七节 单例设计模式共享数据分析、解决,call_once

  1. 单例:整个项目中,只需创建一个的类。
    单例模式中的双重检查锁
class A{

static A* instance_;
std::mutex instance_mtx_;
A(){}

public:
    static A* getInstance(){
         if(instance_ == nullptr){
             std::unique_lock lock(instance_mtx_);
             if(instance_ == nullptr){
				instance_ = new A();
				}
         }
         return instance_;
    }
}
  1. std::call_once(): c++11引入的新函数,该函数的第二个参数是一个函数名a(),其功能是保证函数a()只被调用一次,具有互斥量的作用,但更高效。他需要与一个标记std::once_flag结合使用,通过该标记来决定对应的函数a()是否执行。调用call_once成功后,call_once就把该标记设置为一种已调用状态,此后对应的函数a()不会再被执行。

第八节 condition_variable、wait、notify_one、notify_all
1)condition_variable、wait、notify_one: 线程A等待(wait)一个条件满足,线程B则在消息队列中仍消息,并发出信号(notify_one)。condition_variable实际上是一个和条件相关的类,就是等待一个条件达成,该类需要和互斥量来配合工作,用的时候需要生成该类的对象。

std::queue msgQueue;
std::condition_variable cond;
std::mutex msg_mtx;

void inMsgRecvQueue(Msg message){
    std::unique_lock lock(msg_mtx);
    msgQueue.push(message);
    cond.notify_one(); 尝试把wait线程唤醒
}

void outMsgRecvQueue(){
    while (true){
    std::unique_lock lock(msg_mtx);
    //如果第二个参数lambda表达式返回false,wait()将解锁互斥量,并阻塞在本行,直到其他线程调用notify_one()函数为止。否则wait()直接返回,程序继续往后走。当其他线程使用notify_one将本wait()唤醒,wait会不断尝试重新获取互斥量锁,如果获取不到,则继续阻塞以等待获取,获取到锁之后会继续执行:上锁、判断lambda表达式(有第二个lambda表达式,返回false,则解锁并继续阻塞等待,否则执行后续代码)。第二个参数可以是lambda表达式,也可以是任何可调用的对象。
    cond.wait(lock, [this]{
    return !msgQueue.empty();
	})
    msgQueue.pop();
    }
}

实测发现,即使有其他线程进行了notify_one(),如果wait没有第二个参数,或者第二个函数参数返回false,该线程也会阻塞在wait处,此处与查到的资料不一致,有待进一步验证。

2)notify_all
notify_one只能唤醒一个wait,notify_all可以唤醒所有wait。

第九节 async、future、packaged_task、promise

  1. std::async、std::future创建后台任务并返回值
    std::async是一个函数模板,用于启动一个异步任务,返回一个std::future对象(也是一个类模板),该对象含有线程入口函数(线程)所返回的结果,用于可利用其成员函数get获取结果(线程运行结束后的返回值)。
    启动异步任务:自动创建一个线程,并开始执行对应的线程入口函数。

std::future对象里面含有线程入口函数的返回结果,可通过该对象的成员函数get()来获取。它提供了一种访问异步 *** 作结果的机制,在线程执行完毕返回的时候future对象可以获取到该返回值,同时线程会阻塞到get()函数处以等待返回值。

std::launch是一个枚举类型,用于控制std::async的执行:

  1. std::launch::deferred: 表示线程被延迟到wait()或者get()函数时才开始创建线程并执行,如果没有调用后者,则线程不会被创建。
  2. std::launch::async: 表示执行std::async时就创建了线程,并立即开始执行;

std::packaged_task 打包任务, 把任务包装起来。它是个类模板,他的模板参数是各种可调用对象,通过std::packaged_task来把各种可调用对象(函数、类对象、Lambda函以及packaged_task等)包装起来,方便作为线程的入口函数调用。

std::promise: 类模板,我们能够在某个线程给它赋值,而后在其他线程中取出该值使用,可实现不同线程之间的数据传递。

class A{
  public:
  int do(int n ){
  return n;
  }
A(){
}
};

int testThread(){
    sleep(5);return 10;
}

void testPromise(std::promise& a, int b){
    sleep(2)
    a.set_value(b * 10);
}

void testUsingPromise(std::future&  c){
    auto v = c.get();
    std::cout << c << std::endl;
}

int main(){
std::future result = std::async(testThread);
std::cout << result.get()<< std::endl;// 主函数在此处会等待testThrea执行结束,只能调用一次
//result。wait(); //等待线程结束但并不获取返回值

A a;
int n;
std::future result2 = std::async(std::launch::async,&A::do, &a, n);
std::cout << result2.get()<< std::endl;

std::packaged_task mypt(testThread);
std::thread t1(std::ref(mypt));
t1.join();
std::future result3 = mypt.get_future();
std::cout << result3.get() << std::endl;

std::promise myprom;
std::thread t4(testPromise, std::ref(myprom),160);
t4.join();

std::future f1 = myprom.get_future();
std::thread t5(testUsingPromise, std::ref(f1));
}

第十节 future其他成员函数、shared_future和atomic
1) future其他成员函数
std::future_status是一个表示线程运行状态的枚举类型。
std::future_status status=result.wait_for(std::chrono::seconds(1)) 返回线程执行1s后的状态。
std::future_status::timeout:超时,即指定等待时间后线程没有执行完;
std::future_status::deferred:线程还没开始执行;
std::future_status::ready:线程已经执行完毕并成功返回。

2)shared_future
解决的问题:future对象的成员函数get()内部执行的是move *** 作,所有只能被执行一次,某些情况下需要多次get其返回值,此时需要shared_future。它是一个类模板,其get()执行了复制数据。
std::shared_future result_share(result.share());//result将对象转移给了result_share。

3)原子 *** 作std::atomic
3.1) 原子 *** 作概念
原子 *** 作即是进行过程中不能被中断的 *** 作,针对某个值的原子 *** 作在被进行的过程中,CPU绝不会再去进行其他的针对该值的 *** 作。为了实现这样的严谨性,原子 *** 作仅会由一个独立的CPU指令代表和完成。原子 *** 作是无锁的,常常直接通过CPU指令直接实现。事实上,其它同步技术的实现常常依赖于原子 *** 作。
多线程中引入互斥量进行共享数据的保护。有两个线程只对一个变量进行 *** 作,比如一个线程读取,另一个线程写入。原子 *** 作可理解成一种无锁技术的多线程并发编程方式,不会被打断的程序执行片段,比互斥量更高效。互斥量针对一段代码进行保护,原子 *** 作只能对单个变量进行保护。
std::atomic是一个封装某个内置类型(不能是自定义类型)值的类模板,用于执行原子 *** 作。

std::atomic n=0;
void plus_thread(){
for(int i = 0; i < 1000000000)
    n++;
}

int main(){
std::thread t1(plus_thread);
std::thread t2(plus_thread);
t1.join();
t2.join();
std::cout << n << std::endl;
}

上述代码不使用互斥量保护和原子 *** 作,将可能得到预料之外的结果;原子 *** 作和互斥量都能使其得到预期结果,但前者效率更高。

第十一节 std::atomic续谈、std::async深入了解
1)std::atomic续谈

std::atomic n=0;
void plus_thread(){
for(int i = 0; i < 1000000000)
    n=n+1;
}

int main(){
std::thread t1(plus_thread);
std::thread t2(plus_thread);
t1.join();
t2.join();
std::cout << n << std::endl;
}

上述原子 *** 作后的结果仍然会产生错误结果,说明原子 *** 作支持的 *** 作类型有限,这些支持的 *** 作包括:++,–,+=,&=,|=,*=。

2)std::async深入了解
std::async用于创建一个异步线程。
(2.1)参数详述
a) std::launch::deferred:延迟调用,并且不创建新线程,延迟到get()或者wait()的时候才调用;
b) std::launch::async:强制异步任务在新线程上执行,意味着线程必须创建出新线程来;
c) std::launch::deferred|std::launch::async: 调用std::async的行为可能是上述两者情况中任一个;
d) 不带参数:与c)效果一致;
异步:创建新线程,同步则不会创建新线程。

(2.2) std::async与std::thread区别
a) std::thread(),如果系统资源紧张,可能无法创建线程,执行该命令创建线程可能会崩溃,std::thread不方便取回返回值;
b) std::async与std::thread最明显的不同,前者在资源紧张的时候不会创建新线程,而是后续在get()或者wait()所在的线程中执行入口函数。
c) std::async创建异步任务,可能创建也可能不创建线程,调用方法可以得到线程入口函数的返回值。
d)使用std::launch::async可以强制创建新线程。
e)经验:一个程序里,同时运行的线程数量不宜超过200。

2.3) std::async不确定性问题的解决
std::async不加参数时,系统会自行决定执行的具体行为,实际使用的时候不知道是异步创建了新线程还是被推迟调用,可以利用第十节介绍的std::future::wait_for(std::choron::seconds(0))函数来得到其执行情况。

第十二节 windows临界区、其他各种mutex互斥量
(1) windows临界区
临界区: 类似于c++11中的互斥量,用于保护共享数据,但可以多次进入。
适用范围:它只能同步一个进程中的线程,不能跨进程同步。一般用它来做单个进程内的代码快同步,效率比较高。
相关结构:CRITICAL_SECTION_critical
相关方法:
InitializeCriticalSection(& _critical) //初始化,最先调用的函数。
DeleteCriticalSection(& _critical) //释放资源,确定不使用_critical时调用,一般在程序退出的时候调用。如果以后还要用_critical,则要重新调用InitializeCriticalSection
EnterCriticalSection(& _critical)//把代码保护起来。调用此函数后,以后的资源其他线程不能访问。
LeaveCriticalSection(& _critical) //离开临界区,表示其他线程能够进来了。注意EnterCritical和LeaveCrticalSection必须是成对出现的!

(2) recursive_mutex 递归的独占互斥量,允许同一个线程中同一个互斥量被多次lock,成员函数也是lock()和unlock(),递归调用数量有限,递归太多可能会报异常,以下的代码是合法的。

std::recursive_mutex mtx;

mtx.lock()
mtx.lock()
~~~dosome thing~~~~
mtx.unlock();
mtx.unlock();

(3) 带超时的互斥量std::timed_mutex和std::recursive_timed_mutex
std::timed_mutex:带超时功能的独占互斥量。
try_lock_for():等待一段时间,如果成功拿到了锁,或者等待时间超时没拿到锁,就继续走下去;
try_lock_unit():参数是一个未来的时间点,在这个未来时间之前,一直尝试去获取锁,到点或者拿到锁都将继续走下去,行为与try_lock_for()相同。

 std::timed_mutex mtx;

std::chrono::milliseconds timeout(100);
if(mtx.try_lock_for(timeout)){
//拿到锁了
mtx.unlock();
}
else{
没有拿到锁
}

std::recursive_timed_mutex:带超时的独占递归锁

第十三节 虚假唤醒、atomic其他知识、线程池、线程数量
(1) 虚假唤醒:一个线程唤醒了之后,另一个线程抢在前面把条件改到不满足了,造成所谓的虚假唤醒,使用wait()的第二个参数可以避免虚假唤醒造成的潜在问题。

(2) atomic:非原子 *** 作会导致未知的错误结果,不允许拷贝构造和=赋值,但可以用load()成员函数进行拷贝初始化。

std::atomic a1 = 1;
std::atomic a2(a1.load());

(3) 线程池
程序稳定性问题:程序中耦合创建一个线程,这种行为在系统资源紧张的情况下可能导致异常错误,针对这个问题,提出了线程池,它是指将一堆线程弄在一起统一管理,循环利用,避开了在程序运行过程中偶尔创建线程的隐患问题。
实现方式:初始化的时候创建一定数量的线程,运行过程中只需选择运行,无需创建新线程和销毁线程,提高了效率、节省了资源,程序代码更稳定。

(4) 线程数量:依经验来看,2000是一个进程中的线程数量上限,实际开辟的线程数量应该与业务需求和cpu核心数量有关,尽量控制在200个以内。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5665934.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-16
下一篇 2022-12-16

发表评论

登录后才能评论

评论列表(0条)

保存