c++11多线程入门<学习记录>

最近学习了c++多线程相关知识,也算是对这方面内容的入门
视频链接c++11并发与多线程视频课程

看了大概两周,简单进行总结
参考文章C++11并发与多线程

PS:c++11提供了标准的可跨平台的线程库,本次多线程开发以此库为核心

一.并发,进程,线程理解

1.并发:两个或者更多的任务(独立的活动)同时发生(进行):一个程序同时执行多个独立的任务

2.进程:运行起来的可执行程序

3.线程:每个进程有唯一的主进程,可以通过写代码创建其他子线程
<一个新线程,代表一条新的代码执行路径>

二.子线程创建与结束

1.线程创建:thread类提供了创建线程的接口 #include <thread>
thread myThread(可调用对象); //创建myThread子线程,线程执行参数内代码(或者函数)
c++中的可调用对象可以是函数、函数指针、lambda表达式、bind创建的对象或者重载了函数调用运算符的类对象<称为线程函数>

2.线程结束
线程一旦创建,就会与主线程并发进行,此时如果主线程执行完毕,会强制退出程序,因此需要保证子线程在主线程之前执行完毕。库提供两种方式:
myThread.join(); //表示此时阻塞主线程,等待子线程执行完毕与主线程汇合,一起结束整个进程
myThread.detach(); //主线程不再与子线程汇合,不再等待子线程
//detach后,子线程和主线程失去关联,主调线程无法再取得该被调线程的控制权,驻留在后台,由C++运行时库接管
<由于detach使线程脱离主线程控制,若该线程使用了主线程或者其他线程中的内容,要注意内容是否有效合法,坑!>

三.线程ID和线程参数

1.线程ID:每一个线程都有自己独一无二的线程ID,可以用std::this_thread::get_id()来获取目前代码执行位置处于的线程ID

2.线程参数
thread myThread(print(), ...);
其中print为函数指针,后续参数就是传入print函数的参数,注意传参格式

四.多个线程创建,数据共享问题

1.多个线程创建
可以用STL容器来便于多个线程的创建,如std::verctor<thread>,不过要注意创建一个线程要给对应的join或者detach函数,让线程可控

2.数据共享
多线程程序要注意对共享数据的管理,由于线程执行顺序随机性,若对共享数据同时进行读和写,会导致读数据的一方得到脏数据,而写的一方修改混乱
<若线程都只读共享数据,可无需管理>

五.互斥量和死锁

1.互斥量 #include <mutex>
对共享数据的管理,最简便也是最佳方案就是用mutex锁进行管理
mutex类似一把锁,锁住共享数据部分,如果需要访问共享数据时,先要查看目前锁是否打开,若锁打开则进入访问数据,并且锁上防止其他线程进入,访问结束后解锁;若锁未打开则在外循环等待直到锁打开

std::mutex myMutex;

myMutex.lock();
//...共享数据
myMutex.unlock();

互斥量锁上了一定要记得打开,不然后续无法读取数据且线程会卡死
类似于指针,要释放内存。因此也有lock_guard类模板,和智能指针一样,创建时自动调用lock,作用域外自动调用unlock
std::lock_guard<std::mutex> myGuard(myMutex);

std::mutex myMutex;

{
  std::lock_guard<std::mutex> myGuard(myMutex); //lock
  //...共享数据
} //unlock

2.死锁
两个或两个以上的互斥量,由于在进程中锁的顺序不一样,导致两个或多个进程相互等待对方锁住的锁时,就产生了死锁。

<两个互斥量mutex1,mutex2。>
a.线程A执行时,这个线程先锁mutex1,并且锁成功了,然后去锁mutex2的时候,出现了上下文切换。
b.线程B执行,这个线程先锁mutex2,因为mutex2没有被锁,即mutex2可以被锁成功,然后线程B要去锁mutex1.
c.此时,死锁产生了,A锁着mutex1,需要锁mutex2,B锁着mutex2,需要锁mutex1,两个线程没办法继续运行下去。。。

