C++11 多线程编程 学习总结(下)

单例设计模式 

Class MyCAS //这是一个单例类
{
private:
	MyCAS() {} //私有化了构造函数
	
	Static MyCAS *m_instance; //静态成员变量
	
public:
	Static MyCAS *GetInstance()
	{
		If(m_instance == NULL)
		{
			m_instance = new MyCAS();
			Static Cgarhuishou cl;
		}
		return m_instance;
	}
	Class CGarhuishou //类中套类,用来释放对象
	{
	Public:
		~CGarhuishou ()
		{
			If(MyCAS::m_instance)
			{
				Delete MyCAS::m_instance;
				MyCAS::m_instance = NULL;
			}
		}
	}
	Void func()
	{
		Cout << "测试" << endl;
	}
}

单例设计模式共享数据问题分析、解决

面临的问题:在自己的线程中创建单例设计模式类,即不是主线程,而且这种线程不止一个,就可能面临需要getInStance成员函数互斥。

解决:在创建对象的函数中使用,再使用双重检查(双重锁定)的写法提高效率。

Class MyCAS //这是一个单例类
{
private:
	MyCAS() {} //私有化了构造函数
	
	static MyCAS *m_instance; //静态成员变量
    static std::mutex mtx;
	
public:
	static MyCAS *GetInstance()
	{
        //双重锁定。因为只是需要在第一次new的时候加锁,为了防止每次都要加锁,加上两个判断。效率提高。
        if(m_instance == NULL){
            std::unique_lock uniLock(mtx);
            if(m_instance == NULL)
            {
                m_instance = new MyCAS();
                static Cgarhuishou cl;
            }
        }
        return m_instance;
	}
	class CGarhuishou //类中套类,用来释放对象
	{
	public:
		~CGarhuishou ()
		{
			If(MyCAS::m_instance)
			{
				Delete MyCAS::m_instance;
				MyCAS::m_instance = NULL;
			}
		}
	}
	void func()
	{
		cout << "测试" << endl;
	}
}
//静态成员默认初始化
MyCAS *MyCAS::m_instance = nullptr;

call_once

  1. c++11引入的函数,该函数的第二个参数是一个函数名a();
  2. call_once功能是能够保证函数a()只被调用一次;
  3. call_once具备互斥量这种能力,而且效率上比互斥量消耗的资源更少;
  4. call_once()需要与一个标记结合使用,这个标记std::once_flag;其实once_flag是一个结构;
  5. call_once()就是通过这个标记来决定对应的函数a()是否执行,调用call_once()成功后,call_once()就把这个标记设置为一种已调用状态;
  6. 后续再次调用call_once(),只要once_flag被设置为了“已调用”状态,那么对应的函数a()就不会再被执行了;
  7. 比如两个线程执行到这,抢先的线程执行了 call_once(),另一个线程就必须等待,等到抢先的线程执行完毕,会通过标志位oneFlag反馈给别的线程,以告知他们不用再执行这个函数,通过这种机制,实现这个函数的 call_once();
  8. 建议:在主线程中先创建单例对象,再创建线程。
Class MyCAS //这是一个单例类
{
private:
	MyCAS() {} //私有化了构造函数
	
	static MyCAS *m_instance; //静态成员变量
    static std::once_flag oneFlag;

    static void createInstance(){ 
        m_instance = new MyCAS();
        static CGarhuishou cl;
    }
	
public:
	static MyCAS *GetInstance()
	{
        std::call_once(oneFlag, createInstance);//比如两个线程执行到这,抢先的线程执行了 call_once(),另一个线程就必须等待,等到抢先的线程执行完毕,会通过标志位oneFlag反馈给别的线程,以告知他们不用再执行这个函数,通过这种机制,实现这个函数的 call_once();
        return m_instance;
	}
	class CGarhuishou //类中套类,用来释放对象
	{
	public:
		~CGarhuishou ()
		{
			If(MyCAS::m_instance)
			{
				Delete MyCAS::m_instance;
				MyCAS::m_instance = NULL;
			}
		}
	}
	void func()
	{
		cout << "测试" << endl;
	}
}
//静态成员默认初始化
MyCAS *MyCAS::m_instance = nullptr;

条件变量

