网站开发php学校,淘宝客导购网站,莒南县网站建设,兰州大学网页与网站设计目录
一#xff0c;线程库简介
二#xff0c;线程库简单使用
2.1 传函数指针
编辑 2.2 传lamdba表达式
2.3 简单综合运用
2.4 线程函数参数
三#xff0c;线程安全问题
3.1 为什么会有这个问题#xff1f;
3.2 锁
3.2.1 互斥锁
3.2.2 递归锁
3.3 原子操作
3…目录
一线程库简介
二线程库简单使用
2.1 传函数指针
编辑 2.2 传lamdba表达式
2.3 简单综合运用
2.4 线程函数参数
三线程安全问题
3.1 为什么会有这个问题
3.2 锁
3.2.1 互斥锁
3.2.2 递归锁
3.3 原子操作
3.3.1 原子操作变量
3.3.2 atomic模板
五lock_guard和unique_lock
5.1 可能发生的情况
5.2 lock_guard
5.3 unique_lock
六两个线程交替打印一个打印奇数一个打印偶数 一线程库简介
在Linux中我们可以使用用户层的线程接口来实现线程操作但是Linux下的接口无法在Windows下使用因为Linux支持了POSIX线程标准但是Windows没有支持它搞了一套属于它自己的线程标准。所以在C11之前涉及到多线程问题的代码可移植性比较差
所以C11中最重要的特性就是对线程进行支持了使得C在并行编程时不需要依赖第三方库而且在原子操作中还引入了原子类得概念。
CLinux和Windows下都可以支持多线程程序 -- 条件编译
#ifdef _WIN32CreateThread...
#elsepthread_create...
#endif
下面是C中常用的线程函数的介绍
①thread()构造一个线程对象没有关联任何线程函数即没有启动任何线程
②thread(fn, args1, args2...)构造一个线程对象并使其关联函数fnargs是fn线程函数的参数
③get_id获取线程id
④join()类似malloc或new之后需要手动free或者deletejoin被调用后会阻塞主先当该线程结束后主线程继续执行
⑤detach()一般在创建线程对象后马上调用用于把创建出来的线程与线程对象分离开分离后的线程变为后台线程在这之后后台线程就与主线程无关
二线程库简单使用
线程函数一般情况下可以按照三种方式提供①函数指针 ②lamdba表达式 ③函数对象
2.1 传函数指针
先来个最简单的线程使用如下代码
void Func(int n, int num)
{for (int i 0; i n; i){cout num: i endl;}cout endl;
}void main()
{thread t1(Func, 10, 1);thread t2(Func, 20, 2);t1.join();t2.join();
}
注由于是多线程同时运行所以打印结果会比较乱属于正常情况 2.2 传lamdba表达式
我们还可以直接用lamdba表达式传给线程如下代码
void main()
{int n1, n2;cin n1 n2;thread t1([n1](int num){for (int i 0; i n1; i){cout num : i endl;}cout endl;}, 1);thread t2([n1](int num){for (int i 0; i n1; i){cout num : i endl;}cout endl;}, 2);t1.join();t2.join();
}
2.3 简单综合运用
但是单独用lamdba表达式传过去的话代码会重复所以也可以用循环把多个线程放进数组里如下代码
void main()
{int m;cin m;//要求m个线程分别打印nvectorthread vthds(m);//把线程作为一个对象放到容器里去size_t n;for (size_t i 0; im; i){size_t n 10;vthds[i] thread([i, n]() {for (int j 0; j n; j){cout this_thread::get_id() : j endl; //用this_thread命名空间打印线程idthis_thread::sleep_for(chrono::seconds(1)); //每打印一次休眠一秒}cout endl;});//cout vthds[i].get_id() endl;}for (auto t : vthds) //库线程禁掉了拷贝赋值所以这里用引用{t.join();}}
2.4 线程函数参数
线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的因此即使线程参数为引用类型在线程中修改后也不能修改外部实参因为其实际引用的是线程栈中的拷贝而不是外部实参
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;
}
三线程安全问题
3.1 为什么会有这个问题
先看下面代码
unsigned long sum 0;void fun(size_t num)
{for (size_t i 0; i num; i)sum;
}
int main()
{cout Before joining,sum sum std::endl;thread t1(fun, 10000000);thread t2(fun, 10000000);t1.join();t2.join();cout After joining,sum sum std::endl;return 0;
} 从上面的运行结果可以看出两个线程都对一个全局变量进行相加时结果与我们预期的不同。
多线程最主要的问题就是访问临界资源带来的线程安全问题如果所有临界资源都是只读的那就是线程安全的因为数据不会被修改但是当一个或多个线程要修改临界资源时如果没有对临界资源给予相关的保护措施比如那么就是线程不安全的因为我们说一条汇编语句是原子的但是等操作经过编译器编译后会编程三条汇编语句那么操作就不是原子的具体的底层细节我们到linux系统编程部分再讲。
3.2 锁
关于锁的概念我们到Linux系统编程再进行讲解这里只展示用法
3.2.1 互斥锁
listint lt;
int x 0;
mutex mtx;
void Func2(int n)
{//并行for (int i 0; i n; i){mtx.lock(); x;lt.push_back(x);mtx.unlock();cout i endl;cout i endl;cout i endl;}//串行 -- 如果我们只是没有push_back等其他操作时可以看出并行并不比串行快并行会有大量的加锁和解锁而且由于计算太快了还有切换线程的切换上下文的消耗//但是当我们后再push_back插到链表后还是并行快而且随着多临界资源的访问消耗变大时并行的优势更大/*mtx1.lock(); for (int i 0; i n; i){x;lt.push_back(x);cout i endl;cout i endl;cout i endl;}mtx1.unlock();*/
}void main()
{int n 2000000;size_t begin clock();thread t1(Func2, 10000); //每个线程都有自己独自的栈这个栈在共享区中由库提供thread t2(Func2, 20000);t1.join();t2.join();size_t end clock();cout (end - begin) endl;cout x endl;
} 上面的有点复杂我们直接传lamdba
void main()
{int n 20000;int x 0;mutex mtx1;size_t begin clock();thread t1([, n](){mtx.lock();for (int i 0; i n; i){x;}mtx1.unlock();});thread t2([, n]() {mtx1.lock();for (int i 0; i n; i){x;}mtx1.unlock();});t1.join();t2.join();size_t end clock();cout (end - begin) endl;cout x endl;
}
3.2.2 递归锁
recursive_mutex mtx2;
void Func3(int n)
{if (n 0)return;mtx2.lock();x;Func3(n - 1);mtx2.unlock(); //当普通锁的解锁在这里定义的时候会造成死锁递归锁解决在递归场景中的死锁问题
}
void main()
{thread t1(Func3, 1000); thread t2(Func3, 1000);t1.join();t2.join();cout x endl;
}
3.3 原子操作
虽然加锁可以解决线程安全问题但是加锁有一个缺陷就是只要一个线程在对sum其他线程就必须阻塞等待一旦线程很多的情况下就会影响总体的效率而且如果控制不好容易造成死锁问题。
因此C11引入了原子操作也叫无锁操作需要用到头文件#includeatomic
(该操作也是用到了一个叫做CAS的同步原语就是当我写一个数的时候如果这个数已经改变过了那我就不写了如果每改变就写)
3.3.1 原子操作变量
#include thread
#include atomicatomic_long sum{ 0 }; //这只是其中一个原子操作变量void fun(size_t num)
{for (size_t i 0; i num; i)sum ; // 原子操作
}
int main()
{cout Before joining, sum sum std::endl;thread t1(fun, 1000000);thread t2(fun, 1000000);t1.join();t2.join();cout After joining, sum sum std::endl;return 0;
}
3.3.2 atomic模板
atmoicT t; //声明一个类型为T的原子类型变量t 注意原子类型通常属于“资源型”数据多个线程只能访问单个原子类型的拷贝因此在C11中原子类型只能从其模板参数中进行改造不允许原子类型进行拷贝构造移动构造和operator等所以为了防止意外标准库直接将atomic模板类中的拷贝构造移动构造赋值运算符重载全部删除了
#include atomic
int main()
{atomicint a1(0);//atomicint a2(a1); // 编译失败atomicint a2(0);//a2 a1; // 编译失败return 0;
}
下面是atomic模板类的演示代码
void Func5(int n)
{cout x endl;
}
void main()
{int n 200;//atomicint x 0; 三种写法都一样都是去调用构造函数//atomicint x {0};atomicint x{ 0 };thread t1([, n]() {for (int i 0; i n; i){x;}});thread t2([, n]() {for (int i 0; i n; i){x;}});Func5(x.load());t1.join();t2.join();cout x endl;
}
五lock_guard和unique_lock
5.1 可能发生的情况
在多线程中如果想要保证临界资源的安全性可以将其设置为原子类型避免死锁也可以通过加锁保证一段代码的安全性。但是还有一种情况那就是如果在加锁和解锁中间抛异常了那么代码会直接跳到捕获的地方导致无法执行unlock来释放锁。因此C11采用RAII的方式对锁进行了封装即lock_guard和unique_lock
5.2 lock_guard
templateclass Lock
class LockGuard
{
public:LockGuard(Lock lk):_lk(lk){_lk.lock();}~LockGuard(){_lk.unlock();}
private:Lock _lk; //三类成员必须在初始化列表初始化引用const和没有默认构造的成员变量
};
void Func4(int n)
{for (int i 0; i n; i){try{//把锁交给对象构造函数时加锁析构函数解锁 -- RAII//mtx.lock();//LockGuardmutex lock(mtx); // -- 我们自己实现的lock_guardmutex lock(mtx); // -- 库里的x;//...抛异常if (rand() % 3 0){throw exception(抛异常);}mtx.unlock();}catch (const exception e){cout e.what() endl;}}
}
void main()
{thread t1(Func4, 100);thread t2(Func4, 100);t1.join();t2.join();
} 通过上述代码可以看到lock_guard类模板通过RAII的方式对其管理的互斥量进行了封装在需要加锁的地方生成一个lock_guard调用构造函数自动上锁然后出作用域时通过析构函数自动解锁
5.3 unique_lock
lock_guard的缺陷太单一用户没办法对其进行控制因此C11还提供了unique_lock.
unique_lock与lock_guard类似也是采用RAII进行封装但是unique_lock对象需要传一个mutex对象作为参数对传入的锁进行上锁和解锁操作。而且它还提供了更多的成员函数
①上锁/解锁操作locktry_locktry_lock_fortry_lock_until和unlock
②修改操作移动赋值交换swap与另一个unique_lock对象交换所管理的互斥量释放release返回它所管理的互斥量的指针释放所有权
③获取属性owns_lock返回当前对象是否上了锁operator bool(),mutex返回当前unique_lock所管理互斥量的指针
六两个线程交替打印一个打印奇数一个打印偶数
//C也支持条件变量不是线程安全的所以要配合锁使用
//题目两个线程交替打印一个线程打印奇数一个打印偶数
//condition_variable::wait 当前进程会进行阻塞直到被唤醒和linux一样在阻塞之前会进行解锁防止死锁问题然后唤醒后重新申请锁
//文档中的 wait_for和wait_until表示等待的时间notify_one和notify_all表示唤醒一个线程和唤醒所有线程
#includecondition_variable
void main39()
{condition_variable cv;int n 100;int x 1;//如何做到t1和t2不管谁抢到锁都保证t1先运行t2阻塞 -- t1不等待t2有等待这样t1获得了锁t2就等着t2获得了锁但是wait释放锁t1获得锁//如何防止一个线程不断申请锁释放锁不断运行 t1打印的时候释放锁并且通知t2t2重新获得锁但是t1没停下来又等待了t1又获得了锁 -- t1, if(x%2 0) cv.wait(lock); t2,if (x % 2 ! 0) cv.wait(lock);//为什么要防止一个线程连续打印 -- 假设t1先获取到锁t2后获取到锁t2就阻塞在锁上面 -- t1打印奇数xx变成偶数 -- t1 notift唤醒但是没有线程wait因为t1有锁t2没有锁所以在阻塞等待 -- t1解锁t1时间片到了切出去了 // -- t2获取到锁打印notify但是没有线程等待t2再出作用域解锁 -- t2的时间片充裕会比t1先获得锁所以如果没有条件控制就会导致t2连续打印thread t1([, n]() {while(1){unique_lockmutex lock(mtx);if (x 100)break;//if(x%2 0) //奇数// cv.wait(lock);cv.wait(lock, [x]() {return x % 2 ! 0; });cout this_thread::get_id() : x endl;x;cv.notify_one();}});thread t2([, n]() {while(1){unique_lockmutex lock(mtx);if (x 100)break;//if (x % 2 ! 0) //偶数// cv.wait(lock);cv.wait(lock, [x](){return x % 2 0; });cout this_thread::get_id() : x endl;x;cv.notify_one();}});t1.join();t2.join();
}unsigned long sum 0;void fun(size_t num)
{for (size_t i 0; i num; i)sum;
}
int main()
{cout Before joining,sum sum std::endl;thread t1(fun, 10000000);thread t2(fun, 10000000);t1.join();t2.join();cout After joining,sum sum std::endl;return 0;
}