只要保证多个互斥量上锁的顺序一样就不会造成死锁。

六.unique_lock类模板

<很类似unique_ptr>
unique_lock可以取代lock_guard,理解为更加灵活的lock_guard,但效率相对较低

1.第二参数:
std::adopt_lock:表示这个互斥量已经被lock(),即不需要在构造函数中lock这个互斥量了

std::try_to_lock:尝试用mutex的lock()去锁定这个mutex,但如果没有锁定成功,会立即返回,不会阻塞在那里

std::defer_lock:如果没有第二个参数就对mutex进行加锁,加上defer_lock是始化了一个没有加锁的mutex

2.常用成员函数:
lock():加锁

unlock():解锁

try_lock():尝试给互斥量加锁
如果拿不到锁,返回false,否则返回true。

release():解除与锁的绑定,返回它所管理的mutex对象的指针,并释放所有权

七.条件变量<condition_variable>

1.condition_variable:为一个类,为互斥量解锁设定条件

std::mutex mymutex1;
std::unique_lock<std::mutex> sbguard1(mymutex1);
std::condition_variable condition;
condition.wait(sbguard1, [this] {if (!msgRecvQueue.empty())
                                    return true;
                                return false;
                                });  //锁 + 解锁条件
 
condition.wait(sbguard1); //不建议这么写

2.notify_one、notify_all
wait阻塞时,如果接收到其他地方的notify指令,则会尝试解锁.
a)如果lambda表达式为true,则wait返回,流程可以继续执行(此时互斥量已被锁住)
b)如果表达式为false,那wait又对互斥量解锁,然后又休眠,等待再次被notify_one()唤醒
<无第二参数默认为true>
PS:由于多线程执行随机性,可能会出现虚假notify,notify的时候wait线程不处于wait

notify_one():通知一个线程的wait()
notify_all():通知所有线程的wait()

八.async、future、packaged_task、promise

1.async、future <async支持接收函数返回值,future对象为接收者>
std::async是一个函数模板,用来启动一个异步任务,启动起来一个异步任务之后,它返回一个std::future对象

std::future对象,为类模板。
“future”将来的意思,也有人称呼std::future提供了一种访问异步操作结果的机制,就是说这个结果你可能没办法马上拿到,但是在不久的将来,这个线程执行完毕的时候,你就能够拿到结果了,所以,大家这么理解:future中保存着一个值,这个值是在将来的某个时刻能够拿到

std::future对象的get()成员函数会等待线程执行结束并返回结果,拿不到结果它就会一直等待,感觉有点像join()。但是,它是可以获取结果的。
std::future对象的wait()成员函数,用于等待线程返回,本身并不返回结果,这个效果和std::thread 的join()更像。

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

int mythread(int mypar){...}

std::packaged_task<int(int)> mypt(mythread);
//用法一
std::thread t1(std::ref(mypt), 1);
t1.join();
std::future<int> result = mypt.get_future(); 
cout << result.get() << endl;
//用法二,直接调用
mypt(1);
std::future<int> result = mypt.get_future();
cout << result.get() << endl;

3.std::promise类模板
我们能够在某个线程中给它赋值,然后我们可以在其他线程中,把这个值取出来

九.future其他成员函数、shared_future、atomic

1.future其他成员函数
std::future_status status = result.wait_for(std::chrono::seconds(5s));
卡住当前流程,等待std::async()的异步任务运行一段时间,然后返回其状态std::future_status。
std::future_status是枚举类型,表示异步任务的执行状态。类型的取值有
std::future_status::timeout //时间耗尽,还未运行结束
std::future_status::ready //运行结束
std::future_status::deferred //async为deferred状态

2.std::shared_future:也是个类模板
std::future的 get() 成员函数是转移数据
std::shared_future 的 get()成员函数是复制数据

