C++学习记录——이십칠 C++11(3)

文章目录

  • 1、lambda
    • 1、捕捉列表
    • 2、简述C++线程
    • 3、lambda对象大小
  • 2、C++线程
    • 1、整体了解
    • 2、锁
      • 1、互斥锁
      • 2、递归互斥锁
      • 3、时间控制锁
      • 4、lock_guard
    • 3、atomic(原子)
    • 4、条件变量


1、lambda

在之前写排序时,用到过排升序,排降序,写了仿函数,有less和greater,这样就写好了排序。但是这样还是感觉有点复杂,因为我们还需要写两个函数。lambda可以解决这个问题,我们先看看lambda的语法。

C++学习记录——이십칠 C++11(3)_第1张图片

int main()
{
	auto add1 = [](int x, int y)->int {return x + y; };
	cout << add1(1, 2) << endl;
	return 0;
}

add1是一个lambda对象。上面也可以写成[](int x, int y)…(1, 2)。还可以写成这样

	auto add2 = [](int x, int y)
	{
		return x + y;
	};

lambda表达式之间不能互相赋值

1、捕捉列表

假如要交换两个变量的值

	int x = 0, y = 1;
	auto swap1 = [](int& x, int& y)
	{
		int tmp = x;
		x = y;
		y = tmp;
	};
	swap1(x, y);
	cout << x << " " << y << endl;

	//利用捕捉列表
	auto swap2 = []()
	{
		int tmp = x;
		x = y;
		y = tmp;
	};

swap2是无法用x和y,这相当于一个独立的域,只是定义在main里面,但不能访问main的变量。

	auto swap2 = [x, y]() mutable
	{
		int tmp = x;
		x = y;
		y = tmp;
	};

如果不写mutable,那么像这样写的就是传值捕捉,lambda内部是默认const修饰的,不能修改,加上异变mutable就是去掉这一属性,加上了mutable必须带上参数列表,即使为空。但这样写完后,值并没有交换,因为这是传值。把传值捕捉改成引用捕捉[&x, &y],这样不加mutable也可以交换值,不过这里就是一个坑点,&被识别为引用而不是取地址。

还可以混合捕捉,[&x, y]。全部引用捕捉[&],全部传值捕捉[=],[&, x]意思就是只有x是传值,其它都是引用,但不能这样写[&, =],因为意图不明确。

2、简述C++线程

我们不能像Linux那样写歌pthread_create来创建线程,因为这是POSIX
的线程库,是类Unix的系统用的,它是一个可移植的操作系统接口,Linux选择了这个标准,而Windows没有选择,Windows有自己的标准,所以Windows想要和Linux支持完全一样的线程代码,可以用条件编译

#ifdef _WIN32
    CreateThread
#else
    pthread_create

像这样来写,就麻烦。后来Linux,Windows都支持一个thread库,转换的操作由库来完成,这个库是一个面向对象的,库的用法和POSIX有很多相似之处,也有不一样的,比如函数参数args,C++11就用可变模板参数替代POSIX的args。

C++中线程头文件是。我们写一点lambda的代码来初步了解一下C++线程。

//传统写法
void Func(int n, int num)
{
	for (int i = 0; i < n; ++i)
	{
		cout << num << ":" << i << " ";
	}
	cout << endl;
}

int main()
{
	thread t1(Func, 7, 1);//一般不传仿函数,而是传函数指针
	thread t2(Func, 4, 2);
	//但是这里主线程提前结束了,就会崩,所以要join
	t1.join();
	t2.join();
	return 0;
}
//lambda写法
int main()
{
	int n1, n2;
	cin >> n1 >> n2;
	//thread t1(Func, 7, 1);//一般不传仿函数,而是传函数指针
	//thread t2(Func, 4, 2);
	//但是这里主线程提前结束了,就会崩,所以要join
	thread t1([n1](int num)
	{
		for (int i = 0; i < n1; ++i)
		{
			cout << num << ":" << i << " ";
		}
		cout << endl;
	}, 1);
	thread t2([n2](int num)
	{
		for (int i = 0; i < n2; ++i)
		{
			cout << num << ":" << i << " ";
		}
		cout << endl;
	}, 2);
	t1.join();
	t2.join();
	return 0;
}

