028-86922220

建站动态

根据您的个性需求进行定制 先人一步 抢占小程序红利时代

C++11———线程库-创新互联

文章目录

创新互联建站专注为客户提供全方位的互联网综合服务,包含不限于成都做网站、成都网站制作、乌海海南网络推广、重庆小程序开发公司、乌海海南网络营销、乌海海南企业策划、乌海海南品牌公关、搜索引擎seo、人物专访、企业宣传片、企业代运营等,从售前售中售后,我们都将竭诚为您服务,您的肯定,是我们大的嘉奖;创新互联建站为所有大学生创业者提供乌海海南建站搭建服务,24小时服务热线:18980820575,官方网址:www.cdcxhl.com线程库

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

线程库(thread) 线程对象的构造方式

一、调用无参的构造函数

thread提供了无参的构造函数,调用无参的构造函数创建出来的线程对象没有关联任何线程函数,即没有启动任何线程。比如:

thread t1;

由于thread提供了移动赋值函数,因此当后续需要让该线程对象与线程函数关联时,可以以带参的方式创建一个匿名对象,然后调用移动赋值将该匿名对象关联线程的状态转移给该线程对象。比如:

void func(int n)
{for (int i = 0; i<= n; i++)
	{cout<< i<< endl;
	}
}
int main()
{thread t1;
	//...
	t1 = thread(func, 10);

	t1.join();
	return 0;
}

场景: 实现线程池的时候就是需要先创建一批线程,但一开始这些线程什么也不做,当有任务到来时再让这些线程来处理这些任务。

二、调用带参的构造函数

thread的带参的构造函数的定义如下:

templateexplicit thread (Fn&& fn, Args&&... args);

参数说明:

调用带参的构造函数创建线程对象,能够将线程对象与线程函数fn进行关联。比如:

void func(int n)
{for (int i = 0; i<= n; i++)
	{cout<< i<< endl;
	}
}
int main()
{thread t2(func, 10);

	t2.join();
	return 0;
}

三、调用移动构造函数

thread提供了移动构造函数,能够用一个右值线程对象来构造一个线程对象。比如:

void func(int n)
{for (int i = 0; i<= n; i++)
	{cout<< i<< endl;
	}
}
int main()
{thread t3 = thread(func, 10);

	t3.join();
	return 0;
}

说明一下:

thread提供的成员函数

thread中常用的成员函数如下:

成员函数功能
join对该线程进行等待,在等待的线程返回之前,调用join函数的线程将会被阻塞
joinable判断该线程是否已经执行完毕,如果是则返回true,否则返回false
detach将该线程与创建线程进行分离,被分离后的线程不再需要创建线程调用join函数对其进行等待
get_id获取该线程的id
swap将两个线程对象关联线程的状态进行交换

此外,joinable函数还可以用于判定线程是否是有效的,如果是以下任意情况,则线程无效:

获取线程的id的方式

调用thread的成员函数get_id可以获取线程的id,但该方法必须通过线程对象来调用get_id函数,如果要在线程对象关联的线程函数中获取线程id,可以调用this_thread命名空间下的get_id函数。比如:

void func()
{cout<< this_thread::get_id()<< endl; //获取线程id
}
int main()
{thread t(func);

	t.join();
	return 0;
}

this_thread命名空间中还提供了以下三个函数:

函数名功能
yield当前线程“放弃”执行,让操作系统调度另一线程继续执行
sleep_until让当前线程休眠到一个具体时间点
sleep_for让当前线程休眠一个时间段
线程函数的参数问题

线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,就算线程函数的参数为引用类型,在线程函数中修改后也不会影响到外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参。比如:

void add(int& num)
{num++;
}
int main()
{int num = 0;
	thread t(add, num);
	t.join();

	cout<< num<< endl; //0
	return 0;
}

如果要通过线程函数的形参改变外部的实参,可以参考以下三种方式:

方式一:借助std::ref函数

