C++11是一个重要的标准,它引入了许多新的特性和库,其中一个非常重要的方面是多线程编程,即让一个进程可以同时运行多个任务。C++11为多线程编程提供了一系列的库。
在本系列中,我将介绍这些库的用法和示例。在第一篇,我将介绍thread和mutex两个库,讲解如何创建和管理线程,以及如何使用互斥锁来保证临界资源的数据安全。
C++11对线程接口做了封装,使得代码在Linux和Windows下可以通用,提高了可移植性。
std::thread用于创建一个线程,即一个执行流。各个执行流在当前进程内共享进程的代码和数据。每一个线程各自独立的数据位于进程地址空间的共享区。
thread() noexcept; // 无参构造
template< class Function, class... Args >
explicit thread( Function&& f, Args&&... args ); // 带参构造
thread( thread&& other ) noexcept; // 移动构造
thread( const thread& ) = delete; // 不允许拷贝构造
thread对象不可拷贝,只可移动。因为thread删除了拷贝构造和赋值构造,所以下面的代码是错误的。
int main()
{
std::vector<std::thread> allThread;
std::thread t1(func1);
allThread.push_back(t1); // Error
return 0;
}
函数 | 功能 |
---|---|
get_id | 获取该线程的id |
joinable | 判断该线程是否执行完毕,是:true,否:false |
join | 该函数调用后当前进程会阻塞等待该线程,当该线程结束后,当前线程才往后执行 |
detach | 将当前线程和该线程分离,当前线程后续无需join该线程 |
swap | 交换两个线程对象 |
template<class T>
void func1(T& val)
{}
void func2(int& val)
{}
int main()
{
int num = 0;
std::thread t1; // 没有执行流,空对象
std::thread t2(func1<int>, num); // 创建一个执行流用于执行func1函数,并把num以传值形式传递进去,线程里的num是新的。
std::thread t3(func2, std::ref(num)); // 创建一个执行流用于执行func2函数,并且将num以引用形式传递,线程里的num是和主线程共享的。
t1 = t3; // 移动构造,将t3对象移动到t1对象处,此时t3不再代表一个执行流。
t2.detach();
t3.join();
return 0;
}
引用传参的时候要使用std::ref,是因为thread构造函数中,参数列表是一个右值引用。因此C++提供了ref来包装以引用传递的值。
同样的还有std::bind的场景中,如果function对象要传入参数,且参数是引用,则需要使用ref。
void func(int& num1, int& num2, int& num3)
{
// 在这里,num1和num2都是两个独立的变量,而num3是main中num3的引用。
}
int main()
{
int num1, num2, num3;
auto obj = std::bind(func, num1, num2, std::ref(num3));
return 0;
}
在多线程中,可以使用ref来传递引用。ref只是模拟引用传递,而不是真实的引用。
命名空间this_thread提供了一批给当前线程使用的函数。
thread::id get_id();
void yield();
void sleep_for(const chrono::duration<_Rep, _Period>& __rtime);
template<typename _Clock, typename _Duration>
void sleep_until(const chrono::time_point<_Clock, _Duration>& __atime);
调用一个thread对象的get_id可以获取该线程的ID,但是如果在函数执行流中要获取本线程的ID,可以通过this_thread这个命名空间的get_id()方法。
返回值是一个``thread::id``对象,类内有一个运算符重载函数,用于输出
friend basic_ostream<_Ch, _Tr>& operator<<(basic_ostream<_Ch, _Tr>& _Str, thread::id _Id);
在Linux下,一层层去找id的来源,最终会找到id是一个 pthread_t
类型,即thread是封装了原生接口的。
关于打印这个id,有如下方法:
一、直接用cout输出
std::cout << std::this_thread::get_id() << std::endl;
二、用stringstream接收,然后输出原生字符串。
std::ostringstream oss;
oss << std::this_thread::get_id() << std::endl;
printf("%s\n", oss.str().c_str());
让当前线程放弃本次执行占用的时间片,让CPU调度其他线程。避免线程长时间占用CPU资源,导致多线程处理性能下降。
场景:当线程在等待某个事件就绪,如果一直检测,就会占用CPU时间。因此可以判断条件是否满足,如果不满足就yield。
while(true)
{
if(事件就绪)
{
}
else
yield;
}
但是这样看起来好像并不是有多大用处。实际上让出时间片并不会让CPU占用减少多少。所以类似事件驱动型的操作,用同步的方式,如condition_variable来通知线程会更好。
线程与进程一样具有状态:运行态、终止态、挂起态、阻塞态。每个线程都要占用CPU资源,但是资源是有限的,为了实现并发,线程是分时复用CPU的。sleep_for是睡眠函数,可以使这个线程变为阻塞态,并休眠一定时间,且休眠过程不占用CPU的资源。参数类型是时间长度duration。
std::this_thread::sleep_for(std::chrono::milliseconds(200)); // 睡眠200毫秒
与sleep_for类似,而until是休眠到一个时间点。参数类型是time_point。
auto now = std::chrono::steady_clock::now(); // 获取当前时间
auto wake = now + std::chrono::seconds(5); // 计算唤醒时间
std::this_thread::sleep_until(wake); // 睡眠直到唤醒
sleep都是利用操作系统的调度机制来实现睡眠。
多线程编程中,线程互斥是一个重要的话题。对于临界资源,需要访问控制机制来保证数据的准确性。mutex是C++11中基本的互斥量。C++11提供了4种互斥量:
mutex(); // 无参构造
mutex( const mutex& ) = delete; // 禁止拷贝构造
与thread同样的,mutex不支持拷贝构造。此外,不支持移动构造。
成员函数 | 功能 |
---|---|
lock | 上锁。如果mutex已经被其它线程上锁,那么会阻塞,直到解锁; |
unlock | 解锁。调用此函数会将mutex的主动权随机交给一个正在尝试上锁的线程。 |
try_lock | 尝试上锁。如果mutex未被上锁,则将其上锁并返回true;如果mutex已被锁则返回false。 |
对于lock,如果线程阻塞在lock,且该线程拥有mutex的所有权,那么会造成死锁。
与mutex不同,recursive支持递归上锁,即对同一个互斥量上多层的锁。相应的,上多层锁,解锁的时候也要开多层的锁。
与mutex相比,time_mutex多了两个成员函数:try_lock_for和try_lock_until。
try_lock_for:允许在一段时间范围内,如果没有成功解锁即被阻塞一段时间,如果超时即返回false。而try_lock是不会阻塞的。
try_lock_until:则是允许在一个时间点之前。
RAII的思想是C++等语言资源管理的重要指导思想。C++引入lock_guard,像智能指针管理内存一样管理一个互斥量。在一个作用域内获得一个锁,并在出了作用域,对象析构时归还这个锁。
std::mutex myMutex;
{
std::lock_guard<std::mutex> guard(myMutex);
}
lock_guard有一个缺陷:在作用域内定义lock_guard时会构造并加锁,直到析构时才解锁。如果作用域范围很大,那么锁的粒度就会很大。于是也引入了unique_lock。
void work() // 线程函数
{
while (true)
{
g_mutex.lock();
g_count++;
printf("This Is Thread");std::cout << this_thread::get_id() << std::endl;
g_mutex.unlock();
}
}
要注意使用mutex的接口,lock之后必须手动unlock。不然进程会crash,提一嘴,线程收到信号,进程会被kill掉。
所以可以使用lock_guard和unique_lock。
void work() // 线程函数
{
while (true)
{
std::unique_lock<std::mutex> locker(g_mutex);
g_count++;
printf("This Is Thread");std::cout << this_thread::get_id() << std::endl;
}
}
这样就省去了lock和unlock的步骤。
要注意的是:locker构造后,在手动unlock之前,lock也会crash。
因为对一个锁重复加锁和解锁都是错误的行为。
如果运行了上面关于mutex的示例代码,你会发现运行的时候,一个线程可能会长时间占用这个锁,直到一段时间过后才会交给另一个线程。并且这个新线程同样会占用不短的时间。
这就是线程的竞争问题,如果一个线程长时间占用不到时间片,就可以称为线程饥饿,会浪费资源。我的下一篇讲condition_variable的博客将会提到如何解决这个问题。