1. 线程库
1.1 线程库
C++11中的线程库提供了一种方便的方式来创建和管理线程。其中,std::thread是一个重要的类,它允许我们创建新线程并控制它们的执行。以下是std::thread的一些重要函数:
- thread():默认构造函数,创建一个空的thread执行对象。
- explicit thread(Fn&& fn, Args&&… args):初始化构造函数,创建一个带函数调用参数的thread,这个线程是可joinable的。
- thread(const thread&) = delete:拷贝构造函数被禁用,意味着thread对象不可拷贝构造。
- thread(thread&& x) noexcept:移动构造函数,调用成功之后,x不代表任何thread执行对象。
- get_id():获取线程的ID,它将返回一个类型为std::thread::id的对象。
- joinable():检查线程是否可被join。
对于join,值得注意的是:在任意一个时间点上,线程是可结合(joinable)或者可分离(detached)的。一个可结合线程是可以被其它线程回收资源和杀死结束的,而对于detached状态的线程,其资源不能被其它线程回收和杀死,只能等待线程结束才能由系统自动释放。由默认构造函数创建的线程是不能被join的。
此外,std::thread还提供了其他一些重要的成员函数,如detach()、swap()、std::this_thread::get_id()、std::this_thread::yield()、sleep_until()、sleep_for()等。
注意:
1.线程是操作系统中的一个概念,线程对象可以关联一个线程,用来控制线程以及获取线程的状态。
2.当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程。
get_id()的返回值类型为id类型,id类型实际为std::thread命名空间下封装的一个类,该类中包含了一个结构体:
// vs下查看
typedef struct
{ /* thread identifier for Win32 */
void *_Hnd; /* Win32 HANDLE */
unsigned int _Id;
} _Thrd_imp_t;
3.当创建一个线程对象后,并且给线程关联线程函数,该线程就被启动,与主线程一起运行。
线程函数一般情况下可按照以下三种方式提供:1.函数指针; 2.lambda表达式; 3.函数对象。如下为三种用法:
#include <thread>
#include <chrono>
void func1()
{
int cnt = 5;
while (cnt)
{
cout << "我是线程" << this_thread::get_id() << "我正在运行中,运行剩余时间:" << cnt-- << endl;
this_thread::sleep_for(chrono::seconds(1));
}
}
struct func2
{
public:
void operator()()
{
int cnt = 5;
while (cnt)
{
cout << "我是线程"<< this_thread::get_id() << "我正在运行中,运行剩余时间:" << cnt-- << endl;
this_thread::sleep_for(chrono::seconds(1));
}
}
};
int main()
{
// 线程函数为函数指针
thread t1(func1);
// 线程函数为函数对象
func2 f;
thread t2(f);
// 线程函数为lambda表达式
thread t3([]()
{
int cnt = 5;
while (cnt)
{
cout << "我是线程" << this_thread::get_id() << "我正在运行中,运行剩余时间:" << cnt-- << endl;
this_thread::sleep_for(chrono::seconds(1));
}
});
t1.join();
t2.join();
t3.join();
return 0;
}
4.thread类是防拷贝的,不允许拷贝构造以及赋值,但是可以移动构造和移动赋值,即将一个线程对象关联线程的状态转移给其他线程对象,转移期间不意向线程的执行。
5.可以通过jionable()函数判断线程是否是有效的,如果是以下任意情况,则线程无效。1.采用无参构造函数构造的线程对象; 2.线程对象的状态已经转移给其他线程对象;3.线程已经调用jion或者detach结束。
线程函数参数 :线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,因此:即使线程参数为引用类型,在线程中修改后也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参。
#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作为线程函数参数。
多线程最主要的问题是共享数据带来的问题(即线程安全)。 如果共享数据都是只读的,那么没问题,因为只读操作不会影响到数据,更不会涉及对数据的修改,所以所有线程都会获得同样的数据。但是,当一个或多个线程要修改共享数据时,就会产生很多潜在的麻烦。
虽然加锁可以解决,但是加锁有一个缺陷就是:只要一个线程在对sum++时,其他线程就会被阻塞,会影响程序运行的效率,而且锁如果控制不好,还容易造成死锁。
因此C++11中引入了原子操作。所谓原子操作:即不可被中断的一个或一系列操作,C++11引入的原子操作类型,使得线程间数据的同步变得非常高效。
在C++11中,原子操作是通过std::atomic类型来实现的。std::atomic类型是一种模板类型,可以用于定义各种数据类型的原子变量,例如整型、浮点型、指针等.
std::atomic类型提供了一系列的原子操作函数,例如load()、store()、exchange()、compare_exchange_weak()、compare_exchange_strong()等,这些函数可以保证对共享变量的操作是原子的,即不会被其他线程的操作干扰.
使用原子操作可以避免使用锁带来的性能损失,因为原子操作不需要阻塞线程,而锁需要阻塞线程。
1.2 锁mutex
在多线程编程中,锁是一种常见的工具,用于保护共享资源,例如内存中的各种变量。锁的本质属性是为事物提供“访问保护”,以防止多个线程同时访问同一共享资源时出现不可预期的操作。在C++11中,引入了std::mutex类型,对于多线程的加锁操作提供了很好的支持。
当多个线程访问同一共享资源时,如果没有使用锁,就会出现多个线程对同一个变量进行读写操作,从而导致不可预期的操作。使用锁可以保证同一时间只有一个线程可以访问共享资源,从而避免了多个线程同时访问同一共享资源时出现的问题.
在C++11中,std::mutex对象是用来提供“访问保护”的,任意时刻最多允许一个线程对其进行上锁。如果一个线程想要访问共享资源,首先要进行“加锁”操作,如果加锁成功,则进行共享资源的读写操作,读写操作完成后释放锁;如果“加锁”不成功,则线程阻塞,直到加锁成功.
std::mutex,C++11提供的最基本的互斥量,该类的对象之间不能拷贝,也不能进行移动。mutex最常用
的三个函数:
函数名 | 函数功能 |
---|---|
lock() | 上锁:锁住互斥量 |
unlock() | 解锁:释放对互斥量的所有权 |
try_unlock() | 尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞 |
以下是一个使用C++11中锁的简单例子:
#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
int counter = 0;
std::mutex mtx; // 保护counter
void increase(int time)
{
for (int i = 0; i < time; i++)
{
mtx.lock();
counter++;
mtx.unlock();
}
}
int main(int argc, char** argv)
{
std::thread t1(increase, 10000);
std::thread t2(increase, 10000);
t1.join();
t2.join();
std::cout << "counter: " << counter << std::endl;
return 0;
}
支持两个线程交替打印,一个打印奇数,一个打印偶数:
#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
int main()
{
int n = 1000;
int x = 1;
mutex mtx;
condition_variable cv;
thread t1([&]()
{
while (x < n)
{
unique_lock<mutex> lock(mtx);
if (x % 2 == 0)
cv.wait(lock);
cout << this_thread::get_id() << " : " << x << endl;
++x;
cv.notify_one();
}
});
thread t2([&]()
{
while (x < n)
{
unique_lock<mutex> lock(mtx);
if (x % 2 != 0)
cv.wait(lock);
cout << this_thread::get_id() << " : " << x << endl;
++x;
cv.notify_one();
}
});
t1.join();
t2.join();
return 0;
}
2. 包装器
2.1 funciton
function包装器介绍:std::function 是一个通用的多态函数包装器,它可以存储、复制和调用任何可复制的可调用目标——函数(通过指向它们的指针)、lambda 表达式、绑定表达式或其他函数对象,以及指向成员函数和数据成员的指针 。
在 C++11 中,std::function 通常用作函数对象的容器。 它可以将任何可调用对象(例如函数、函数指针、成员函数指针、lambda 表达式等)封装为一个可调用对象,并支持将其作为参数传递和返回值返回 。以下是std::function的一些特点:
- std::function是一个类模板,可以用于定义函数对象。
- std::function对象可以存储任何可调用对象,包括函数、函数指针、成员函数指针、函数对象等。
- std::function对象可以像函数一样调用,即可以使用函数调用运算符()来调用它所存储的可调用对象。
- std::function对象可以复制和赋值,即可以像普通对象一样进行拷贝和赋值操作。
- std::function对象可以存储空函数对象,即不存储任何可调用对象。
int add1(int a, int b)
{
return a + b;
}
struct add2
{
public:
int operator()(int a, int b)
{
return a + b;
}
};
int main()
{
function<int(int, int)> fun1 = add1;
function<int(int, int)> fun2 = add2();
function<int(int, int)> fun3 = [](int a, int b)->int
{
return a + b;
};
cout << "fun1:" << fun1(10, 20) << endl;
cout << "fun2:" << fun2(10, 20) << endl;
cout << "fun3:" << fun3(10, 20) << endl;
return 0;
}
以上示例展示了如何使用function包装函数、仿函数、lambda 表达式等。
class Plus
{
public:
static int plusi(int a, int b)
{
return a + b;
}
double plusd(double a, double b)
{
return a + b;
}
};
int main()
{
function<int(int, int)> fun1 = Plus::plusi; // 静态成员没有this指针,所以正常调用即可,需注意访问类的成员函数带上类的作用域
function<double(Plus, double, double)> fun2 = &Plus::plusd;// 类的成员函数有默认的this指针,所以调用需要带上类名
return 0;
}
以上示例展示了如何使用function包装类的非成员函数和成员函数。
2.2 bind
std::bind是C++标准库中的一个函数模板,用于创建函数对象(也称为绑定器),将参数绑定到函数中。它的使用场景包括:
- 参数绑定:你可以使用std::bind将函数的一部分参数绑定到特定的值或者对象上,从而创建一个新的函数对象。这在需要将函数作为回调函数传递,但又需要固定一些参数时非常有用。
- 非成员函数的绑定:std::bind可以用于绑定非成员函数(全局函数或者静态成员函数),从而创建一个可调用的函数对象,该对象可以在不传递任何对象的情况下调用。
- 成员函数的绑定:std::bind也可以用于绑定成员函数,将对象的成员函数和对象本身绑定到一起,从而创建一个函数对象。这在需要将成员函数作为回调函数传递时非常有用。
// 原型如下:
template <class Fn, class… Args>
/* unspecified / bind (Fn&& fn, Args&&… args);
// with return type (2)
template <class Ret, class Fn, class… Args>
/ unspecified */ bind (Fn&& fn, Args&&… args);
可以将bind函数看作是一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对
象来“适应”原对象的参数列表。
调用bind的一般形式:auto newCallable = bind(callable,arg_list)
;其中,newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的callable的参数。当我们调用newCallable时,newCallable会调用callable,并传给它arg_list中的参数。
arg_list中的参数可能包含形如_n的名字,其中n是一个整数,这些参数是“占位符”,表示newCallable的参数,它们占据了传递给newCallable的参数的“位置”。数值n表示生成的可调用对
象中参数的位置:_1为newCallable的第一个参数,_2为第二个参数,以此类推。
如下是将函数参数调换顺序的用法:
#include <functional>
void print(int a, int b, int c)
{
cout << a << " " << b << " " << c << endl;
}
int main()
{
//_1,_2,_3在placeholders这个命名空间中,所以需要在placeholders中访问
auto rprint = bind(print, placeholders::_3, placeholders::_1, placeholders::_2);
// 修改参数顺序之前
print(10, 20, 30);
// 修改参数顺序之后
rprint(10, 20, 30);
return 0;
}
打印结果如下:
通过使用std::bind,可以灵活地创建新的函数对象,处理函数参数的绑定和适配,以及实现回调函数的自定义功能。以下是一个示例,展示了std::bind绑定函数值的用法:
void foo(int a, int b, int c)
{
std::cout << "a = " << a << ", b = " << b << ", c = " << c << std::endl;
}
int main()
{
std::function<void(int)> func = std::bind(foo, 1, 2, std::placeholders::_1);
func(3); // 调用 func,实际上调用 foo (1, 2, 3)
// 打印结果为 a = 1, b = 2, c = 3
// 因为将1,2绑定到func,func传参数3,_1为占位符
return 0;
}
class MyClass
{
public:
void printSum(int a, int b) // 类的成员函数有隐藏的this指针
{
std::cout << "Sum: " << a + b << std::endl;
}
};
int main()
{
MyClass obj;
auto printSumFunc = std::bind(&MyClass::printSum, &obj, 10, std::placeholders::_1);
printSumFunc(5); // 调用 printSumFunc,实际上调用 obj.printSum (10, 5)
return 0;
}
以上两个示例分别展示了如何使用std::bind绑定非成员函数和成员函数。