当线程函数的参数类型为引用类型时,如果要想线程函数形参引用的是外部传入的实参,而不是线程栈空间中的拷贝,那么在传入实参时需要借助ref函数保持对实参的引用。比如:

void add(int& num)
{num++;
}
int main()
{int num = 0;
	thread t(add, ref(num));
	t.join();

	cout<< num<< endl; //1
	return 0;
}

方式二:地址的拷贝

将线程函数的参数类型改为指针类型,将实参的地址传入线程函数,此时在线程函数中可以通过修改该地址处的变量,进而影响到外部实参。比如:

void add(int* num)
{(*num)++;
}
int main()
{int num = 0;
	thread t(add, &num);
	t.join();

	cout<< num<< endl; //1
	return 0;
}

方式三:借助lambda表达式

将lambda表达式作为线程函数,利用lambda函数的捕捉列表,以引用的方式对外部实参进行捕捉,此时在lambda表达式中对形参的修改也能影响到外部实参。比如:

int main()
{int num = 0;
	thread t([&num]{num++; });
	t.join();

	cout<< num<< endl; //1
	return 0;
}
join与detach

启动一个线程后,当这个线程退出时,需要对该线程所使用的资源进行回收,否则可能会导致内存泄露等问题。thread库给我们提供了如下两种回收线程资源的方式:

join方式

主线程创建新线程后,可以调用join函数等待新线程终止,当新线程终止时join函数就会自动清理线程相关的资源。

join函数清理线程的相关资源后,thread对象与已销毁的线程就没有关系了,因此一个线程对象一般只会使用一次join,否则程序会崩溃。比如:

void func(int n)
{for (int i = 0; i<= n; i++)
	{cout<< i<< endl;
	}
}
int main()
{thread t(func, 20);
	t.join();
	t.join(); //程序崩溃
	return 0;
}

但如果一个线程对象join后,又调用移动赋值函数,将一个右值线程对象的关联线程的状态转移过来了,那么这个线程对象又可以调用一次join。比如:

void func(int n)
{for (int i = 0; i<= n; i++)
	{cout<< i<< endl;
	}
}
int main()
{thread t(func, 20);
	t.join();

	t = thread(func, 30);
	t.join();
	return 0;
}

但采用join的方式结束线程,在某些场景下也可能会出现问题。比如在该线程被join之前,如果中途因为某些原因导致程序不再执行后续代码,这时这个线程将不会被join

void func(int n)
{for (int i = 0; i<= n; i++)
	{cout<< i<< endl;
	}
}
bool DoSomething()
{return false;
}
int main()
{thread t(func, 20);

	//...
	if (!DoSomething())
		return -1;
	//...

	t.join(); //不会被执行
	return 0;
}

因此采用join方式结束线程时,join的调用位置非常关键,为了避免上述问题,可以采用RAII的方式对线程对象进行封装,也就是利用对象的生命周期来控制线程资源的释放。比如:

class myThread
{public:
	myThread(thread& t)
		:_t(t)
	{}
	~myThread()
	{if (_t.joinable())
			_t.join();
	}
	//防拷贝
	myThread(myThread const&) = delete;
	myThread& operator=(const myThread&) = delete;
private:
	thread& _t;
};

使用方式如下:

例如刚才的代码中,使用myThread类对线程对象进行封装后,就能保证线程一定会被join

int main()
{thread t(func, 20);
	myThread mt(t); //使用myThread对线程对象进行封装

	//...
	if (!DoSomething())
		return -1;
	//...

	t.join();
	return 0;
}

detach方式

主线程创建新线程后,也可以调用detach函数将新线程与主线程进行分离,分离后新线程会在后台运行,其所有权和控制权将会交给C++运行库,此时C++运行库会保证当线程退出时,其相关资源能够被正确回收。

互斥量库(mutex) mutex的种类

四种互斥量

在C++11中,mutex中总共包了四种互斥量:

