一个程序(单核CPU)通过操作系统的调度进行“任务切换”,从而同时执行多个独立的任务,即提高性能,可以同时干多个事,但切换需要时间的开销。
就是一个可执行程序运行起来了 windows(双击.exe文件)/linux(./文件名)。一个进程中有一个主线程,用来执行main函数中的代码。
顺序性:顺序执行代码。
可见性:当多个线程并发访问共享变量时,一个线程对共享变量的修改,其他线程能够立即看到。
原子性:一个操作(可能包含多个步骤)要么全部执行,要么全部不执行。
主线程是从main()函数开始执行的;子线程的执行也必须从一个函数开始;
当一个进程中,主线程执行结束后,如果子线程还没有结束会被操作系统强制终止。如果要保证子线程的运行状态,就必须保证主线程的运行状态,或者使用detach使子线程处于“分离状态”。
c++11标准线程库#include
,提高了移植性。其中,thread是一个标准库中的类,使用时要创建类对象。
// 使用时,只是引用了地址并未发生拷贝动作
std::ref() // 给线程函数传递参数时,可通过此函数避免发生拷贝动作
/* 构造函数 */
// 1.默认构造函数,构造一个线程对象且不执行任何任务:
thread() noexcept;
// 2.创建线程对象,并在线程中执行任务函数
template<class Function, class... Args>
explicit thread(Function&& fx, Args&&... args); // fx是任务函数,args是任务函数执行时需要的参数
// 任务函数可以是普通函数、类的静态成员函数、类的非静态成员函数、lambda表达式、仿函数
// 3.删除了拷贝构造函数,不允许线程对象之间的拷贝
thread(const thread&)=delete
// 4.移动构造函数,将线程other的资源所有权转移给新创建的线程对象
thread(thread&& other) noexcept
/* 赋值函数 */
// 左值的other禁止拷贝,故删除该赋值函数
thread& operator=(const thread& other)=delete
// 右值的other能赋值,会发生资源所有权的转移
thread& operator=(const thread&& other) noexcept
/* 阻塞主线程,并等待子线程执行完毕后回收它的资源,然后子线程与主线程汇合一起执行其他程序 */
join()
/* 主线程不用等待子线程结束;一旦detach后,该子线程会被c++运行时库接管,运行结束后,由运行时库负责清理该线程相关的资源 */
detach():
/* 判断子线程的分离状态并返回布尔类型,即是否可以成功使用join()、detach()(true则是可以使用) */
joinable():
/* c++11中,命名空间std::this_thread的全局函数 */
this_thread::get_id() // 得到线程的id
this_thread::sleep_for() // 使线程休眠一段时间
// 线程休眠1秒
Sleep(1000);
this_thread::sleep_for(chrono::seconds(1));
this_thread::sleep_until() // 让线程休眠到某个时间点,可实现线程定时任务
this_thread::yield() // 让线程主动让出自己已抢到的CPU时间片
/* thread类的其他成员函数 */
swap(std::thread& other) // 交换两个线程对象
static unsigned hardware_concurrency() noexcept // 返回硬件线程上下文的数量
其他创建线程的方法:
用类:类中必须要重载()运算符,将该类对象变成可调用对象,才能用来初始化线程对象。其中涉及到的过程:有参构造 --> 拷贝构造函数 --> 析构函数,最后对有参构造的类对象进行析构。
用lambda表达式,完成创建。
auto mylambdathread = []() {
cout << "子线程开始执行" << endl;
// .....
cout << "子线程执行结束" << endl;
};
简单的类型参数要用,值传递,不要用引用。
如果传递类对象,则要避免隐式类型转换(即创建子线程时,就生成临时对象并拷贝给入口函数)且该入口函数的形参中类对象要用常量引用。
// 该入口函数的形参中,类对象用常量引用
myPrint(const int i, const string &mystring) {...}
// 创建子线程时,就生成临时对象并拷贝给入口函数
thread mythreadObj(myPrint, i, string(mychar));
std::this_thread:;get_id()
得到,给子线程入口函数传递类对象时,直接进行类型转换,这会将产生的临时对象会拷贝给入口函数(整个过程都发生在主线程中)。只使用.join()
,就不会存在局部变量失效,导致线程对内存的非法引用的问题。
传递类对象时,如果希望在子线程中修改该类对象的成员,则要用std::ref()
函数引用类对象的地址且不会发生拷贝动作。
智能指针unique_ptr作为线程参数:
unique_ptr<int> i_uptr(new int(100));
thread mythreadObj(myPrint, std::move(i_uptr));
类成员函数指针:
thread mythreadObj(&A::func, a, 10);
thread mythreadObj(&A::func, &a, 10); // &a == std::ref(a);
// 用thread数组创建多个线程
thread mythreadObj[10];
for (int i = 0; i < sizeof(mythreadObj) / sizeof(mythreadObj[0]); i++)
{
mythreadObj[i] = thread(myPrint1, i);
}
for (int i = 0; i < sizeof(mythreadObj) / sizeof(mythreadObj[0]); i++)
{
mythreadObj[i].join();
}
// 用vector容器创建多个线程
vector<thread> threadVctor;
// 创建10个线程,线程入口函数统一使用myPrint()
for (int i = 0; i < 10; i++)
{
threadVctor.push_back(thread(myPrint1, i)); // 创建thread对象,并发生了拷贝到了容器中
}
for (vector<thread>::iterator iter = threadVctor.begin(); iter != threadVctor.end(); iter++)
{
(*iter).join(); // 等价于iter->join();
}
c++11-14-17_内存管理(RAII)_多线程:详细分析“c++多线程从原理到线程池实现”。
含有的成员函数:.lock()
、.unlock()
、.join()
、.joinable()
、.detach()
。
含有的成员函数:.try_lock_for(时间长度)
、.try_lock_until(时间点)
。
允许同一个线程多次获得互斥锁,可以解决同一线程多次加锁造成的死锁问题。
lock的代码段越少,执行的速度越快,整个程序的运行效率越高。
lock的力度,需要掌控。力度越大,执行效率越低;力度越小,共享数据的能保护力度越低。
线程持有锁的时间越长,程序运行效率越低。
.lock() // 加锁
/*
互斥锁有锁定、未锁定两种状态:
1)未锁定状态下,调用lock()的线程会得到互斥锁的所有权,并将其上锁;
2)锁定状态下,调用lock()的线程会“阻塞等待”,直到互斥锁变成未锁定的状态;
多个线程尝试用lock()成员函数进行加锁,且只有一个线程能加锁成功;没锁成功,则子线程会卡在这并不断尝试去锁这把锁头。
*/
.unlock() // 解锁:只有持有锁的线程才能解锁
/* std::mutex::lock()、std::mutex::unlock(),必须成对使用 */
.try_lock() // 尝试加锁
/*
1)如果互斥锁是为未加锁的状态,则加锁成功,函数返回true
2)如果互斥锁是锁定的状态,则加锁失败,函数立即返回false且“线程不会阻塞”
*/
产生的条件:至少要有两把锁头,也就是两个互斥量才能产生。
死锁的现象:当两个线程(都对两把锁进行锁和解锁的操作,锁的顺序相反)分别锁住了两个锁头,就会各自去寻找另一个锁头,从而产生了死锁。
本质原因:两个线程上锁的顺序不同。
死锁的解决方案:
保证两个互斥量在两个线程中的,锁的顺序相同。
std::lock()函数模板:
// 作用:一次能够锁住两个或者两个以上的互斥量。
// 不存在多个线程中,锁的顺序问题导致死锁的问题。
// 写法一:
std::lock(m_mutex1, m_mutex2);
m_mutex1.unlock(); m_mutex2.unlock();
// 写法二:
std::lock(m_mutex1, m_mutex2);
std::lock_guard<std::mutex> lguard1(m_mutex1, std::adopt_lock);
std::lock_guard<std::mutex> lguard2(m_mutex2, std::adopt_lock);
// 参数std::adopt_lock是结构体对象,起标记作用:作用就是表示该互斥量已经被lock(),不需要lock_guard再进行锁的操作,只需进行解锁
std::lock():
1)如果有一个互斥量没锁住,它就会在那里等,等待所有互斥量都被锁住之后,才能往下走。
2)要么两个互斥量都锁住了,要么都没锁住;如果只有一个锁住了,另一个没锁成功,则会解锁已经被锁住的锁。
建议一个线程中不要对两个互斥量同时上锁,最好一个一个锁;工作中,使用lock_guard
足够了。
unique_lock
和lock_guard
都是管理锁的辅助类:std::mutex m_mutex;
std::lock_guard<std::mutex> lockguard(m_mutex);
lock_guard()采用了RAII
的思想:
std::mutex::lock()
std::mutex::unlock()
缺点:lock_guard的解锁控制不灵活,但可以通过对lock_guard施加作用域来控制lock、unlock的生命周期。
std::lock_guard<std::mutex> lguard(m_mutex, std::adopt_lock);
// std::adopt_lock是结构体对象,起标记作用:表示该互斥量已经被lock(),不需要lock_guard再进行锁的操作,只需进行解锁
RAII
风格,在构造函数中加锁,在析构函数中解锁。unique_lock
的第二个参数:/* 1)std::adopt_lock是结构体对象,起标记作用:表示该互斥量已经被lock(),不需要unique_guard再进行锁的操作,只需进行解锁。*/
/* 2)std::try_to_lock(使用前不能锁):会尝试用mutex的lock()去锁定这个mutex;但如果没有锁成功,则会立即返回,并不会阻塞在那里。*/
std::unique_lock<std::mutex> lck(mtx, std::try_to_lock);
// 判断互斥量是否拿到了锁
bool succ = lck.owns_lock();
/* 3)std::defer_lock:初始化未加锁的互斥量;但需要自己解锁。*/
unique_lock<std::mutex> lck(mtx, std::defer_lock);
// 需要自己加锁;
for (int i = 0; i < n; i++)
{
lck.lock();
//操作共享数据
//........
lck.unlock();
//操作非共享数据
//........
lck.lock();
//操作共享数据
//........
}
lock() // 加锁
unlock() // 解锁
try_lock() // 尝试加锁,拿到锁返回true,否则返回false且不会发生阻塞
// release():
/* 返回它所管理的mutex对象的指针(原始的mutex指针),并释放所有权(即unique_lock和mutex不再有关系了)*/
unique_lock<std::mutex> lck(mtx);
mutex* pt_rel = lck.release();
// mtx所有权转让给pt_rel,因此需要pt_rel自己解锁
//pt_rel->lock(); // Error:接收lck.release()无需再次上锁,即已上锁
// .............操作共享数据
pt_rel->unlock();
通过std::move()进行所有权转移;
std::unique_lock<std::mutex> lck(mtx);
std::unique_lock<std::mutex> lck_move(std::move(lck)); // std::move()移动构造函数使用,转移所有权
通过函数返回局部的unique_lock对象(该局部对象,会生成临时对象并调用unique_lock的移动构造函数);
std::mutex mtx;
unique_lock<mutex> umtx_lockfunc()
{
unique_lock<mutex> temp_mtxLock(mtx); // temp_mtxLock拥有mtx的所有权,可将该互斥量mtx的所有权转移给其他的unique_lock对象(但不能复制)
return temp_mtxLock;
}
// 通过函数umtx_lockfunc(),返回局部的unique_lock对象,生成了临时对象和调用了移动构造函数,从而将所有权转移给lock_move
std::unique_lock<std::mutex> lck_move = umtx_lockfunc();
// .......操作共享数据
lck_move.unlock();
单类模式:确保多线程同时尝试创建一个的单类的实例时,只有一个能创建成功。提供一个访问它的全局访问点,该实例被所有程序模块共享。
单例实例在第一次被使用时才进行初始化,称为“延迟初始化”。
C++11前,多线程环境下local static对象的初始化并不是线程安全的。具体表现就是:
1)如果一个线程正在执行local static对象的初始化语句但还没有完成初始化;
2)此时若其它线程也执行到该语句,那么这个线程会认为自己是第一次执行该语句并进入该local static对象的构造函数中,这会造成这个local static对象的重复构造,进而产生内存泄露问题。
c++11引入std::call_once()的函数模板:保证函数只被调用一次;具备有互斥的能力,且效率高占用的互斥资源少。
#include
template<class callable, class...Args>
void call_once(std::once_flag& flag, Function&& fx, Args&&... args); // fx(args...):函数名和参数
// 当call_once调用成功后,once_flag对象会变成已调用的状态,后续就无法再次调用了;本质上once_flag是取值为0、1的锁。
#include
#include
#include
using namespace std;
/* C++11线程安全 */
class Singleton
{
public:
shared_ptr<Singleton> getInstance()
{
// 该call_once中的函数或可调用对象一旦执行过一次,initFlag的状态就会改变。
// 下次再调用时,会首先检查initFlag的状态,故不会再执行其中的函数或可调用对象。
std::call_once(initFlag, [&this]() {
singleton = shared_ptr<Singleton>(new Singleton());
});
return singleton;
}
private:
static shared_ptr<Singleton> singleton;
// std::once_flag对象是一个不可复制、不可移动的对象,但可被默认构造。
// 作用:跟踪call_once中,函数或可调用对象的是否已经被执行。
static std::once_flag initFlag;
Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
static shared_ptr<Singleton> singleton = nullptr;
static once_flag singletonFlag; // 调用默认构造函数
/* C++11线程安全 */
class Singleton
{
private:
static Singleton* instance;
private:
Singleton() {};
~Singleton() {};
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
public:
static Singleton* getInstance()
{
if(instance == nullptr)
{
instance = new Singleton();
}
return instance;
}
};
// init static member
Singleton* Singleton::instance = nullptr;
#include
using namespace std;
class Singleton
{
public:
// 获取单实例
static Singleton* GetInstance()
{
return singleton;
}
// 释放单实例,进程退出时调用
static void deleteInstance()
{
if (singleton != nullptr)
{
delete singleton;
singleton = nullptr;
}
}
private:
// 禁止外部调用默认构造和析构函数
Singleton() {}
~Singleton() {}
// 禁用拷贝构造和拷贝赋值函数
Singleton(const Singleton &single);
const Singleton &operator=(const Singleton &single);
private:
// 唯一单实例对象指针
static Singleton* singleton;
};
// 代码一运行就初始化创建实例且创建一次,本身就线程安全
Singleton* Singleton::singleton = new Singleton();
int main()
{
Singleton::GetInstance();
return 0;
}
为保护共享资源,“条件变量”需与“互斥锁”结合使用。。
注:生产者可以是单/多线程,而消费者一般都是多线程,俗称线程池。生产者产生的数据放入缓存队列,需要条件变量来通知消费者。
class A2
{
public:
A2() { cout << "默认构造函数" << endl; }
virtual ~A2() { cout << "虚析构函数" << endl; }
// 将收集到的数据放入队列
void InsertMessageQueue(int val)
{
for (int i = 0; i < 100000; i++)
{
std::unique_lock<std::mutex> umtx_lock (m_mutex);
MessageQueue.push_back(val);
m_conditionVar.notify_one(); // 尝试将另一个线程中的wait()唤醒
}
}
void OutMessageQueue()
{
while (true) // 死循环
{
std::unique_lock<std::mutex> umtx_lock(m_mutex);
/* 循环阻塞当前线程,直到通知到达且谓词满足 */
m_conditionVar.wait(umtx_lock, [this]() {
if (!this->MessageQueue.empty())
{
return true;
}
return false;
});
// 流程能执行到这里,表明互斥量一直是锁着的状态;MessageQueue中至少有一条命令command
int retVal = MessageQueue.pop_back();
return retVal;
}
}
public:
list<int> MessageQueue;
std::condition_variable m_conditionVar;
std::mutex m_mutex;
};
#include
条件变量提供了两个类:condition_variable
:只支持与普通mutex搭配,效率更高。
condition_variable() // 默认构造函数
notify_one() // 通知一个线程的wait函数
notify_all() // 同时通知多个线程的wait函数
/*
1)会把互斥锁解开;
2)阻塞,并等待“被唤醒”且“满足谓词”;
3)给互斥锁加锁;
总结:wait导致当前线程阻塞直至“被条件变量通知”或“虚假唤醒发生”,可选地循环直至“满足某谓词”。
*/
wait(unique_lock<mutex> lock) // 阻塞当前线程,直到通知到达
wait(unique_lock<mutex> lock, Pred pred) // 循环阻塞当前线程,直到通知到达且谓词满足
① wait()会尝试不断重新获取互斥量的锁,获取不到会卡在这里;获取到了(等于给互斥量加锁)则会继续执行 ②;
② 如果wait()函数有第二个参数,会判断lambda表达式,为false则解锁互斥量并阻塞在本行;为true则wait()返回,并继续向下执行流程(此时互斥量一直是被锁的状态);如果wait()函数没有第二个参数,则wait()返回并继续向下执行流程;
condition_variable_any
:一种通用的条件变量,可以与任意的mutex搭配使用,包括自定义的锁类型。
std::async函数模板:用来启动一个异步任务,并返回std::future对象(类模板对象)。
std::future对象中含有线程入口函数所返回的结果,能通过其成员函数get()来获取结果。
// 自动创建一个“异步线程”(执行“异步任务”)并开始执行对应的线程入口函数,最终返回一个std::future对象
std::future<int> result = std::async(mythread);
cout << result.get() << endl;
get()函数,一直在等待,除非拿到结果(且只能调用一次)。
对操作系统的线程库进行封装,都会损失一部分功能,为了弥补c++11线程库的不足,thread类提供了native_handle()成员函数:用于获得与操作系统相关的原生线程句柄。
注:操作系统的原生线程库,就可以用原生线程句柄操作线程。
#include
:c++11提供的模板类atomic
,其模板参数可以是bool、int、long、long long、指针类型(不支持浮点数和自定义类型)。
由CPU指令提供支持,性能比锁和消息传递效率更高,且支持修改、读取、交换、比较并交换等操作。
/* 构造函数:*/
atomic() noexcept = default; // 默认构造函数
constexpr atomic(T desired) noexcept; // 转换函数
atomic(const atomic&) = delete; // 拷贝构造函数
/* 赋值函数(禁用):*/
atomic& operator=(const atomic&) = delete;
/* 常用的函数:*/
void store(T desired, std::memory order order = std::memory order seq cst) noexcept;
// desired:存储到原子变量中的值、order:强制的内存顺序
/* 将desired值存入原子变量中:*/
T load(std::memory order order = std::memory order seq cst) const noexcept;
// 原子地加载并返回原子变量的当前值。按照order的值影响内存。
atomic
模板类的模板参数是指针,表示该指针是原子类型,但不表示它所指向的对象是原子类型。atomic
模板类重载了整数操作的各种运算符。CAS
指令,是实现“无锁队列”的基础。