自学内容网 自学内容网

C++11的多线程

目录

引言

thread类的简单介绍

接口解读

使用范例

move的作用--将资源“夺舍”

原子性操作库(atomic)

lock_guard与unique_lock

前置知识:mutex锁(类似linux下的ptrhead_mutex_t数据)

mutex的种类

1. std::mutex

2. std::recursive_mutex

3. std::timed_mutex

4. std::recursive_timed_mutex

RAII

lock_guard

unique_lock

条件变量


引言

C++11的多线程特性,是对C++语言的一次重大补充,它使得C++在保持高性能和低级操作能力的同时,具备了更加强大的并发编程能力。在这个引言中,我们将探索C++11多线程的核心概念,包括线程的创建与管理、同步机制、数据共享与保护,以及异步操作等。这些特性不仅极大地简化了多线程程序的编写,也为C++开发者打开了一扇通往高效并发编程的大门。本文重点介绍:C++11的线程库部分,已经相对应的线程安全。

thread类的简单介绍

在C++11之前,涉及到多线程问题,都是和平台相关的,比如windows和linux下各有自己的接
口,这使得代码的可移植性比较差C++11中最重要的特性就是对线程进行支持了,使得C++在
并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念。要使用标准库中的
线程,必须包含< thread >头文件。

接口解读

thread类的第一个参数是一个可调用对象,后续的参数是可调用对象的参数

1.线程库禁止赋值拷贝、与赋值重载;但是允许移动构造!

两种构造,第二种是一种可变参数模板

2.以下接口:1.获取pid 2.是否线程可jion 3.阻塞join 4.分离 5.交换
3.thread和this_thread有什么区别?

在C++11中,thread 和 this_thread 是 <thread> 头文件中提供的两个不同的命名空间,它们各自包含了一系列用于多线程编程的函数和类。

以下是 thread 和 this_thread 的主要区别:

  1. std::thread

    • std::thread 是一个类,用于表示一个执行线程。
    • 它可以用来创建新的线程,并与之交互。
    • 通过构造函数,可以传递一个函数或者函数对象,以及可选的参数来启动线程。
    • std::thread 对象可以调用 join() 或 detach() 方法来同步或分离线程。
    • std::thread 类还提供了其他成员函数,如 get_id() 来获取线程ID,joinable() 来检查线程是否可以加入,以及 native_handle() 来获取底层实现特定的线程句柄。
  2. std::this_thread

    • std::this_thread 是一个命名空间,它包含了一系列函数,这些函数作用于当前执行的线程。
    • 它提供了如 get_id() 来获取当前线程的ID,yield() 来提示调度器当前线程愿意放弃处理器,以及 sleep_for() 和 sleep_until() 来使当前线程暂停执行指定的时间。
    • std::this_thread 中的函数通常用于控制或获取有关当前线程的信息,而不是创建或管理其他线程。

简而言之,std::thread 是用来创建和管理线程的类,而 std::this_thread 是一个包含用于操作当前执行线程的函数的命名空间。

使用范例

#include<iostream>
#include<vector>
#include<string>
#include<mutex>
#include<thread>
#include<chrono>

using namespace std;

void Print1(size_t n, const string& s, mutex& m, int& rx)
{
for (size_t i = 0; i < n; i++)
{
m.lock();

cout <<this_thread::get_id()<<s<<":" << i << endl;
++rx;

m.unlock();

this_thread::sleep_for(chrono::milliseconds(300));
}
}

int main()
{
mutex mtx;
int x = 0;
thread t1(Print1, 2,  "xianchen1", ref(mtx), ref(x));
thread t2(Print1, 3, "xiancheng2", ref(mtx), ref(x));

//thread t3(t1);

cout <<"线程1:" << t1.get_id() << endl;
cout <<"线程2:"<< t2.get_id() << endl;

t1.join();//在此处阻塞
t2.join();

cout << x << endl;

return 0;
}

注意点:

1.在线程的执行方法中,如果想要引用传参,必须要加上ref。保持引用的属性。

