thread t(foo, i);
void foo(int i)
{
....
}
C++有两种方式来等待线程结束
比如
t.detach();
t.join;
使用detach会有一些问题:
假如说以detach的方式,让线程在后台运行,那么线程的实例和线程本身就分离开了;有可能出现一种场景,
处理这种问题的方法可以有两种
关于线程传参detach和join的一些问题
线程传参时的形式类似 thread mytobj(fun,参数……)
由于detach和join在传参的时候,总是在主线程中先生成参数的一个拷贝,然后再将参数传到子线程中,这样即使子线程结束时或者使用detach主线程先结束的时候,由于两个线程使用的不是一个内存的变量(因为进行了复制),所以不会出现不可预料的问题,但是还有一个问题就是如果传进去的是指针,在detach中可能是不安全的,因为两者仍然指向的是同一块内存。
当传的是char *数组的时候,可以在主线程中转成string对象这样便不会出现问题,因为他是构建了一个临时对象,传进去的参数也是临时对象。如果线程函数接受的是一个对象,但是你传进去的是一个数字,他会在子线程中将数构造成一个对象,但是这个时候会出现问题,如果你使用的是detach,有可能传的局部变量数字在主线程中已经被释放了,就会出现很多问题,所以需要在主线程中提前生成对象,这个时候生成对象之后,编译器仍然会生成一份拷贝,传入子线程。但是有时候我们需要修改主线程中传入对象或者变量,需要它不总是复制传入子线程,这个时候可以使用std::ref(参数)函数来实现。
std::this_thread::get_id()可以获取线程id
如果你传入子线程中的值是智能指针对象,最好是使用join,不要使用detach,传的时候是用std::move(智能指针)传。
如果子线程的线程函数是一个成员函数
那么方式是thread mytobl(fun,对象,参数)
当同时执行多个线程的时候,由于操作系统调度的问题,他们每个都会分到不同的时间片,执行结果会比较混乱。
在c++中可以使用容器创建线程数组。
多个线程同时进行读写的时候尤其要考虑读写互斥和写写互斥,防止出错。可以通过加锁来避免这些情况。
mutex的用法
#include
mutex my_mutex;
my_mutex.lock()
my_mutex.unlock();
进行成对使用,lock()函数如果加锁失败会一直尝试进行加锁。
lock_guard类似智能指针可以自动解锁,在对象析构的时候。
用法lock_guard
可以通过大括号作用域来限定对象的作用域,使他在特定区域加锁,后面析构。
lock()函数可以一次为多个对象加锁(2个或者以上)
要么全加要么不加锁。
lock(mutex1,mutex2.....);
mutex1.unlock() unlock用法一样
集合lock_guard可以这样使用
在lock(mutex1,mutex2.....);后加lock_guard
adopt_lock是一个结构体对象,表示互斥量已经上锁,不需要再次加锁了。
这样就可以自动unlock()了
unique_lock类模板的使用方法
它比lock_guard灵活但是,效率稍低,内存占用多一些。
运用方法方式为unique_lock
后面也可以应用一些参数
如adopt_lock
这个参数与lock_guard的应用方式相同
try_to_lock,参数,尝试加锁,注意本线程在之前不能加锁。
owns_lock测试对象是否拥有一个锁,如果是返回true,不是返回fasle
defer_lock,初始化一个没加锁的mutex
unique_lock的成员函数
try_to_lock()尝试给互斥量加锁,拿到了返回true,没拿到返回false,不阻塞。
release()尝试给互斥量解锁并且返回指向mutex的对象指针。
这个unique_lock只能传递所有权,不能复制所有权。
用std::move()和一个函数构造一个零时对象并返回,这样就会转移他们之间的所有权
单例设计模式中,如果只想要某一个类声明一个对象,可以私有化构造函数,然后用一个成员函数来生成对象
然后再这个成员函数中声明一个静态对象,这个这个对象的类可以在类中声明,类中类,然后这个类中的析构函数中同时释放原来的那个对象。
在多线程中这种写法可能会出现对象被初始化多次的情况,因此需要加锁,并且为了效率的需要还可以使用一种双重锁定的方法,就是判断两次的方法提高效率。
还有一种写法,利用call_once函数,它可以保证某个函数只被执行一次,用法是call_once(once_flag变量,函数名),只要函数被执行过一次,once_flag就会被设置成某个状态,后续就不会再执行了。
condition_viarible是一个类,可以不用每次判断条件,加互斥量这种写法,可以让其条件满足的时候自己通知另外一个线程执行,使用con_vi.wait(mutex对象指针)第二个参数可以是一个lambda表达式,返回状态的true或者false,true的话直接返回,false就wait()解锁互斥量,并且堵塞到本行直到某个线程调用了notify_one函数。如果没有第二个参数,默认与lambda表达式返回false的效果相同。
当wait()函数被notify_one唤醒之后,他会不断尝试获取锁,获取到锁之后会对lambda表达式进行判断,为true返回,false就wait()解锁互斥量,并且堵塞到本行直到某个线程调用了notify_one函数。但是如果此时wait()是没有第二个参数的时候,那么wait会返回,程序继续执行。
如果当你notify_one唤醒的时候线程不是再wait()的时候,它可能没有效果。
notify_all与notify_one类似,只是它能一次唤醒所有的wait()的线程。
std::async的用法
async是一个类模板,相当于启动一个异步任务,自动创建一个线程并且开始执行线程的入口函数,返回值是一个fiutre对象,fiture是一个类模板,可以使用这个对象的get()函数来获得线程的返回值。fiture代表的是一种将来的状态,可以在将来的某个时刻得到线程返回的值。如果使用wait()函数就只是等待异步线程返回值,不会返回结果,与get()不同。get()对应一个异步线程只能执行一次。此时如果不使用get(),wait()主线程也会等待异步线程执行完毕。
如果在异步线程调用的时候,使用std::launch::deferred,表示线程调用被延迟到wait()或者get()的时候,如果没有wait()或者get()那么就不产生新线程,实际上即使调用,也不会产生新线程,而是在主线程中执行的线程的代码。
默认标记是std::launch::async|std::launch::deferred标记。
packaged_task是一个类模板,参数是各种可调用的对象,把可调用的对象包装起来。
std::packaged_tasktask(countdown); // 设置 packaged_task,返回类型为整形,参数是两个int的函数 std::future ret = task.get_future(); // 获得与 packaged_task 共享状态相关联的 future 对象
可以通过 std::packged_task::get_future 来获取与共享状态相关联的 std::future 对象。在调用该函数之后,两个对象共享相同的共享状态,具体解释如下:
std::packaged_task 的共享状态的生命周期一直持续到最后一个与之相关联的对象被释放或者销毁为止。
std::packaged_task可以直接调用,比如使用my_packet(123,123)这种。然后后面再使用,get_fiture(),get()等函数。
promise类模板,能够再某个线程中给他赋值,在另外一个线程中取出来用。
用法
void mythread(std::promise(int) &a ,参数)
{
a.set_value(想放的值);
}
获取值可以使用
fiture
ft.get();
fiture_state sta=ft.wait_for(时间);//wait_for的返回值
sta有下面几种情况:
timeout,表明超时,比如我希望等待你一秒你能够返回,但是你没有返回值。
ready,表示线程成功执行完毕,成功返回。
deferred,表明线程第一个参数是deferred,表明线程被延迟执行。
对应fiture只能使用一个get(),因为get()是一个移动语义,相当于把fiture对象中的值移动到结果中,当他是空的时候仍然调用这个方法的时候就会产生异常。
shared_fiture是一个类模板
它不是一种转移语义,他是一种复制数据。
如果有多个线程都可以通过get()来获取函数值。
通俗的左值的定义就是非临时对象,那些可以在多条语句中使用的对象。所有的变量都满足这个定义,在多条代码中都可以使用,都是左值。右值是指临时的对象,它们只在当前的语句中有效。
原子操作 std::atomic不会被打断,效率比互斥量更好,互斥量一般是多行代码,原子操作一般针对的是一个变量。
atomic是一个类模板,用于封装某个类型的值。
比如多个线程计数++,这种情况可以++来实现。
atomic
t对于++,+=-=,/=*=是支持的,如果是t=t+1,可能就不太对,因为它不是一个原子操作。
不能用一个拷贝构造函数声明原子对象,也不能使用赋值操作。
可以使用atomic
tt.store(12);
tt=12;
thread创建线程的时候,如果资源紧张,可能创建线程失败,会崩溃,而且想拿到线程返回值不容易。
async创建线程的时候它有时候并不创建线程。较容易拿到线程返回值,不会崩溃,因为不加额外参数的调用,资源紧张的时候,他不会产生新线程,会在原线程上执行。所以不加参数的时候不保证创建新线程
当使用async使用参数 deferred |async,它是在这两种状态中选择一个,有可能创建新线程,也有可能创建新线程。
创建线程不宜太多,创建太多会导致效率降低。
要知道异步任务没第一个参数的时候到底是怎么执行的时候需要使用fiture wait_for()来判断。
windows临界区
专用在windows中,linux不能使用,上面的则是跨平台的。
需要包含windows.h文件
def _WINDOWSJO_
ifdef __WINDOWSJO_
CRITICAL_SECTION test
endif
windows的临界区需要初始化
ifdef __WINDOWSJO_
InitialCriticalSection(&test);
endif
临界区的使用
ifdef __WINDOWSJO_
EnterCriticalSection(&test);和加锁类似
代码
LeaveCriticalSection(&test);与解锁类似
endif
同一个线程中多次进入相同的临界区必须多次离开临界区
而在c++11中同一个线程中不能lock多次
自动析构技术
自己实现一个类来实现自动的进入临界区,离开临界区。RAII类,资源获取即初始化类。
recusive_mutex递归的独占互斥量
自己拿到锁的时候别人拿不了,递归的意义是能够多次lock(),允许同一个互斥量被多次lock
效率比mutex底,递归次数也会有限制,次数太多可能会出现异常。
timed_mutex带超时功能的独占互斥量
try_lock_for()
普通的锁,拿不到就一直在那里尝试拿锁,这个会等待一段时间,超时的时候会程序继续运行。
拿到了就加锁
try_lock_until(chrono::stead_clock::now()+timeout)参数表示的是一个时间点,在未来的时间没到的时候,时间到了还没拿到锁,仍然继续执行。
recusive_timed_mutex带超时功能的递归独占互斥量,功能类似,允许同一个线程多次获取互斥量。
虚假唤醒,当程序在wait()的时候,数据为空的时候,notify_one是一个虚假唤醒,没有数据可以被正确执行。
线程池:把许多线程放在一起,统一管理循环利用。
实现方式,程序启动的时候一次性创建很多线程,稳定性更好,不会临时创建线程浪费资源。
线程创建数量:
根据专业的建议来,确保程序高效运行。
根据特定的任务来。