现在这两个线程代码有些冗余,因为它们做的操作都一样。线程不允许拷贝构造,但是可以移动构造。

int main()
{
	size_t m;
	cin >> m;
	//要求m个线程分别打印n
	vector<thread> vthds(m);
	for (size_t i = 0; i < m; i++)
	{
		size_t n;
		cin >> n;
		vthds[i] = thread([i, n]() {
			for (int j = 0; j < n; j++)
			{
				cout << i << ":" << j << endl;
			}
			cout << endl;
			});
	}
	return 0;
}

给vthds[i]的是一个将亡值。如果要去掉这些线程,因为库中没有拷贝构造的原因,我们得用引用来删除。

	for (auto& t : vthds)
	{
		t.join();
	}

3、lambda对象大小

int main()
{
	int x = 0, y = 1;
	int m = 0, n = 1;
	auto swap1 = [](int& rx, int& ry)
	{
		int tmp = rx;
		rx = ry;
		ry = tmp;
	};
	cout << sizeof(swap1) << endl;
	return 0;
}

会打印出1。编译器会把lambda变成一个仿函数,并且又会把仿函数变成一个仿函数的类,lambda会被编译成一个仿函数类型的对象,仿函数的类是一个空类,没有给成员,所以它的大小是1。

在反汇编代码中,lambda会先构造一个仿函数对象,不过这个对象独特,每个lambda对象都不一样的仿函数类型,然后调用operator()函数。为什么两个lambda对象不能互相赋值?是因为它们的类型就不一样。

2、C++线程

1、整体了解

C++中线程的id是一个类,Linux是一个整数,获取id用get_id(),如果还在创建线程时就要看id,那么就用std里的子空间this_thread里的get_id函数来查看,哪个线程用就打印谁的id;Linux的sleep在C++中着两个函数,sleep_until和sleep_for,绝对和相对时间,时间上比如秒,second,休息多少秒是封装在一个chrono空间中的,这个需要引入头文件chrono

this_thread::sleep_for(chrono::seconds(1));

chrono里也有hours,minutes函数,milliseconds毫秒函数等。

无锁编程作为了解。无锁编程中会用yield接口,它可以主动让出时间片,也就是很少用锁的线程,实现无锁编程需要用到CAS,CAS能提供原子性的一系列操作,比如把旧值存起来,进行比较,如果在对这个值进行写操作的时候,它已经被改变了,那就放弃,进行swap或者set操作。在面对线程安全问题时,常用操作就是加锁,C++中也有锁的类,mutex。如果不加锁,那就是利用原子操作,它的本质就是CAS。

可以看陈皓大佬的这篇文章

无锁队列的实现

int i = 0;
i += 1;

CAS会如何处理这个+=?要对i加1,加1之前会先去i这个位置进行比较,旧值是0,新值是1,如果在写之前i的值和旧值一样,就set或者swap,不一样就false,不做操作,比较和set或swap是硬件上保证原子的。如果一个线程先改了值,另一个线程再过来时值已经被改动了,它会false,然后再继续判断它为什么false。

那么无锁编程如何利用CAS?在入队列时,先找到队列尾部,两个线程都想插入节点,如果tail的next是空,那就可以添加,那么另一个线程再去时就会发现tail的next不是空了,那它就退出,更新tail,再去看看能不能添加。

2、锁

1、互斥锁

经典场景

int x = 0;

void Func(int n)
{
	for (int i = 0; i < n; i++)
	{
		++x;
	}
}

int main()
{
	thread t1(Func, 100);
	thread t2(Func, 200);

	t1.join();
	t2.join();
	return 0;
}

程序能够正常运行,是因为线程的栈是独立的,两个线程执行两个函数会在各自的栈中开辟栈帧,不过访问的全局变量是一样的。这里肯定有线程安全问题,毕竟访问的是同一个全局变量。为了解决安全问题就加锁。

