C++基础知识[4]——多线程并发&锁机制
并发的途径
多进程并发
在一个应用程序中使用并发的第一种方法,是将应用程序分为多个、独立的、单线程的进程,它们运行在同一时刻,就像你可以同时进行网页浏览和文字处理。这些独立的进程可以通过所有的常规的进程间通信渠道互相传递消讯息(信号、套接字、文件、管道等等)。
有一个缺点是这种进程之间的通信通常设置复杂,或是速度较慢,或两者兼备,因为操作系统通常在进程间提供了大量的保护,以避免一个进程不小心修改了属于另一个进程的数据;另一个缺点是运行多个进程所需的固有的开销:启动进程需要时间,操作系统必须投入内部资源来管理进程,等等。
当然,也并不全是缺点:操作系统在线程间提供的附加保护操作和更高级别的通信机制,意味着可以比线程更容易地编写安全的并发代码;使用独立的进程实现并发还有一个额外的优势——你可以在通过网络连接的不同的机器上运行的独立的进程。虽然这增加了通信成本,但在一个精心设计的系统上,它可能是一个提高并行可用行和提高性能的低成本方法。
多线程并发
并发的另一个途径是在单个进程中运行多个线程。线程很像轻量级的进程:每个线程相互独立运行,且每个线程可以运行不同的指令序列。但进程中的所有线程都共享相同的地址空间,并且从所有线程中访问到大部分数据——全局变量仍然是全局的,指针、对象的引用或数据可以在线程之间传递。虽然通常可以在进程之间共享内存,但这难以建立并且通常难以管理,因为同一数据的内存地址在不同的进程中也不尽相同。
共享的地址空间,以及缺少线程间的数据保护,使得使用多线程相关的开销远小于使用多个进程,因为操作系统有更少的簿记要做。但是,共享内存的灵活性是有代价的:如果数据要被多个线程访问,那么程序员必须确保当每个线程访问时所看到的数据是一致的。
使用并发的原因
为了关注点分离而使用并发
通过将相关的代码放在一起并将无关的代码分开,可以使你的程序更容易理解和测试,从而减少出错的可能性。你可以使用并发来分隔不同的功能区域,即使在这些不同功能区域的操作需要在同一时刻发生的穷况下;若不显式地使用并发,你要么被迫编写任务切换框架,要么在操作中主动地调用不相关的一段代码。
为了性能而使用并发
有两种方式为了性能使用并发。首先,也是最明显的,是将一个单个任务分成几部分且各自并行运行,从而降低总运行时间。这就是任务并行(task parallelism)。虽然这听起来很直观,但它可以是一个相当复杂的过程,因为在各个部分之间可能存在很多的依赖。区别可能是在过程方面——一个线程执行算法的一部分而另一个线程执行算法的另一个部分——或是在数据方面——每个线程在不同的数据部分上执行相同的操作。后一种方法被称为数据并行(data parallelism)。
使用并发来提升性能的第二种方法是使用可用的并行方式来解决更大的问题;与其同时处理一个文件,不如酌情处理2个或10个或20个。虽然这实际上只是数据并行的一种应用,通过对多组数据同时执行相同的操作,但还是有不同的重点。处理一个数据块仍然需要同样的时间,但在相同的时间内却可以处理更多的数据。当然,这种方法也存在限制,且并非在所有情况下都是有益的,但是这种方法所带来的吞吐量提升可以让一些新玩意变得可能,例如,如果图片的各部分可以并行处理,就能提高视频处理的分辨率。
C++中的并发编程
这里提一下我在 windows 下使用 mingw 时,使用的版本没有 thread 支持的问题(表面上来看就是没有 _GLIBCXX_HAS_GTHREADS 这一宏定义,如图)。
因为懒得改动本地的文件了,换到了一个 centOS 的服务器上跑代码。如果遇到此问题,解决方案见C++ Multithreading with MinGW 、Does MinGW-w64 support std::thread out of the box when using the Win32 threading model?
主线程等待子线程
#include <iostream>
#include <thread>
#include <unistd.h>
using namespace std;
void thread01()
{
for (int i = 0; i < 5; i++)
{
cout << "Thread 01 is working !" << endl;
sleep(1);
}
}
void thread02()
{
for (int i = 0; i < 5; i++)
{
cout << "Thread 02 is working !" << endl;
sleep(2);
}
}
int main()
{
thread task01(thread01);
thread task02(thread02);
task01.join();
task02.join();
for (int i = 0; i < 5; i++)
{
cout << "Main thread is working !" << endl;
sleep(2);
}
return 0;
}
[root@Teza cppDemo]# g++ -o threadDemo threadDemo.cpp -std=c++11 -lpthread
[root@Teza cppDemo]# ./threadDemo
Thread 02 is working !
Thread 01 is working !
Thread 01 is working !
Thread 02 is working !
Thread 01 is working !
Thread 01 is working !
Thread 02 is working !
Thread 01 is working !
Thread 02 is working !
Thread 02 is working !
Main thread is working !
Main thread is working !
Main thread is working !
Main thread is working !
Main thread is working !
带参数的子线程
#include <iostream>
#include <thread>
#include <unistd.h>
using namespace std;
void thread01()
{
for (int i = 0; i < 5; i++)
{
cout << "Thread 01 is working !" << endl;
sleep(1);
}
}
void thread02()
{
for (int i = 0; i < 5; i++)
{
cout << "Thread 02 is working !" << endl;
sleep(2);
}
}
int main()
{
thread task01(thread01);
thread task02(thread02);
task01.detach();
task02.detach();
for (int i = 0; i < 5; i++)
{
cout << "Main thread is working !" << endl;
sleep(2);
}
return 0;
}
[root@Teza cppDemo]# g++ -o threadDemo threadDemo.cpp -std=c++11 -lpthread
[root@Teza cppDemo]# ./threadDemo
Main thread is working !
Thread 02 is working !
Thread 01 is working !
Thread 01 is working !
Main thread is working !
Thread 02 is working !
Thread 01 is working !
Thread 01 is working !
Main thread is working !
Thread 02 is working !
Thread 01 is working !
Main thread is working !
Main thread is working !
多线程竞争
#include <iostream>
#include <thread>
#include <unistd.h>
#include <mutex>
using namespace std;
mutex mu;
int totalN = 10;
void thread01()
{
while (totalN > 0)
{
mu.lock(); //同步数据锁
cout << "thread 1 totalN = " << totalN << endl;
totalN--;
sleep(1);
mu.unlock(); //解除锁定
}
}
void thread02()
{
while (totalN > 0)
{
mu.lock(); //同步数据锁
cout << "thread 2 totalN = " << totalN << endl;
totalN--;
sleep(1);
mu.unlock(); //解除锁定
}
}
int main()
{
thread task02(thread02);
thread task01(thread01);
task01.join();
task02.join();
return 0;
}
[root@Teza cppDemo]# g++ -o threadDemo threadDemo.cpp -std=c++11 -lpthread
[root@Teza cppDemo]# ./threadDemo
thread 2 totalN = 10
thread 2 totalN = 9
thread 2 totalN = 8
thread 2 totalN = 7
thread 2 totalN = 6
thread 2 totalN = 5
thread 2 totalN = 4
thread 2 totalN = 3
thread 2 totalN = 2
thread 2 totalN = 1
thread 1 totalN = 0
锁机制
互斥锁/量(mutex)
提供了以排他方式防止数据结构被并发修改的方法。
互斥锁用于控制多个线程对他们之间共享资源互斥访问的一个信号量。也就是说是为了避免多个线程在某一时刻同时操作一个共享资源。例如线程池中的有多个空闲线程和一个任务队列。任何是一个线程都要使用互斥锁互斥访问任务队列,以避免多个线程同时访问任务队列以发生错乱。
在某一时刻,只有一个线程可以获取互斥锁,在释放互斥锁之前其他线程都不能获取该互斥锁。如果其他线程想要获取这个互斥锁,那么这个线程只能以阻塞方式进行等待。
#include <pthread.h>
pthread_mutex_init(pthread_mutex_t * mutex, const phtread_mutexattr_t * mutexattr);//动态方式创建锁,相当于new动态创建一个对象
pthread_mutex_destory(pthread_mutex_t *mutex)//释放互斥锁,相当于delete
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;//以静态方式创建锁
pthread_mutex_lock(pthread_mutex_t *mutex)//以阻塞方式运行的。如果之前mutex被加锁了,那么程序会阻塞在这里。
pthread_mutex_unlock(pthread_mutex_t *mutex)
int pthread_mutex_trylock(pthread_mutex_t * mutex);//会尝试对mutex加锁。如果mutex之前已经被锁定,返回非0;如果mutex没有被锁定,则函数返回并锁定mutex。该函数是以非阻塞方式运行。也就是说如果mutex之前已经被锁定,函数会返回非0,程序继续往下执行。
条件变量(condition)
可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。
条件锁就是所谓的条件变量,某一个线程因为某个条件为满足时可以使用条件变量使改程序处于阻塞状态。一旦条件满足以"信号量"的方式唤醒一个因为该条件而被阻塞的线程。最为常见就是在线程池中,起初没有任务时任务队列为空,此时线程池中的线程因为"任务队列为空"这个条件处于阻塞状态。一旦有任务进来,就会以信号量的方式唤醒一个线程来处理这个任务。
#include <pthread.h>
pthread_cond_init(pthread_cond_t * condtion, const phtread_condattr_t * condattr);//对条件变量进行动态初始化,相当于new创建对象
pthread_cond_destory(pthread_cond_t * condition);//释放动态申请的条件变量,相当于delete释放对象
pthread_cond_t condition = PTHREAD_COND_INITIALIZER;//静态初始化条件变量
pthread_cond_wait(pthread_cond_t * cond, pthread_mutex_t * mutex);//该函数以阻塞方式执行。如果某个线程中的程序执行了该函数,那么这个线程就会以阻塞方式等待,直到收到pthread_cond_signal或者pthread_cond_broadcast函数发来的信号而被唤醒。
//注意:pthread_cond_wait函数的语义相当于:首先解锁互斥锁,然后以阻塞方式等待条件变量的信号,收到信号后又会对互斥锁加锁。
//为了防止“虚假唤醒”,该函数一般放在while循环体中。例如
pthread_mutex_lock(mutex);//加互斥锁
while(条件不成立)//当前线程中条件变量不成立
{
pthread_cond_wait(cond, mutex);//解锁,其他线程使条件成立发送信号,加锁。
}
...//对进程之间的共享资源进行操作
pthread_mutex_unlock(mutex);//释放互斥锁
读写锁(reader-writer lock)
允许多个线程同时读共享数据,而对写操作是互斥的。
自旋锁(spin lock)
与互斥锁类似,都是为了保护共享资源。互斥锁是当资源被占用,申请者进入睡眠状态;而自旋锁则循环检测保持者是否已经释放锁。
#include <linux\spinlock.h>
spin_lock_init(spinlock_t *x);
spin_lock(x);//只有在获得锁的情况下才返回,否则一直"自旋"
spin_is_locked(x);//该宏用于判断自旋锁x是否已经被某执行单元保持(即被锁)
//注意:自旋锁适合于短时间的的轻量级的加锁机制。
C++简单运用
互斥锁
#include<stdlib.h>
#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
int sum;
pthread_mutex_t lock;
void* add1(void *sum)
{
int i, sum_temp = 0;
for(i = 0; i < 50; i++)
sum_temp += i;
pthread_mutex_lock(&lock);
*((int *)sum) += sum_temp;
printf("add1 complete\tsum = %d\n", *((int *)sum));
pthread_mutex_unlock(&lock);
pthread_exit(NULL);
}
void* add2(void *sum)
{
int i, sum_temp = 0;
for(i = 50; i < 100; i++)
sum_temp += i;
pthread_mutex_lock(&lock);
*((int *)sum) += sum_temp;
printf("add2 complete\tsum = %d\n", *((int *)sum));
pthread_mutex_unlock(&lock);
pthread_exit(NULL);
}
int main(void)
{
int i;
pthread_t ptid1, ptid2;
pthread_mutex_init(&lock, NULL);
sum = 0;
pthread_create(&ptid1, NULL, &add1, &sum);
pthread_create(&ptid2, NULL, &add2, &sum);
pthread_join(ptid1, NULL);
pthread_join(ptid2, NULL);
pthread_mutex_lock(&lock);
printf("sum %d\n", sum);
pthread_mutex_unlock(&lock);
pthread_mutex_destroy(&lock);
return 0;
}
[root@Teza cppDemo]# g++ -o mutexDemo mutexDemo.cpp -std=c++11 -lpthread
[root@Teza cppDemo]# ./mutexDemo
add2 complete sum = 3725
add1 complete sum = 4950
sum 4950
线程安全
在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。
保证线程安全的方式
- 给共享的资源加锁,保证每个资源变量每时每刻至多被一个线程占用。
- 让线程也拥有资源,不用去共享进程中的资源。如: 使用 threadlocal 可以为每个线程的维护一个私有的本地变量。
单例模式
单例模式指在整个系统生命周期里,保证一个类只能产生一个实例,确保该类的唯一性。
可分为懒汉式和饿汉式,两者之间的区别在于创建实例的时间不同:
- 懒汉式:指系统运行中,实例并不存在,只有当需要使用该实例时,才会去创建并使用实例。(这种方式要考虑线程安全)
- 饿汉式:指系统一运行,就初始化创建实例,当需要时,直接调用即可。(本身就线程安全,没有多线程的问题)
单例类的特点:
- 构造函数和析构函数为 private 类型,目的禁止外部构造和析构
- 拷贝构造和赋值构造函数为 private 类型,目的是禁止外部拷贝和赋值,确保实例的唯一性
- 类里有个获取实例的静态函数,可以全局访问
参考资料:
《C++ 并发编程》- 第1章 你好,C++的并发世界
c++的并发操作(多线程)
C++线程中的几种锁
C++ 线程安全的单例模式总结