std::condition_variable 条件变量:就是一个类,等待一个条件达成,和互斥量配和使用

1.wait() 和 notify()配合

  • void wait<_Predicate>(std::unique_lock &__lock, _Predicate __p)
  • void wait(std::unique_lock &__lock)

wait(mutex, predicate); 第二个参数默认是fasle,即wait()会unlock mutex的对象,但是会堵塞在wait()这一行,直到别的线程notify()执行,wait()才会返回。

但是可以传入一个predicate,比如lambda表达式,返回的结果如果是false,效果同上。返回true,wait立即返回,不堵塞。

2.别的线程执行了notify_one()后,首先notify所在的线程肯定要先unlock互斥量。然后wait()会再次获得互斥量的锁,对互斥量进行lock。对互斥量lock()后:

(1)如果wait()的第二个predicate有参数,会再次判断predicate是否为true,

         + 如果是true,则wait()返回,不再堵塞,但是此时并没有对互斥量unlock,因为下面还需对数据进行处理。wait()的行为相当于等待别的线程产生的数据,这个线程接受、处理,所以wait返回后,依然处于lock状态,待处理完数据,可以等到编译器自己unlock,或者自己手动unlock。

         + 如果是false,则继续堵塞,等待被notify()唤醒。重复上述过程。

(2)如果wait()的第二个predicate没有参数,默认是true。效果和返回true一样。

3.注意

(1)notify_once唤醒了wait()后,不一定wait()会成功对互斥量lock(),因为可能notify()所在的线程,在notify对互斥量unlock()后会再次进行试图对它lock。因此不一定Wait就会成功对互斥量lock成功。

         因此,wait()因为多次没有lock而积累太多数据,处理不过来怎么办???具体应用中应该考虑的问题----可以限流

(2)如果某个线程执行力notify_once,但是另一个线程的程序当前没有堵塞在wait(),而是在别的地方执行,那么执行了notify_once的执行毫无意义。

4.notify_all():notifies all waiting threads

5.虚假唤醒:wait()中要有第二个参数(lambda)并且这个lambda要正确判断要处理的公共程序是否存在;

   wait(),notify_one(),notify_all()

线程返回一个结果

1.std::asyncstd::future:希望线程返回一个结果

(1)std::async:函数模板,启动一个异步任务,它返回一个std::future类模板对象。

(2)std::future:类模板,常用函数,T是返回结果的类型

         + get()       //等待,直到获取子线程返回值,解除堵塞。注意只能调用一次,因为get()是用移动语义实现的,使用future.get()后,再次使用future.get()将变成nullptr

         + wait()     //等待,直到子线程结束,不需要返回值,解除堵塞

启动一个异步任务,就是自动创建一个线程并且开始执行对应的线程入口函数,它返回一个std::future类模板对象。在这个对象里,含有线程入口函数的返回结果,即线程返回的结果,我们可以通过std::future对象的成员函数get()获取结果。

std::future,提供了一种访问异步操作结果的机制,就是说这个结果你可能没有办法马上拿到,在线程执行完毕的时候,你就能够拿到结果了。