#include 

int x = 0;
mutex mtx;

void Func(int n)
{
	for (int i = 0; i < n; i++)
	{
		mtx.lock();
		++x;
		mtx.unlock();
	}
}

int main()
{
	thread t1(Func, 10000);
	thread t2(Func, 20000);

	t1.join();
	t2.join();

	cout << x << endl;
	return 0;
}

这样写有问题,for循环应当在临界区内,进入临界区之前加锁。

void Func(int n)
{
    //串行
	mtx.lock();
	for (int i = 0; i < n; i++)
	{
		++x;
	}
	mtx.unlock();
	//并行
	/*for (int i = 0; i < n; i++)
	{
		mtx.lock();
		++x;
		mtx.unlock();
	}*/
}

int main()
{
	int n = 100000;
	size_t begin = clock();
	thread t1(Func, n);
	thread t2(Func, n);

	t1.join();
	t2.join();
	size_t end = clock();
	cout << x << " | " << (end - begin) << endl;
	return 0;
}

实际上它也会快很多。并行不一定就会串行快,还要看具体的场景。上面的并行场景,t1加锁了,然后在执行,t2看到了就去阻塞,带着上下文切出去,但是代码只有++x,t1走得很快,可能t2上下文还没切出去t1就结束了,t2又得回来。当执行的任务多了,并行就会比串行效率高了。

lambda写法

int main()
{
	int n = 100000;
	int x = 0;
	mutex mtx;
	size_t begin = clock();
	thread t1([&, n](){
		mtx.lock();
		for (int i = 0; i < n; i++)
		{
			++x;
		}
		mtx.unlock();
	});
	thread t2([&, n]() {
		mtx.lock();
		for (int i = 0; i < n; i++)
		{
			++x;
		}
		mtx.unlock();
	});

	t1.join();
	t2.join();
	size_t end = clock();
	cout << x << " | " << (end - begin) << endl;
	return 0;
}

2、递归互斥锁

int x = 0;

void Func(int n)
{
	if (n == 0)
		return;
	++x; 
	Func(n - 1);
}

int main()
{
	thread t1(Func, 10000);
	thread t2(Func, 10000);

	t1.join();
	t2.join();
	
	cout << x << endl;
	return 0;
}

Debug模式下会爆栈。加锁一下

void Func(int n)
{
	if (n == 0)
		return;
	mtx.lock();
	++x; 
	mtx.unlock();
	Func(n - 1);
}

这样加锁可以运行,但是解锁放在Func(n - 1)后就不行了,造成了死锁问题,这里就需要用递归互斥锁,即使是放在Func(n - 1)后面也能运行,不过如果n过大还是会爆栈。递归互斥锁在进入递归时,如果发现有自己之前加的锁,那就不加锁了,即使有lock。

3、时间控制锁

timed_mutex类型,有try_lock_for try_lock_until try_lock等函数,具体的查看cplusplus.com,for接口会接收一个时间,如果过了这段时间还没解锁那就会直接解锁。还有递归的时间控制锁。

4、lock_guard

假设加锁后,临界区内抛出了异常,抛后会直接跳到捕获异常的地方,那就会造成死锁,因为没有解锁。这个的解决办法是写一个类,构造时加锁,析构时自动解锁

template <class T>
class LockGuard
{
public:
	LockGuard(T& lk)
		:_lk(lk)
	{
		_lk.lock();
	}
	~LockGuard()
	{
		_lk.unlock();
	}
private:
	T _lk;
};

int x = 0;
mutex mtx;

