并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行。所以无论从微观还是从宏观来看,二者都是一起执行的。
C++11的标准库中提供了多线程库,使用时需要#include <thread>
头文件
1、线程的声明
//声明一个线程,名字是T,name是实现这个线程的函数名字 thread T(name); //调用一个线程 T.join(); //声明多个线程 thread T[3]; for(int i=0;i<3;i++) { T[i]=thread(name); } //调用多个线程 for(int i=0;i<3;i++) { T[i].join() }
2、传参
//声明一个线程 thread T(name,int x);
3、函数join()和detach()的区别
join()和detach()函数都是用来决定线程的运行方式。当使用join方式时,会阻塞当前代码,等待新线程完成退出后,才会继续向下执行主线程;
而使用detach方式则不会对当前代码造成影响,当前代码继续向下执行,创建的新线程同时并发执行.
举个例子,执行下面代码
#include<iostream> #include<thread> using namespace std; void thread2() { for (int i = 0; i < 4; i++) cout << "thread1" << endl; }//抢占式,线程1和线程2都在抢占cout这个资源 int main() { //声明一个线程 thread T(thread2); //启动线程的两种方法 //T.detach(); T.join(); for (int i = 0; i < 4; i++) cout << "thread2" << endl; system("pause"); return 0; }
使用T.join()时输出如下 使用T.detach()时输出如下
多线程可能是并发也可能是并行,但是在使用多线程的时候,要注意资源的互斥使用,比如上面的T.detach()方式,Thread1和Thread2都在争抢资源cout的使用,所以在输出的时候的会有点乱,
T.join()方式也一样,只不过上面的代码只有Thread1在运行,没有其它线程抢占cout的资源,所以看起来是有序的输出
上面的cout我们叫做临界资源,它一次只能供一个进程使用,为了实现让输出有序( 资源的互斥使用 ),我们要给互斥资源加锁
4、互斥锁的使用(mutex)
互斥锁可以实现资源的互斥使用,头文件是#include<mutex>
//声明一个锁 mutex m; //加锁 m.lock(); /* *临 *界 *区 */ //解锁 m.unlock(); //自解锁 { lock_guard<mutex> lg(m); } //使用自解锁是为了避免因临界区执行时出现意外,导致一直不能解锁的情况出现 //自解锁的作用域是在{ }内的,执行程序进入括号内会自动加锁,出了括号外会自动解锁
注意:要避免锁的频繁使用,因为它会有巨大的消耗
5、临界资源和临界区
临界资源:
多道程序系统中存在许多进程,它们共享各种资源,然而有很多资源一次只能供一个进程使用。一次仅允许一个进程使用的资源称为临界资源。许多物理设备都属于临界资源,如输入机、打印机、磁带机等。
临界区:
每个进程中访问临界资源的那段代码称为临界区。显然,若能保证诸进程互斥地进入自己的临界区,便可实现诸进程对临界资源的互斥访问。为此,每个进程在进入临界区之前,应先对欲访问的临界资源进行检查,看它是否正被访问。如果此刻该临界资源未被访问,进程便可进入临界区对该资源进行访问,并设置它正被访问的标志;如果此刻该临界资源正被某进程访问,则本进程不能进入临界区。
#include<iostream> #include<thread> #include<mutex>//互斥锁 using namespace std; mutex m; void thread2(int index) { for (int i = 0; i < 10; i++) { m.lock(); cout << index << " thread1 " << i << endl; m.unlock(); } }//抢占式,线程1和线程2都在抢占cout这个资源 int main() { //声明一个线程 thread T[4]; for (int i = 0; i < 4; i++) { T[i] = thread(thread2, i); } for (int i = 0; i < 4; i++) T[i].join(); for (int i = 0; i < 4; i++) cout << "thread2" << endl; system("pause"); return 0; }
6、原子操作
原子操作(atomic operation)意为“不可被中断的一个或一系列操作”,处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作。首先处理器会自动保证基本的内存操作的原子性。处理器保证从系统内存中读取或写入一个字节是原子的,意思是当一个处理器读取一个字节时,其他处理器不能访问这个字节的内存地址。
//定义某个数据为原子操作 atomic_ +数据类型 +数据名称 atomic_int x; atomic_double y; atomic_bool z;
#include<iostream> #include<thread> #include<mutex>//互斥锁 #include<atomic> using namespace std; //atomic_int sum = 0; int sum = 0; void thread2(int index) { for (int i = 0; i < 1000000; i++) { sum++; } }//抢占式,线程1和线程2都在抢占cout这个资源 int main() { int n = 20; while (n--) { sum = 0; //声明一个线程 thread T[4]; for (int i = 0; i < 4; i++) { T[i] = thread(thread2, i); } for (int i = 0; i < 4; i++) T[i].join(); cout << sum << endl; } system("pause"); return 0; }
没有把sum定义为原子操作 把sum定义为原子操作
所以i++,是不是原子操作?(答案为否)
系统实现这个操作分三步走
1、读内存到寄存器;
2、在寄存器中自增;
3、写回内存
举个例子:
有A、B两个线程对i进行操作,初始值i=0;假设开始时间t=0;
t=1: A线程将i的内存读到寄存器中,此时i=0
t=2:A线程在寄存器将i自增1,此时i=1
t=3:B线程将i的内存读到寄存器,此时i=0
t=4:A线程将寄存器中的i写回内存,i=1
t=5:B线程在寄存器将i自增1,此时i=1
t=6:B线程将寄存器中的i写回内存,i=1
i在A、B两个线程中各自增一次,但是最后i的值为1
如果是原子操作的话,i的值应为2