线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,因此:即使线程参数为引用类型,在
线程中修改后也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参
我们可以理解为:数据-->值拷贝到线程栈--->在线程栈的线程方法中执行。
#include <thread>
void ThreadFunc1(int& x)
{
 x += 10;
}
void ThreadFunc2(int* x)
{
 *x += 10;
}
int main()
{
 int a = 10;
 // 在线程函数中对a修改,不会影响外部实参,因为:线程函数参数虽然是引用方式,但其实际
引用的是线程栈中的拷贝
 thread t1(ThreadFunc1, a);
 t1.join();
 cout << a << endl;
 // 如果想要通过形参改变外部实参时,必须借助std::ref()函数
 thread t2(ThreadFunc1, std::ref(a);
 t2.join();
 cout << a << endl;
 // 地址的拷贝
 thread t3(ThreadFunc2, &a);
 t3.join();
 cout << a << endl;
 return 0;
}
注意:如果是类成员函数作为线程参数时,必须将this作为线程函数参数。

2.chrono类提供了时间管理方法。

例二:支持移动赋值。移动构造使得可以创建线程池(最开始全是空线程)。

void Print2(size_t n, const string& s)
{
for (size_t i = 0; i < n; i++)
{
cout << this_thread::get_id() << s << ":" << i << endl;
}
}

int main()
{
size_t n;
cin >> n;

//创建n个线程执行Print(空线程)
vector<thread> vthd(n);
size_t j = 0;
for (auto& thd : vthd)
{
// 移动赋值(线程启动)
thd = thread(Print2, 10,  "线程" + to_string(j++));
}

for (auto& thd : vthd)
{
thd.join();
}

thread t1(Print1, 100, 1, "我是小明");
thread t2(move(t1));

t2.join();

return 0;
}

赋值时的thread()是一个匿名对象,可以理解为将亡值。由于支持移动语义。因此可以进行“夺舍”。

直接转转不过去,但是move就可以。

thread t1(Print, 100, 1, "111");
thread t2(move(t1));
t2.join();

move的作用--将资源“夺舍”

在调用 std::move(t) 的那行代码中,t 被视为一个右值,你可以将它移动到一个新的对象中。但是,一旦移动操作完成,t 就变成了一个空的 std::thread 对象,它不再管理任何线程资源。如果在移动之后继续使用 t,可能会导致未定义行为,因为 t 可能不再指向任何有效的线程。

这个函数与上述的效果一致。

原子性操作库(atomic)

当对数据并发访问的时候,就需要对数据进行上锁保护。但是对于简单的数据操作,可以采用atomic类去封装数据。
多线程最主要的问题是共享数据带来的问题(即线程安全)。如果共享数据都是只读的,那么没问
题,因为只读操作不会影响到数据,更不会涉及对数据的修改,所以所有线程都会获得同样的数
据。但是,当一个或多个线程要修改共享数据时,就会产生很多潜在的麻烦。
C++98中传统的解决方式:可以对共享修改的数据可以加锁保护。
虽然加锁可以解决,但是加锁有一个缺陷就是:只要一个线程在对sum++时,其他线程就会被阻
塞,会影响程序运行的效率,而且锁如果控制不好,还容易造成死锁。
因此C++11中引入了原子操作。所谓原子操作:即不可被中断的一个或一系列操作,C++11引入
的原子操作类型,使得线程间数据的同步变得非常高效。
在C++11中,程序员不需要对原子类型变量进行加锁解锁操作,线程能够对原子类型变量互斥的
访问
更为普遍的,程序员可以使用atomic类模板,定义出需要的任意原子类型
原子类型通常属于"资源型"数据,多个线程只能访问单个原子类型的拷贝,因此在C++11
中,原子类型只能从其模板参数中进行构造,不允许原子类型进行拷贝构造、移动构造以及
operator=等,为了防止意外,标准库已经将atmoic模板类中的拷贝构造、移动构造、赋值运算
符重载默认删除掉了。---防止资源抢占。
#include <atomic>
int main()
{
 atomic<int> a1(0);
 //atomic<int> a2(a1);   // 编译失败
 atomic<int> a2(0);
 //a2 = a1;               // 编译失败
 return 0;
}

这样进行a1++,就是调用的operator++。

lock_guardunique_lock

在多线程环境下,如果想要保证某个变量的安全性,只要将其设置成对应的原子类型即可,即高
效又不容易出现死锁问题。但是有些情况下,我们可能需要保证一段代码的安全性,那么就只能
通过锁的方式来进行控制。

前置知识:mutex锁(类似linux下的ptrhead_mutex_t数据)

// mutex example
#include <iostream>       // std::cout
#include <thread>         // std::thread
#include <mutex>          // std::mutex

std::mutex mtx;           // mutex for critical section

void print_block (int n, char c) {
  // critical section (exclusive access to std::cout signaled by locking mtx):
  mtx.lock();
  for (int i=0; i<n; ++i) { std::cout << c; }
  std::cout << '\n';
  mtx.unlock();
}

int main ()
{
  std::thread th1 (print_block,50,'*');
  std::thread th2 (print_block,50,'$');

  th1.join();
  th2.join();

  return 0;
}

mutex的种类

在C++11中,Mutex总共包了四个互斥量的种类:

1. std::mutex

        
注意,线程函数调用lock()时,可能会发生以下三种情况:
如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,
该线程一直拥有该锁
如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住
如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)(锁的重复申请lock)
线程函数调用try_lock()时,可能会发生以下三种情况:
如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock
释放互斥量
如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉
如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。

2. std::recursive_mutex

这是递归锁。在递归函数中,如果对互斥量上锁,需要用递归锁

允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权,
释放互斥量时需要调用与该锁层次深度相同次数的 unlock(),除此之外,std::recursive_mutex 的特性和 std::mutex 大致相同。

3. std::timed_mutex

时间锁,支持在临界区休眠。

std::mutex 多了两个成员函数,try_lock_for()try_lock_until()

4. std::recursive_timed_mutex

RAII

当我们对数据不好控制时,特别是两把锁,容易产生死锁,因此在锁锁在的作用域中,我们期待出作用域直接销毁。这就需要RAII。

lock_guard

只有构造和析构

lock_guard类模板主要是通过RAII的方式,对其管理的互斥量进行了封
,在需要加锁的地方,只需要用上述介绍的任意互斥体实例化一个lock_guard,调用构造函数
成功上锁,出作用域前,lock_guard对象要被销毁,调用析构函数自动解锁,可以有效避免死锁
问题
lock_guard的缺陷:太单一,用户没有办法对该锁进行控制,因此C++11又提供了unique_lock。

unique_lock

相较于lock_guard,它最大的区别就是:

1.支持手动解锁

2.支持更多的锁类型,因此出现了这些接口

条件变量

由于存在线程同步机制,因此C++库也更新了条件变量。

行为上仍然类似于POSIX条件变量。

其中notify_one为唤醒一个线程,notify_all为全部唤醒。


原文地址:https://blog.csdn.net/2302_80190394/article/details/145268242

免责声明:本站文章内容转载自网络资源,如侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!