void Func(int n)
{
	for (int i = 0; i < n; i++)
	{
		try
		{
			//mtx.lock();
			LockGuard<mutex> lock(mtx);
			++x;
			//模拟抛异常
			if (rand() % 3 == 0)
			{
				throw exception("抛异常");
			}
			//mtx.unlock();
		}
		catch (const exception& e)
		{
			cout << e.what() << endl;
		}
	}

异常之后会写。在抛异常时,锁出了作用域,就会自己解锁,也就是解决了死锁问题。但是这个写法是不对的

C++学习记录——이십칠 C++11(3)_第2张图片

即使改成右值,初始化列表里写成_lk(move(lk))也不行,因为锁只有拷贝构造,没有移动构造。这样写就行

private:
	T& _lk;

加引用的成员变量必须在初始化列表里初始化。

库中有这个类,不需要我们自己写,有两个lock_guard和unique_lock,头文件mutex。如果有可能抛异常的话,那就用这两个接口,unique_lock支持手动解锁。

3、atomic(原子)

C++11把它封装成库,支持CAS。它支持±*/与或异或的原子性。常用的是atomic的类,默认构造可以不给值,并且不允许拷贝,看一下之前的这个代码

int main()
{
	int n = 100000;
	int x = 0;
	mutex mtx;
	size_t begin = clock();
	thread t1([&, n](){
		mtx.lock();
		for (int i = 0; i < n; i++)
		{
			++i;
		}
		mtx.unlock();
	});
	thread t2([&, n]() {
		mtx.lock();
		for (int i = 0; i < n; i++)
		{
			++i;
		}
		mtx.unlock();
	});

	t1.join();
	t2.join();
	size_t end = clock();
	cout << x << " | " << (end - begin) << endl;
	return 0;
}

改成原子的

int main()
{
	int n = 100000;
	atomic<int> x = 0;// = {0} or x{0}
	//mutex mtx;
	size_t begin = clock();
	thread t1([&, n](){
		//mtx.lock();
		for (int i = 0; i < n; i++)
		{
			++x;
		}
		//mtx.unlock();
	});
	thread t2([&, n]() {
		//mtx.lock();
		for (int i = 0; i < n; i++)
		{
			++x;
		}
		//mtx.unlock();
	});

	t1.join();
	t2.join();
	size_t end = clock();
	cout << x << " | " << (end - begin) << endl;
	return 0;
}

底层是用CAS支持的。内置类型的变量要做一些简单的操作用原子类就好,不需要加锁。但是用原子类来定义的变量,传给参数不是原子类定义的函数,不能传,就需要用store或者load函数,x.load()和x.store()。

4、条件变量

头文件condition_variable,提供了两种接口,wait和notify

写两个线程交替打印数字,一个打印奇数,一个打印偶数。

int main()
{
	int n = 100;
	int x = 1;
	size_t begin = clock();
	thread t1([&, n](){
		for (int i = 0; i < n; i++)
		{
			cout << this_thread::get_id() << ":" << x << endl;
		}
	});
	thread t2([&, n]() {
		for (int i = 0; i < n; i++)
		{
			cout << this_thread::get_id() << ":" << x << endl;
		}
	});

	t1.join();
	t2.join();
	size_t end = clock();
	cout << x << " | " << (end - begin) << endl;
	return 0;
}

打印出来的结果并不是我们想象的那样的交替,有可能同时打印一个数,即使加锁也不能解决问题,这就得用到条件变量。条件变量要配合锁,锁会用unique_lock。

wait接口,当前线程会阻塞,直到被唤醒。wait时会修改条件变量的值,其它线程也有可能修改,所以在wait之前需要加锁;在wait发生的一瞬间,就会解锁,其它线程就可以使用锁了,后面notify的时候,唤醒线程,一瞬间就会加上锁,所以要用unique_lock,需要不靠类来解锁。wait有wait_for和wait_until。

notify有notify_one和notify_all,随机唤醒一个和唤醒所有,但是唤醒所有的话谁先占据资源就不确定,所以通常不使用all。 notify_one会唤醒正在等待条件的线程,没有等待的就什么都不做。

	mutex mtx;
	condition_variable cv;
	int n = 100;
	int x = 1;
	size_t begin = clock();
	thread t1([&, n](){
		while (x < n)
		{
			unique_lock<mutex> lock(mutex);
			cv.wait(lock);
			cout << this_thread::get_id() << ":" << x << endl;
			++x;
		}
	});
	thread t2([&, n]() {
		while(x < n)
		{
			unique_lock<mutex> lock(mutex);
			cv.wait(lock);
			cout << this_thread::get_id() << ":" << x << endl;
			++x;
		}
	});

如何保证t1先运行,t2阻塞?wait会让线程阻塞在条件变量上。

	thread t1([&, n](){
		while (x < n)
		{
			unique_lock<mutex> lock(mutex);
			cout << this_thread::get_id() << ":" << x << endl;
			++x;
		}
	});
	thread t2([&, n]() {
		while(x < n)
		{
			unique_lock<mutex> lock(mutex);
			cv.wait(lock);
			cout << this_thread::get_id() << ":" << x << endl;
			++x;
		}
	});

如果t1申请了锁,那就执行;如果t2申请了锁,然后去wait,释放锁,t1再申请到了锁,然后执行。等到t1执行完,t2才能申请到锁来执行代码。

那么接下来如何防止一个线程不断运行?如果t1这样写

	thread t1([&, n](){
		while (x < n)
		{
			unique_lock<mutex> lock(mtx);
			cout << this_thread::get_id() << ":" << x << endl;
			++x;
			cv.notify_one();
		}
	});

线程都是要看自己的时间片来运行的,唤醒t2后,t2会去加锁,t1时间又回到了加锁那一行,因为t1时间片还没到,t1和t2抢锁,t1申请到了,t1就会继续打印。实际也确实会出现这种情况。

	thread t1([&, n](){
		while (x < n)
		{
			unique_lock<mutex> lock(mtx);
			if (x % 2 == 0)//t1打印奇数,偶数就去阻塞
			{
				cv.wait(lock);
			}
			cout << this_thread::get_id() << ":" << x << endl;
			++x;
			cv.notify_one();
		}
	});
	thread t2([&, n]() {
		while(x < n)
		{
			unique_lock<mutex> lock(mtx);
			cv.wait(lock);
			cout << this_thread::get_id() << ":" << x << endl;
			++x;
		}
	});

按照这样写,t1连续得到锁后,它wait,释放锁,阻塞在条件变量上,t2就能拿到锁,然后执行,回到加锁代码行上,此时t1还在wait,没唤醒它,然后t2也来到wait那一行,两者就都wait,所以t2也要有唤醒的动作。但是即使有唤醒,也有可能两者运行速度不一样而导致问题,所以t2也得有条件判断。

	thread t1([&, n](){
		while (x < n)
		{
			unique_lock<mutex> lock(mtx);
			if (x % 2 == 0)//t1打印奇数,偶数就去阻塞
			{
				cv.wait(lock);
			}
			cout << this_thread::get_id() << ":" << x << endl;
			++x;
			cv.notify_one();
		}
	});
	thread t2([&, n]() {
		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拿到了锁,t2阻塞在锁上,t1打印,t1notify,但是没有线程wait,那么t1的锁出了作用域,解锁,这时候t1和t2竞争锁,一般情况下t2会获取锁,即使t1获取锁,它还是会wait。即使某个线程时间片到了,切出去了,另一个线程也不会继续打印。

也可以这样改

	thread t1([&, n](){
		while (1)
		{
			unique_lock<mutex> lock(mtx);
			if (x >= n) break;
			if (x % 2 == 0)//t1打印奇数,偶数就去阻塞
			{
				cv.wait(lock);
			}
			cout << this_thread::get_id() << ":" << x << endl;
			++x;
			cv.notify_one();
		}
	});
	thread t2([&, n]() {
		while(1)
		{
			unique_lock<mutex> lock(mtx);
			if (x > n) break;
			if (x % 2 != 0)
			{
				cv.wait(lock);
			}
			cout << this_thread::get_id() << ":" << x << endl;
			++x;
			cv.notify_one();
		}
	});

另外还有一个版本的wait,wait返回false就阻塞,true就继续执行,适合多生产多消费。

cv.wait(lock, [&x]() {return x % 2 != 0; });//t1
cv.wait(lock, [&x]() {return x % 2 == 0; });//t2

本篇gitee

结束。

你可能感兴趣的:(C++学习,c++,学习)