c++11中的线程库和包装器

本文详细介绍了C++11中的线程库,包括std::thread的构造函数、线程状态、线程函数的传递方式,以及std::mutex和锁的使用。同时讨论了std::function和std::bind这两个包装器,展示了如何绑定函数和成员函数以适应不同的调用场景,以及原子操作在避免竞态条件中的作用。
摘要由CSDN通过智能技术生成

1. 线程库

1.1 线程库

C++11中的线程库提供了一种方便的方式来创建和管理线程。其中,std::thread是一个重要的类,它允许我们创建新线程并控制它们的执行。以下是std::thread的一些重要函数:

  1. thread():默认构造函数,创建一个空的thread执行对象。
  2. explicit thread(Fn&& fn, Args&&… args):初始化构造函数,创建一个带函数调用参数的thread,这个线程是可joinable的。
  3. thread(const thread&) = delete:拷贝构造函数被禁用,意味着thread对象不可拷贝构造。
  4. thread(thread&& x) noexcept:移动构造函数,调用成功之后,x不代表任何thread执行对象。
  5. get_id():获取线程的ID,它将返回一个类型为std::thread::id的对象。
  6. 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的一些特点:

  1. std::function是一个类模板,可以用于定义函数对象。
  2. std::function对象可以存储任何可调用对象,包括函数、函数指针、成员函数指针、函数对象等。
  3. std::function对象可以像函数一样调用,即可以使用函数调用运算符()来调用它所存储的可调用对象。
  4. std::function对象可以复制和赋值,即可以像普通对象一样进行拷贝和赋值操作。
  5. 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++标准库中的一个函数模板,用于创建函数对象(也称为绑定器),将参数绑定到函数中。它的使用场景包括:

  1. 参数绑定:你可以使用std::bind将函数的一部分参数绑定到特定的值或者对象上,从而创建一个新的函数对象。这在需要将函数作为回调函数传递,但又需要固定一些参数时非常有用。
  2. 非成员函数的绑定:std::bind可以用于绑定非成员函数(全局函数或者静态成员函数),从而创建一个可调用的函数对象,该对象可以在不传递任何对象的情况下调用。
  3. 成员函数的绑定: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绑定非成员函数和成员函数。

评论 23
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ly@눈_눈

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值