int myThread(){
    std::cout<<"subThread id: "< retVal = std::async(myThread);//线程开始执行,虽然线程函数会延迟5s,但是不会卡在这儿
    for (size_t i = 0; i < 10; i++) 
        std::cout<<"test..."<

比如,在上面的这个程序里, std::future<int> retVal = std::async(myThread);启动一个线程后,虽然线程函数里有个5s的延时操作,主线程并不会等待这个子线程的完成才继续执行下去,只有当主线程里需要用到子线程的数据时,才会等到子线程的结束,比如retVal.get()会使得主线程必须等到子线程结束。因此这个futute,可以理解为让主线程在将来用到子线程返回值时才等到,否则不等。

这个功能就类似于 thread::join(),让主线程等到子线程的完成,但是这个异步性质,可以等价准备的控制等待的时间,即,在哪等待。

比如:

std::future retVal = std::async(myThread);
for (size_t i = 0; i < 1000; i++) 
    std::cout << "test_1..." << i << std::endl;
std::cout << "\nsubThread return value: " << retVal.get() << std::endl;          
for (size_t i = 0; i < 1000; i++) 
    std::cout << "test_2..." << i << std::endl;

第一个for循环,会和子线程争夺资源,但是第二个线程必须等子线程结束才能执行,如果使用join(),第一for循环结束,第二个就会执行。使用异步性质,可以控制程序的执行顺序。

注意:如果没有调用get()或者wait(),那么子线程会在main()return前将子线程执行结束。相当于编译器自己在主线程结束前帮你写了一个wait()。

2.std::async

第一个参数:std::launch类型,是一个枚举类型,来实现特殊的目的:

/// Launch code for futures
enum class launch
{
    async = 1,
    deferred = 2
};
std::future retVal = std::async(std::launch::deferred,&Async::myThread, &ayc, val);

(1)std::launch::deferred:

  • the task is executed on the calling thread the first time its result is requested (lazy evaluation)
  • 即,这个标志位下,子线程根本不会被创建,直到get()/wait()相应的入口函数才会被执行;没有get()/wait()入口函数就不会被执行。
  • 而且,在有get()/wait()时,线程的入口函数是在调用get()/wait()所在线程执行的

(2)std::launch::async

  • a new thread is launched to execute the task asynchronously
  • 即,显式的创建一个新的线程执行线程入口函数。

(3)std::launch::deferred | std::launch::asysnc

  • if both the std::launch::async and std::launch::deferred flags are set in policy, it is up to the implementation whether to perform asynchronous execution or lazy evaluation.
  • 系统的默认的标志位,即当第一个参数不传入的时候,和显示的设置std::launch::deferred | std::launch::async效果一样,即取决于自己的实现,执行哪个。
  • 此时可能创建新的线程执行异步任务(std::launch::async),也有可能在调用异步任务返回值的线程里直接调用这个异步任务函数而不创建新的线程(std::launch::deferred)。因此可以配合future_status一起使用,来确定这个异步任务执行情况。
int myThread(){
    std::chrono::milliseconds time(5000);  //等待时间为5s
	std::this_thread::sleep_for(time);
    std::cout<<"subThread id: "< ret = std::async(myThread);
	std::future_status stus = ret.wait_for(std::chrono::seconds(0)); //等待子线程的时间        
	if(stus == std::future_status::deferred){
	    //系统资源紧张了,它采用了std::launch::deferred策略
	    std::cout<<"main|异步任务延迟执行|返回值: "<

3.std::packaged_task

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

用法如下:

int myThread(int val){
    /***代码***/
}
int main(int argc, char const *argv[])
{
    std::cout << "current id: " << std::this_thread::get_id() << std::endl;
    std::packaged_task pkg(myThread);
    std::thread trd(std::ref(pkg), 10);
    trd.join();
		
    std::future ret = pkg.get_future();
    std::cout << ret.get() << std::endl;
    std::cout << "Main thread.\n";
    return 0;
}

std::packaged_task 对象将线程入口函数myThread进行封装,然后传入std::thread 对象,和普通线程一样,调用join(),当再次调用ret.get()就不会再等待,前面join()已经等待子线程执行结束了。ret.get()就可以直接获取值。如果不加join(),直接ret.get(),会报错。

4.std::promise:类模板

能够在某个线程中给它赋值,然后在其他线程中,把这个值取出来。std::promise prom,可以作为一个线程的参数,在这个线程里进行某些运算,运算结果,保存在std::promise对象之中,在另一个线程中,再将这个值取出来。当然,得确保这个std::promise对象是同一个对象,因此需要使用引用传递参数。

与取值有关的函数是std::future obj,通过prom.get_futrue();就可以取出furture对象,然后future.get()取出这个值。

//这个线程计算
void mythread(std::promise& prom, int val){
    val++;
    //假设这个线程花了2s, 得到了运算结果
    prom.set_value(val);
    return;
}
			
//这个线程使用上面那个线程的计算结果
void myTrd(std::future& future){
    int ret = future.get();
    std::cout<<"myTrd val: "< prom; //int 为保存的数据类型,通过这个prom实现两个线程的数据交互
			
    std::thread trd(mythread, std::ref(prom), 10);
    std::future future = prom.get_future();   // 获取结果值
    std::thread trd2(myTrd, std::ref(future));    // 传递给第二个线程
			
    std::cout<<"main thread.\n";
    trd.join();
    trd2.join();
    return 0;
}

5.总结

std::asysnc

std::packaged_task,

std::promise:更像是一个传递数据的变量,通过它在线程之间传递数据。

(1)他们都可以和std::future配合使用,通过std::future来取出线程中自己需要的值,方式有所不同。

  • std::async是函数模板,函数返回值就是std::future对象
  • td::packaged_taskstd::promise他们的对象有get_future方法,可以得到std::future对象,再获取相应的值。其中T是获取的值类型。他们都需要结合std::thread 进行使用,然后别忘记 join()。

(2)std::async

(a)async如果创建子线程,如果子线程还没结束,主线程任务已经执行结束,那么主线程会在结束前将子线程任务执行结束再返回。

(b)async更加准确的叫法是,创建一个异步任务,但并不一定创建一个子线程,

         + std::launch:deferred传入时,谁调用get就当前线程里创建异步任务,并没有创建新的子线程;

         + std::launch::async传入时,强制创建子线程执行异步任务。

(c)std::asysncstd::thread区别

         + thread创建线程,如果系统资源紧张,创建线程失败,那么整个程序就会报异常崩溃(有脾气),而async会强制创建一个新的线程

         + thread如果想获取线程的返回值,或者一些自己需要的中间值,不容易实现。但是async返回的是std::future对象,或者std::share_futute对象,可以方便的获取返回值。

(d)由于系统资源限制:

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

         + 如果用std::async,一般就不会报异常不会奔溃,因为,如果系统资源紧张导致无法创建新线程的时候,

            std::async这种不加额外参数的调用就不会创建新线程。而是后续谁调用了result.get()来请求结果,那么这个异步任务就运行在执行这条get()语句所在的线程上。

            如果强制用std::async一定要创建新线程,那么就必须使用std::launch::async。承受的代价就是系统资源紧张时,程序奔溃。

         经验:一个程序里,线程数量不宜超过100-200。

6.future_status

enum class future_status{
    ready,
    timeout,
    deferred
};

使用:

std::future<int> ret = std::async(std::launch::deferred,myThread, 10);

std::future_status stus = ret.wait_for(std::chrono::seconds(6)); //等待子线程的时间

  • timeout:表示子线程的执行时间,超过主线程等待子线程的时间设定值,就会触发timeout。但是即便如此,std::async函数模板创建的子线程依然会在主线程返回前执行完。
  • ready:表示线程成功返回。
  • deferred:在使用std::async函数模板创建子线程,并且第一个参数设置为std::launch::deferred时,这个deferred才会有效。同时,子线程也是等到std::futureobj.get()才会执行,并且是在主线程中执行。

7.std::shared_future

由于future.get()只能调用一次,所以要想实现不同线程之间通过future实现数据共享,那么怎么办?使std::shared_future<T> ,它也是个类模板

用法:

std::packaged_task pkg(myThread);
std::thread trd(std::ref(pkg), 10);
trd.join();
	
// std::future ret_1 = pkg.get_future();
		
// std::shared_future ret(pkg.get_future());
//std::shared_future ret = pkg.get_future();
std::shared_future ret(std::move(ret_1));

这样ret就可以反复的回去线程的返回值,顾名思义,share_future是将返回值通过复制的方获取,所以很安全,可以多次使用。

原子操作

std::atomic

情况:

有两个线程,一个线程对一个变量进行读取操作,另一个线程对一个变量进去写操作,如果任由两个线程自由进行操作,最终读取到的值,可能就不是当前值,也不是写线程操作之后的值,或许是一个不可预料的中间值。

比如两个线程同时对一个变量进行写操作,

int commVar = 0;
std::mutex mtx;
void Write(){
    for (size_t i = 0; i < 1000000; i++) {
        mtx.lock(); 
        commVar++;
        mtx.unlock();
    }
    return;
}
			
int main(int argc, char const *argv[]){
	std::thread t1(Write);
    std::thread t2(Write);
	t1.join();
	t2.join();
			
	std::cout<

需要通过互斥量mutex,来保持两个线程的有序进行,如果不加锁输出的值不可预料,不是理想的结果。现在提供一次新的技术,即原子操作技术,不需要加锁也能保证多个线程对同一块数据进行有序访问,要么访问到的是其余线程没有对这个数据进行操作前的值(即原来的值),或者是被别的线程改动后的值,而不是中间值。

原子操作:在程序执行中,不会被别的线程打断的程序片段。注意了,原子操作针对的是单个变量,而不是大段的代码段,大段的代码还是需要mutex实现。

原子操作状态:要么是完成的,要么是未完成的,不可能出现半中间状态。

std::atomic操作:其余不变

std::atomic commVar(10);
			
void Write(){
	for (size_t i = 0; i < 1000000; i++)  
        commVar++;
	return;
}

注意原子操作支持的变量操作:++、--,+=,-=,&=,之类;不支持 var = var+1 之类。

拷贝构造函数,拷贝构造运算符不能用

atomic atm2(atm.load())

auto atm2(atm.load())

Load()以原子方式读

Store()以原子方式写

window临界区

#include

1.临界区基本用法,类似mutex的lock()、unlock()

EnterCriticalSection(&winsSec);
msg.push_back(i);
LeaveCriticalSection(&winsSec);

2.在“同一个线程”(不同线程就会卡住等待)中,windows中的“相同临界区变量”代表的临界区的进入(EnterCriticalSection)可以被多次执行,但是你调用了几次EnterCriticalSection,你就得调用几次LeaveCriticalSection;

而在c++11中,不允许同一个线程中lock同一个互斥量多次,否则报异常

EnterCriticalSection(&winsSec); //ok
EnterCriticalSection(&winsSec);
msg.push_back(i);
LeaveCriticalSection(&winsSec);
LeaveCriticalSection(&winsSec);

3.自动析构技术:

windows临界区实现mutex的自动lock()和unlock()操作(std::lock_guard(std::mutex))。

RAII(resource aquisition is initialization)类,“资源获取即初始化”,容器,智能指针这种类,都属于RAII类

在构造函数里进行初始化,在析构函数里进行释放。

class uniLockWins{
private:
	CRITICAL_SECTION *_critical_sec;
public:
    uniLockWins(CRITICAL_SECTION *sec){
	    _critical_sec = sec;
	    EnterCriticalSection(_critical_sec);
	}
	~uniLockWins(){LeaveCriticalSection(_critical_sec);}
};

补充

1.recursive_mutex:允许同一个线程同一个互斥量多次lock()/unlock()

  • 效率更低
  • 应该考虑重构代码
  • 递归次数据说有限制,多次调用可能会异常

2.带超时的互斥量std::timed_mutex和std::recursive_timed_mutex

(1)std::timed_mutex:是带超时功能的独占互斥量

         Try_lock_for() : 参数是一段时间,是等待一段时间,如果我拿到锁,或者等待超过时间没拿到锁,就走下来;

         Try_lock_until() :   参数是一个未来的时间点,在这个未来的时间没到的时间内,如果拿到了锁,那么就走下来

(2)std::recursive_timed_mutex:是带超时功能的递归独占互斥量

3.浅谈线程池

(1)场景设想

        服务器程序,--》客户端,每来 一个客户端,就创建 一个线程为该客户提供服务。

a)网络游戏,2万玩家不可能给每个玩家创建个新线程,此程序写法在这种场景下不通;

b)程序稳定性问题:编写的代码中,偶尔创建一个线程这种代码,这种写法,就让人感到不安;

线程池:把一堆线程弄到一起,统一管理。这种统一管理调度,循环利用线程的方式,就叫线程池;

(2)实现方式

在程序启动时,我一次性创建好一定数量的线程。10,8,100-200,更让人放心,觉得程序代码更稳定;

4.线程创建数量谈

(1)线程开的数量极限问题,2000个线程基本就是极限,再创建线程就崩溃;

(2)线程创建数量建议

a)采用某些技术开发程序;api提供商建议 创建线程的数量 = cpu数量, cpu*2, cpu*2+2, 遵照专业建议和指示来,专业意见确保程序高效率执行;

b)创建多线程完成业务;一个线程等于一条执行通路;100要堵塞充值,我们这里开110个线程,那是很合适的;

c)1800个线程,建议,线程数量尽量不要超过500个,能控制在200个之内。

 

 

你可能感兴趣的:(C++,多线程,c++)