1、std::mute
mutex锁是C++11提供的最基本的互斥量,mutex对象之间不能进行拷贝,也不能进行移动。

mutex中常用的成员函数如下:

成员函数功能
lock对互斥量进行加锁
try_lock尝试对互斥量进行加锁
unlock对互斥量进行解锁,释放互斥量的所有权

线程函数调用lock时,可能会发生以下三种情况:

线程调用try_lock时,类似也可能会发生以下三种情况:

2、std::recursive_mutex
recursive_mutex叫做递归互斥锁,该锁专门用于递归函数中的加锁操作。

除此之外,recursive_mutex也提供了locktry_lockunlock成员函数,其的特性与mutex大致相同。

3、std::timed_mutex
timed_mutex中提供了以下两个成员函数:

除此之外,timed_mutex也提供了locktry_lockunlock成员函数,其的特性与mutex相同。

4、std::recursive_timed_mutex
recursive_timed_mutex就是recursive_mutex和timed_mutex的结合,recursive_timed_mutex既支持在递归函数中进行加锁操作,也支持定时尝试申请锁。

加锁示例

在没有使用互斥锁保证线程安全的情况下,让两个线程各自打印1-100的数字,就会导致控制台输出错乱。比如:

void func(int n)
{for (int i = 1; i<= n; i++)
	{cout<< i<< endl;
	}
}
int main()
{thread t1(func, 100);
	thread t2(func, 100);

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

如果要让两个线程的输出不会相互影响,即不会让某一次输出中途被另一个线程打断,那么就需要用互斥锁对打印过程进行保护。

这里加锁的方式有两种,一种是在for循环体内进行加锁,一种是在for循环体外进行加锁。比如:

void func(int n, mutex& mtx)
{mtx.lock(); //for循环体外加锁
	for (int i = 1; i<= n; i++)
	{//mtx.lock(); //for循环体内加锁
		cout<< i<< endl;
		//mtx.unlock();
	}
	mtx.unlock();
}
int main()
{mutex mtx;
	thread t1(func, 100, ref(mtx));
	thread t2(func, 100, ref(mtx));

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

说明一下:

经验分享:

lock_guard和unique_lock

使用互斥锁时可能出现的问题

使用互斥锁时,如果加锁的范围太大,那么极有可能在中途返回时忘记了解锁,此后申请这个互斥锁的线程就会被阻塞住,也就是造成了死锁问题。比如:

mutex mtx;
void func()
{mtx.lock();
	//...
	FILE* fout = fopen("data.txt", "r");
	if (fout == nullptr)
	{//...
		return; //中途返回(未解锁)
	}
	//...
	mtx.unlock();
}
int main()
{func();
	return 0;
}

因此使用互斥锁时如果控制不好就会造成死锁,最常见的就是此处在锁中间代码返回,此外还有一个比较常见的情况就是在锁的范围内抛异常,也很容易导致死锁问题。

因此C++11采用RAII的方式对锁进行了封装,于是就出现了lock_guard和unique_lock。

lock_guard

lock_guard是C++11中的一个模板类,其定义如下:

templateclass lock_guard;

lock_guard类模板主要是通过RAII的方式,对其管理的互斥锁进行了封装。

通过这种构造对象时加锁,析构对象时自动解锁的方式就有效的避免了死锁问题。比如:

mutex mtx;
void func()
{lock_guardlg(mtx); //调用构造函数加锁
	//...
	FILE* fout = fopen("data.txt", "r");
	if (fout == nullptr)
	{//...
		return; //调用析构函数解锁
	}
	//...
} //调用析构函数解锁
int main()
{func();
	return 0;
}

从lock_guard对象定义到该对象析构,这段区域的代码都属于互斥锁的保护范围。

如果只想用lock_guard保护某一段代码,可以通过定义匿名的局部域来控制lock_guard对象的生命周期。比如:

mutex mtx;
void func()
{//...
	//匿名局部域
	{lock_guardlg(mtx); //调用构造函数加锁
		FILE* fout = fopen("data.txt", "r");
		if (fout == nullptr)
		{	//...
			return; //调用析构函数解锁
		}
	} //调用析构函数解锁
	//...
}
int main()
{func();
	return 0;
}

模拟实现lock_guard

模拟实现lock_guard类的步骤如下:

代码如下:

namespace cl
{templateclass lock_guard
	{public:
		lock_guard(Mutex& mtx)
			:_mtx(mtx)
		{	mtx.lock(); //加锁
		}
		~lock_guard()
		{	mtx.unlock(); //解锁
		}
		lock_guard(const lock_guard&) = delete;
		lock_guard& operator=(const lock_guard&) = delete;
	private:
		Mutex& _mtx;
	};
}

unique_lock

但由于lock_guard太单一,用户没有办法对锁进行控制,因此C++11又提供了unique_lock。

unique_lock与lock_guard类似,unique_lock类模板也是采用RAII的方式对锁进行了封装。在创建unique_lock对象调用构造函数时也会调用lock进行加锁,在unique_lock对象销毁调用析构函数时也会调用unlock进行解锁。

但lock_guard不同的是,unique_lock更加的灵活,提供了更多的成员函数:

比如如下场景就适合使用unique_lock:

如下图:
在这里插入图片描述

原子性操作库(atomic)

线程安全问题

多线程最主要的问题是共享数据带来的问题(即线程安全)。如果共享数据都是只读的,那么没问题,因为只读操作不会影响到数据,更不会涉及对数据的修改,所以所有线程都会获得同样的数据。但是,当一个或多个线程要修改共享数据时,就会产生很多潜在的麻烦。比如:

void func(int& n, int times)
{for (int i = 0; i< times; i++)
	{n++;
	}
}
int main()
{int n = 0;
	int times = 100000; //每个线程对n++的次数
	thread t1(func, ref(n), times);
	thread t2(func, ref(n), times);

	t1.join();
	t2.join();
	cout<< n<< endl; //打印n的值
	return 0;
}

上述代码中分别让两个线程对同一个变量n进行了100000次++操作,理论上最终n的值应该是200000,但最终打印出n的值却是小于200000的。

根本原因就是++操作并不是一个原子操作,该操作分为三步:

++操作对应的汇编代码如下:
在这里插入图片描述
因此可能当线程1刚将n的值加载到寄存器中就被切走了,也就是只完成了++操作的第一步,而线程2可能顺利完成了一次完整的++操作才被切走,而这时线程1继续用之前加载到寄存器中的值完成剩余的两步操作,最终就会导致两个线程分别对共享变量n进行了一次++操作,但最终n的值却只被++了一次。

加锁解决线程安全问题

C++98中对于这里出现的线程安全的问题,会选择对共享修改的数据进行加锁保护。比如:

void func(int& n, int times, mutex& mtx)
{mtx.lock();
	for (int i = 0; i< times; i++)
	{//mtx.lock();
		n++;
		//mtx.unlock();
	}
	mtx.unlock();
}
int main()
{int n = 0;
	int times = 100000; //每个线程对n++的次数
	mutex mtx;
	thread t1(func, ref(n), times, ref(mtx));
	thread t2(func, ref(n), times, ref(mtx));

	t1.join();
	t2.join();
	cout<< n<< endl; //打印n的值
	return 0;
}

这里可以选择在for循环体里面进行加锁解锁,也可以选择在for循环体外进行加锁解锁。但效果终究是不尽人意的,在for循环体里面进行加锁解锁会导致线程的频繁进行加锁解锁操作,在for循环体外面进行加锁解锁会导致两个线程的执行逻辑变为串行,而且如果锁控制得不好,还容易造成死锁。

原子类解决线程安全问题

C++11中引入了原子操作类型,使得线程间数据的同步变得非常高效。如下:

原子类型名称对应的内置类型名称
atomic_boolbool
atomic_charchar
atomic_scharsigned char
atomic_ucharunsigned char
atomic_intint
atomic_uintunsigned int
atomic_shortshort
atomic_ushortunsigned short
atomic_longlong
atomic_ulongunsigned long
atomic_llonglong long
atomic_ullongunsigned long long
atomic_char16_tchar16_t
atomic_char32_tchar32_t
atomic_wchar_twchar_t

注意: 需要用大括号对原子类型的变量进行初始化。

程序员不需要对原子类型进行加锁解锁操作,线程能够对原子类型变量互斥访问。比如刚才的代码可以改为:

void func(atomic_int& n, int times)
{for (int i = 0; i< times; i++)
	{n++;
	}
}
int main()
{atomic_int n = {0 };
	int times = 100000; //每个线程对n++的次数
	thread t1(func, ref(n), times);
	thread t2(func, ref(n), times);

	t1.join();
	t2.join();
	cout<< n<< endl; //打印n的值
	return 0;
}

除此之外,也可以使用atomic类模板定义出任意原子类型。比如上述代码还可以改为:

void func(atomic& n, int times)
{for (int i = 0; i< times; i++)
	{n++;
	}
}
int main()
{atomicn = 0;
	int times = 100000; //每个线程对n++的次数
	thread t1(func, ref(n), times);
	thread t2(func, ref(n), times);

	t1.join();
	t2.join();
	cout<< n<< endl; //打印n的值
	return 0;
}

说明一下:

条件变量库(condition_variable)

condition_variable中提供的成员函数,可分为wait系列和notify系列两类。

wait系列成员函数

wait系列成员函数的作用就是让调用线程进行阻塞等待,包括waitwait_forwait_until

下面先以wait为例进行介绍,wait函数提供了两个不同版本的接口:

//版本一
void wait(unique_lock& lck);
//版本二
templatevoid wait(unique_lock& lck, Predicate pred);

函数说明:

为什么调用wait系列函数时需要传入一个互斥锁?

wait_for和wait_until函数的使用方式与wait函数类似:

注意: 调用wait系列函数时,传入互斥锁的类型必须是unique_lock。

notify系列成员函数

notify系列成员函数的作用就是唤醒等待的线程,包括notify_onenotify_all

注意: 条件变量下可能会有多个线程在进行阻塞等待,这些线程会被放到一个等待队列中进行排队。

实现两个线程交替打印1-100

尝试用两个线程交替打印1-100的数字,要求一个线程打印奇数,另一个线程打印偶数,并且打印数字从小到大依次递增。

该题目主要考察的就是线程的同步和互斥。

但如果只有同步和互斥是无法满足题目要求的。

鉴于此,这里还需要定义一个flag变量,该变量的初始值设置为true。

代码如下:

int main()
{int n = 100;
	mutex mtx;
	condition_variable cv;
	bool flag = true;
	//奇数
	thread t1([&]{int i = 1;
		while (i<= 100)
		{	unique_lockul(mtx);
			cv.wait(ul, [&flag]()->bool{return flag; }); //等待条件变量满足
			cout<< this_thread::get_id()<< ":"<< i<< endl;
			i += 2;
			flag = false;
			cv.notify_one(); //唤醒条件变量下等待的一个线程
		}
	});
	//偶数
	thread t2([&]{int j = 2;
		while (j<= 100)
		{	unique_lockul(mtx);
			cv.wait(ul, [&flag]()->bool{return !flag; }); //等待条件变量满足
			cout<< this_thread::get_id()<< ":"<< j<< endl;
			j += 2;
			flag = true;
			cv.notify_one(); //唤醒条件变量下等待的一个线程
		}
	});

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

你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧


本文标题:C++11———线程库-创新互联
文章网址:http://www.tsicrk.com/article/dejses.html

其他资讯

让你的专属顾问为你服务

7.0245s