北京高端网站制作公司,雨花区网站建设,优化设计一年级下册数学答案,一般网站空间多大前言 在c11之前涉及多线程的问题都是和平台相关的#xff0c;比如windows和linux都有一套自己的接口#xff0c;这使得代码的可移植性变差。C11中最重要的特性就是对线程进行了支持#xff0c;使得C在编程时不再依赖第三方库#xff0c;而且原子操作中还引入了原子类的概念…前言 在c11之前涉及多线程的问题都是和平台相关的比如windows和linux都有一套自己的接口这使得代码的可移植性变差。C11中最重要的特性就是对线程进行了支持使得C在编程时不再依赖第三方库而且原子操作中还引入了原子类的概念要使用标准库的线程必须包含thread头文件让我们一起来了解一下吧。
目录
1.thread类的简单介绍
2.线程函数参数
3.原子性操作库
4.lock_guard和unique_lock 4.1mutex的种类 4.2lock_guard 4.3unique_lock
5.例子 1.thread类的简单介绍 线程类 thread类成员函数说明 函数名功能thread() 构建一个线程对象没有关联任何线程函数即没有启动任何线程 thread(fn,args1,args2...)构建一个线程对象并关联线程函数fnargs1 args2...为线程函数的参数get_id()获取线程的idjionable()线程是否还在执行jionable代表一个正在执行中的线程jion()该函数调用后会阻塞住线程该线程执行结束后主线程继续执行。detach在创建线程对象后马上调用用于把被创建的线程与线程对象分离分离后的线程变为后台线程创建的线程“死活”就与主线程无关了 注意 1.线程是操作系统的一个概念线程对象可以关联一个线程用于控制线程以及获取线程的状态。 2.当创建一个线程对象后没有提供线程函数该对象实际没有对应任何线程。
#includethread
int main()
{std::thread t1;cout t1.get_id() endl;return 0;
} 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.函数对象 void ThreadFunc(int a)
{cout ThreadFunc a endl;
}
class TF
{
public:void operator()(){cout Thread3 endl;}
};
#includethread
int main()
{//线程函数为函数指针std::thread t1(ThreadFunc,10);//线程函数为lambda表达式thread t2([]() {cout Thread2 endl;});//线程函数为函数对象TF tf;thread t3(tf);t1.join();t2.join();t3.join();cout Mian thread endl;return 0;
} 4.thread类是防拷贝的不允许拷贝构造和赋值但是可以移动构造和移动赋值即将一个线程对象关联线程的状态转移给其他线程对象转移期间不影响线程的执行。 5.可以通过判断joinable函数判断线程是否有效如果在一下任意情况下则线程是无效的 采用无参构造函数构造的线程对象 线程对象的状态已经转移给其它对象 线程已经调用join或者detach结束 2.线程函数参数 线程函数的参数是以值拷贝的方式拷贝到线程空间的因此即使线程参数为引用类型在线程修改后也不能修改外部实参因为其实际引用的是线程栈中的拷贝而不是外部实参。
#includethread
void ThreadFuncT1(int x)
{x x 10;
}
void ThreadFuncT2(int* x)
{*x * 10;
}
int main()
{int a 10;//在线程函数中对a修改不会影响外部实参因为线程函数虽然是引用方式但是其实引用的是线程栈中的拷贝thread t1(ThreadFuncT1, a);t1.join();cout a endl;//如果想要通过形参改变外部实参时必须借助std::ref()函数thread t2(ThreadFuncT1, std::ref(a));t2.join();cout a endl;//地址拷贝thread t3(ThreadFuncT2, a);cout a endl;t3.join();//ruhoureturn 0;}注意如果是类的成员函数作为线程参数的时候必须将this指针作为线程函数参数。
3.原子性操作库 多线程最主要的问题就是数据共享带来的问题即线程安全的问题。如果共享数据都是只读的那么没有什么问题因为只读操作不会影响到数据更不会涉及对数据的修改所以所有的线程都会获得同样的数据。但是当一个或者多个线程要对数据进行修改共享数据的时候就会产生很多潜在的麻烦。比如
#includethread
#includeiostream
using namespace std;
unsigned long sum 0L;int main()
{cout Before joining,sum sum std::endl;int sum 0;thread t1([](int num) {for (int i 0; i num; i){sum;}}, 10000000);thread t2([](int num) {for (int i 0; i num; i){sum;}}, 10000000);t1.join();t2.join();cout After joining,sum sum std::endl;return 0;
} 这样加起来的结果是不准确的。 C98中会采取传统的方式解决可以对共享修改的数据进行加锁保护。
#includethread
#includeiostream
#includemutex
using namespace std;
unsigned long sum 0L;int main()
{std::cout Before joining,sum sum std::endl;int sum 0;mutex mu;thread t1([](int num) {for (int i 0; i num; i){mu.lock();//加锁sum;mu.unlock();//解锁}}, 10000000);thread t2([](int num) {for (int i 0; i num; i){mu.lock();//加锁sum;mu.unlock();//解锁}}, 10000000);t1.join();t2.join();std::cout After joining,sum sum std::endl;return 0;
} 虽然加锁可以解决这个问题但是代码的运行效率会很慢而且如果一个线程正在访问sum其他想要访问sum的线程就会被阻塞。会影响程序的运行效率。而且如果锁控制不好还容易造成死锁。 因此在C11中 引入了原子操作。所谓的原子操作即不可被中断的一个或者一系列的操作。C11引入原子操作类型使得线程间的数据同步变得非常高效。 注意使用以上的原子操作变量的时候必须添加头文件。atomic
int main()
{std::cout Before joining,sum sum std::endl;atomic_int sum 0;//原子类型的变量summutex mu;thread t1([](int num) {for (int i 0; i num; i){sum;}}, 10000000);thread t2([](int num) {for (int i 0; i num; i){sum;}}, 10000000);t1.join();t2.join();std::cout After joining,sum sum std::endl;return 0;
} 在C11中程序员不需要对原子类型的变量进行加锁解锁的操作线程能够对原子变量互斥访问。 更为普遍的是程序员可以使用atomic类型的模版定义出需要的任意原子类型。 atomicT t;//声明一个类型为T的原子变量类型 注意原子类型通常属于“资源型数据”多个线程只允许访问单个原子类型的拷贝因此在C11中原子类型只能从模版参数中进行构造不允许原子类型进行拷贝构造移动构造以及operator等为了防止意外标准库已经将atomic模版类中拷贝构造赋值运算符重载移动构造全都进行了删除。
int main()
{atomicint a1;//会报错atomicint a2(a1);atomicint a3;a3 a1;return 0;
}
4.lock_guard和unique_lock 在多线程的环境 如果想要保证某个变量的安全性只要将其设置为对应的原子类型就可以了高效又不容易出现死锁的问题。但是在某些情况下我们可能需要保证一段代码的安全性那么就需要通过锁的方式来进行控制。 比如一个线程对变量number进行加1 100次另外一个-1,100次每次加减以后都要输出number的值。要求最后number的值为1
int main()
{int number 1;mutex mu;thread t1([](int num) {for (int i 0; i num; i){mu.lock();number;cout number endl;mu.unlock();}}, 100);thread t2([](int num) {for (int i 0; i num; i){mu.lock();--number;cout number endl;mu.unlock();}}, 100);t1.join();t2.join();cout number endl;
} 上述代码存在一定的缺陷如果锁控制不好可能会造成死锁最常见的比如在锁的中间返回或者还没有解锁就抛异常了因此C11采用RALL的方式对锁进行封装即lock_guard和unique_lock。 4.1mutex的种类 在C11中mutex总共包含四种互斥量的种类 1.std::mutex C11提供的最基本的互斥量该类的对象之间不能互相拷贝也不能进行移动 mutex最常用的三个函数 函数声明函数功能lock() 加锁unlock解锁try_lock()尝试锁住互斥量如果互斥量被其它线程占有则当前线程也不会被阻塞 注意: 线程函数调用lock时可能会出现一下三种情况 1.如果该互斥量没有被锁住则线程会将该互斥量锁住直到调用unlock之前该线程都是有锁的。 2.如果当前互斥量被其他线程锁住则当前的调用线程会被阻塞。 3.如果当前互斥量已经被其他线程锁住则会造成死锁。 线程调用try_lock()时可能会发生一下三种情况 1.如果当前互斥量没有被其它线程占用则该线程锁住该互斥量直到该线程调用unlock解锁释放互斥量。 2.如果当前被其它线程占用则调用线程返回false并且不会阻塞。 3.如果当前互斥量被当前线程锁住则会造成死锁。 2.std::recursive_mutex 其允许同一个线程对互斥量多次上锁即递归锁来获得对互斥对象的对层所有权释放互斥量时需要调用与该锁层次深度相同的unlock(),除此之外stdrecursive_mutex的特性和std::mutex的特性大致相同。 3.std::timed_mutex 比std::mutex 多了两个成员try_lock_for(),try_lock_until()。 try_lock_for: 接受一个时间范围表示在这段时间范围之内如果线程如果没有获得锁则被阻塞住与std::mutex的try_lock不同try_lock如果被调用时没有获得锁直接返回false如果在此期间其它线程释放了锁则该线程可以获得对互斥量的锁如果超时即在指定的时间内没有获得锁则返回false。 try_lock_until() 接收一个时间点作为参数在指定的时间点未到来之前线程如果没有获得锁则被阻塞如果在此期间其它线程释放了锁则该线程可以获得互斥量的锁如果超时即在指定的饿时间内还没有获得锁则返回false。 4.std::recursive_timed_mutex 4.2lock_guard std::lock_guard时C中定义的模版类定义如下 templateclass Mutexclass lock_guard{public://构造之前mu还未加锁explicit lock_guard(Mutex mu):_mu(mu){_mu.lock();}// 在构造lock_gard时_Mtx已经被上锁此处不需要再上锁lock_guard(_Mutex _Mtx, adopt_lock_t): _MyMutex(_Mtx){}//析构函数进行解锁~lock_guard(){_mu.unlock();}//不允许拷贝构造和赋值lock_guard(const Mutex mu) delete;Mutex operator()(const Mutex mu) delete;private:Mutex _mu;}; 通过上述代码我们可以看到lock_guard类模板主要通过RAII的方式对其管理的互斥量进行封装在需要加锁的地方只需要对上述介绍的任意互斥的体实例化一个lock_guard,调用构造函数成功上锁出作用域前lock_guard 对象要被销毁调用析构函数完成解锁的过程。因为析构函数是对象声明周期结束以后自动调用的所以可以自动解锁这样就有效的避免了死锁问题。 lock_guard的缺陷是太过单一用户没有办法对该锁进行控制因此C11又提供unique_lock. 4.3unique_lock 与lock_gard类似unique_lock类模板也采用 RAII的方式对锁将进行封装并且也是以独占所有权的方式管理mutex对象的上锁和解锁操作即其对象之间不能发生拷贝在构造或者移动move赋值时unique_lock对象需要传递一个Mutex对象作为它的参数新创建的unique_lock对象负责传入一个Mutex对象的上锁和解锁操作。使用以上类型互斥量实例化unique_lock的对象时自动调用构造函数上锁unique_lock对象销毁自动调用析构函数对锁进行解锁。可以很方便防止死锁问题。 与lock_gard不同的是unique_lock更加的灵活提供了更多的成员函数 1.上锁/解锁操作lock、try_lock,try_lock_for,try_lock_until和unlock。 2.修改操作移动赋值交换swap与另一个unique_lock对象互换所管理的互斥量所有权释放release返回它所管理的互斥量对象的指针并释放所有权 3.获取属性owns_lock(返回当前对象是否上锁)operator bool与owns_lock()功能相同mutex返回当前unique_lock所管理的互斥量的指针。
5.例子 支持两个线程交替打印一个打印奇数一个打印偶数。 主要是依靠条件变量加互斥量来实现的。 #includecondition_variable
int main()
{int n 1000;//创建两个锁和两个条件变量mutex mu1;mutex mu2;condition_variable cv1;condition_variable cv2;thread t1([]() {for (int i 0; i n; i2){unique_lockmutexmut1(mu1);cout i endl;//打印之后通知另一个进程然后自己也等待通知cv2.notify_one();cv1.wait(mut1);}});thread t2([]() {for (int i 1; i n; i 2){unique_lockmutexmut2(mu2);//等待进程的通知cv2.wait(mut2);cout i endl;//打印完后通知另一个进程cv1.notify_one();}});t1.join();t2.join();return 0;
}