3.std::atomic原子操作<“不可分割的操作”>
原子操作,指的是执行该操作时,CPU不会强制切换时间片,必须等该操作完全执行完成,才会切换时间片,因此可以保护数据合法性。
和互斥量类似,但从效率上来说,原子操作要比互斥量的方式效率要高。
互斥量的加锁一般是针对一个代码段,而原子操作针对的一般都是一个变量。

#include <atomic>
std::atomic<int> g_count = 0; //封装了一个类型为int的 对象(值)

void mythread1() {
	for (int i = 0; i < 1000000; i++) {
		g_count++;
	}
}

nt main() {
	std::thread t1(mythread1);
	std::thread t2(mythread1);
	t1.join();
	t2.join();
	cout << "正常情况下结果应该是次,实际是" << g_count << endl; //2000000
}

十.std::atomic续谈、std::async深入谈

1.一般atomic原子操作,针对++,--,+=,-=,&=,|=,^=是支持的,其他操作不一定支持。

2.std::async深入理解
第二参数
std::launch::deferred【延迟调用】
std::launch::async【强制创建一个线程】
如果同时用 std::launch::async | std::launch::deferred这里这个 | 意味着async的行为可能是 std::launch::async 创建新线程立即执行, 也可能是 std::launch::deferred 没有创建新线程并且延迟到调用get()执行,由系统根据实际情况来决定采取哪种方案<若无第二参数,这种为默认值>

async不确定性问题的解决
不加额外参数的async调用时让系统自行决定,是否创建新线程。
std::future result = std::async(mythread);
问题焦点在于这个写法,任务到底有没有被推迟执行。
通过wait_for返回状态来判断:

std::future_status status = result.wait_for(std::chrono::seconds(6));
//std::future_status status = result.wait_for(6s);
	if (status == std::future_status::timeout) {
		//超时:表示线程还没有执行完
		cout << "超时了,线程还没有执行完" << endl;
	}
	else if (status == std::future_status::ready) {
		//表示线程成功放回
		cout << "线程执行成功,返回" << endl;
		cout << result.get() << endl;
	}
	else if (status == std::future_status::deferred) {
		cout << "线程延迟执行" << endl;
		cout << result.get() << endl;
	}

十一. windows临界区、其他各种mutex互斥量

1.windows临界区
Windows临界区,同一个线程是可以重复进入的,但是进入的次数与离开的次数必须相等。
C++互斥量则不允许同一个线程重复加锁。

#include <Windows.h>
CRITICAL_SECTION my_winsec;	//windows中的临界区,非常类似C++11中的mutex
InitializeCriticalSection(&my_winsec);	//用临界区之前要初始化

EnterCriticalSection(&my_winsec);	//进入临界区
// ....
LeaveCriticalSection(&my_winsec);	//离开临界区

2.其他各种mutex互斥量
递归独占互斥量 std::recursive_mutex:允许在同一个线程中同一个互斥量多次被 lock() ,(但是递归加锁的次数是有限制的,太多可能会报异常),效率要比mutex低。

带超时的互斥量 std::timed_mutex 和 std::recursive_timed_mutex:判断是否拿到锁,若未拿到可以继续执行其他代码

十二.线程池浅谈、线程数量谈

1.线程池
把一堆线程弄到一起,统一管理。这种统一管理调度,循环利用的方式,就叫做线程池。
实现方式:程序启动时,一次性创建好一定数量的线程。这种方式让人更放心,觉得程序代码更稳定。

2.数量谈
A、线程创建的数量极限的问题
一般来讲,2000个线程基本就是极限;再创建就会崩溃。

B、线程创建数量建议
a、采用某些计数开发程序提供的建议,遵照建议和指示来确保程序高效执行。
b、创建多线程完成业务;考虑可能被阻塞的线程数量,创建多余最大被阻塞线程数量的线程,如100个线程被阻塞再充值业务,开110个线程就是很合适的
c、线程创建数量尽量不要超过500个,尽量控制在200个之内;

END<信号量呢??>

posted @ 2023-03-14 21:46  gonghw403  阅读(50)  评论(1编辑  收藏  举报