所谓线程安全不是指线程的安全,而是指内存的安全。线程是由进程所承载,所有线程均可访问进程的上下文,意味着所有线程均可访问在进程中的内存空间,这也是线程之间造成问题的潜在原因。当多个线程读取同一片内存空间(变量、对象等)时,不会引起线程安全问题。但是当多个线程对同一片内存空间进行写操作时,就需要考虑内存安全问题。
#include
#include
using namespace std;
int counter = 0;
void func()
{
for (int i = 0; i < 10000; ++i)
{
std::this_thread::sleep_for(std::chrono::milliseconds(1)); // 休眠1ms
++counter; // 写入操作:counter自加
}
}
int main()
{
std::thread t1 = std::thread(func);
std::thread t2 = std::thread(func);
t1.join();
t2.join();
cout << "counter: " << counter << endl;
return 0;
}
上述代码,预期结果是counter输出20000,因为两个线程均会进入循环,counter各自自加10000次。实际输出效果如下:
counter: 19989
counter: 19992
两次输出结果均不相同,且均不等于预期的20000。这就线程不安全导致的BUG。
为了解决上述线程安全问题,就需要线程锁,C和C++最常用的锁包括互斥锁、条件锁、自旋锁和读写锁等。其中最基本的是互斥锁,其他的锁都是基于互斥锁实现。所以锁的功能越强大,性能就越低。
互斥锁(mutex),也成为互斥量,就是保证多线程共享同一个互斥量,然后让线程之间。当一个线程对共享数据操作时,利用互斥锁进行上锁,其他线程在尝试再次上锁时,发现已上锁就会睡眠等待。等到互斥锁解开,才能再次上锁进而继续操作,从而保护共享数据的安全。下面利用互斥锁,解决上述BUG。
#include
#include
#include
int counter = 0;
std::mutex mutex;
void func()
{
for (int i = 0; i < 10000; ++i)
{
std::this_thread::sleep_for(std::chrono::milliseconds(1));
mutex.lock();
++counter;
mutex.unlock();
}
}
int main()
{
std::thread t1 = std::thread(func);
std::thread t2 = std::thread(func);
t1.join();
t2.join();
std::cout << "counter: " << counter << std::endl;
return 0;
}
输出结果如下:
counter: 20000
counter: 20000
两次运行输出均为20000,与预期结果相同,证明加入互斥锁后解决线程安全问题。
RAII(Resource Acquisition Is Initialization)是一种C++编程技术,它将必须在使用前请求的资源(例如:分配的堆内存、执行线程、打开的套接字、打开的文件、锁定的互斥体、磁盘空间、数据库连接等——任何存在受限供给中的事物)的生命周期与一个对象的生存周期相绑定。 RAII保证资源可用于任何会访问该对象的函数。它亦保证所有资源在其控制对象的生存期结束时,以获取顺序的逆序释放。类似地,若资源获取失败(构造函数以异常退出),则为已构造完成的对象和基类子对象所获取的所有资源,会以初始化顺序的逆序释放。这有效地利用了语言特性以消除内存泄漏并保证异常安全。
#include
#include
#include
int counter = 0;
std::mutex mutex;
void func()
{
for (int i = 0; i < 10000; ++i)
{
std::this_thread::sleep_for(std::chrono::milliseconds(1));
std::lock_guard<std::mutex> lock(mutex);
++counter;
}
}
int main()
{
std::thread t1 = std::thread(func);
std::thread t2 = std::thread(func);
t1.join();
t2.join();
std::cout << "counter: " << counter << std::endl;
return 0;
}
lock_guard使用的就是RAII编程技巧,在方法结束的时候,局部变量std::lock_guard
#include
#include
#include
int counter = 0;
pthread_mutex_t mutex;
void *func(void *val)
{
for (int i = 0; i < 10000; ++i)
{
usleep(1 * 1000);
pthread_mutex_lock(&mutex);
++counter;
pthread_mutex_unlock(&mutex);
}
}
int main()
{
pthread_mutex_init(&mutex, NULL);
pthread_t t1;
pthread_t t2;
pthread_create(&t1, NULL, func, NULL);
pthread_create(&t2, NULL, func, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_mutex_destroy(&mutex);
printf("counter: %d\n", counter);
return 0;
}
输出如下:
counter: 20000
输出也是预期的20000。
条件锁,即条件变量,可以用于阻塞一个线程或多个线程,直到另一个线程修改共享变量(条件)后,通知条件锁。条件锁的实现的基于互斥锁和条件变量共同实现的,也可以实现线程同步。
C++条件锁依赖于std::unique_lock实现,条件锁需要满足某个条件才会进行加锁,否则将等待直到条件满足,下面举例无法等待成功情况。
#include
#include
#include
#include
#include
std::mutex mutex;
std::condition_variable condition;
std::string data;
bool ready = false;
void func()
{
std::unique_lock<std::mutex> locker(mutex);
condition.wait(locker, []{return ready;}); // 等待ready为true,再上锁
std::cout << "condition locker finish waiting." << std::endl;
data += " after waiting locker.";
}
int main()
{
std::thread thread(func);
data = "Example data";
thread.join();
std::cout << data << std::endl;
return 0;
}
程序无法退出,且无法输出,因为thread在join后,一直没有等待ready置为true,所以停止在func函数的wait调用中无法退出。
若不想一直等待条件符合,可以使用wait_for函数,等一段时间后若条件达成则返回true,未达成则返回false。下面实现例子
#include
#include
#include
#include
#include
#include
using namespace std::chrono_literals;
std::mutex mutex;
std::condition_variable condition;
std::string data;
bool ready = false;
void func()
{
std::unique_lock<std::mutex> locker(mutex);
if( condition.wait_for(locker, 1s, []{return ready;})) {
std::cout << "condition locker finish waiting." << std::endl;
data += " after waiting locker.";
}
}
int main()
{
std::thread thread(func);
data = "Example data";
thread.join();
std::cout << data << std::endl; // 输出: Example data
return 0;
}
线程等待1s后,未收到ready置为true,输出“Example data”。
#include
#include
#include
#include
#include
#include
using namespace std::chrono_literals;
std::mutex mutex;
std::condition_variable condition;
std::string data;
bool ready = false;
void func()
{
std::unique_lock<std::mutex> locker(mutex);
if( condition.wait_for(locker, 1s, []{return ready;})) {
std::cout << "condition locker finish waiting." << std::endl;
data += " after waiting locker.";
}
}
int main()
{
std::thread thread(func);
data = "Example data";
{
// 线程安全
std::lock_guard<std::mutex> locker(mutex);
ready = true;
}
condition.notify_one();
thread.join();
std::cout << data << std::endl;
return 0;
}
输出如下:
condition locker finish waiting.
Example data after waiting locker.
当thread线程开启后,主线程将ready置为true,并通过notify_one通知线程。thread的条件锁等待成功,往下运行输出信息与预期相同。
notify_one和notify_all,区别在于notity_one只会唤醒一个线程,notify_all会唤醒所有等待的线程。
#include
#include
#include
#include
#include
char data[128] = "Example data";
pthread_mutex_t mutex;
pthread_cond_t condition;
void *func(void *val)
{
pthread_cond_wait(&condition, &mutex);
printf("func condition locker finish waiting.\n");
char *temp = "after func waiting locker.";
strcat(data, temp);
}
int main()
{
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&condition, NULL);
pthread_t t1;
pthread_create(&t1, NULL, func, NULL);
usleep(100 * 1000); // 等待100ms,防止线程未开始等待就发出条件信号
pthread_cond_signal(&condition);
pthread_join(t1, NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&condition);
printf("%s\n", data);
return 0;
}
输出如下:
func condition locker finish waiting.
Example dataafter func waiting locker.
读写锁就是区分线程中的读操作和写操作。由于线程中只执行读操作,对临界区时不构成影响,所以区分读操作和写操作,就可以程序的效率。当线程对读写锁加读操作锁时,其他线程也需要读时,可以直接获得独写锁并读取临界区资源;而当其他线程在读写锁加锁的情况下进行写操作锁的加锁,需要等待读写锁释放才能进行获得锁,且写操作锁只能由一个线程拥有。读写锁也被成为共享-独占锁,因为在读操作,读写锁时共享给读操作的,而在写操作时就是独占于一个线程。
在C++14起加入了std::shared_lock和std::shared_timed_mutex可共享锁的模板,结合std::unique_lock进行读写锁的实现。
#include
#include
#include
#include
int i = 100;
std::shared_timed_mutex mutex;
void funcRead()
{
std::shared_lock<std::shared_timed_mutex> locker(mutex);
std::cout << std::this_thread::get_id() << "funcRead: " << i << std::endl;
std::cout << std::this_thread::get_id() << "end funcRead" << std::endl;
}
void funcWrite()
{
std::unique_lock<std::shared_timed_mutex> locker(mutex);
std::cout << std::this_thread::get_id() << "funcWrite: " << ++i << std::endl;
}
int main()
{
std::thread t1Read(funcRead);
std::thread t2Read(funcRead);
std::thread t3Write(funcWrite);
t1Read.join();
t2Read.join();
t3Write.join();
return 0;
}
3funcRead: 100
2funcRead: 100
2end funcRead
3end funcRead
4funcWrite: 101
由于线程的调度是内核实现,上述实现为简单实现,线程的开启可能有所不同。如果需要控制线程的开启时机可以加入条件锁进行线程函数的处理。上述输出能证明再两个funcRead线程中,互斥量是共享的,而写锁需要等到读锁释放才能获得。
#include
#include
int i = 100;
pthread_rwlock_t rwlock;
void *funcRead(void *val)
{
pthread_rwlock_rdlock(&rwlock);
printf("thread 0x%x: %d\n", (unsigned int)pthread_self(), i);
pthread_rwlock_unlock(&rwlock);
printf("end funcRead\n");
}
void *funcWrite(void *val)
{
pthread_rwlock_wrlock(&rwlock);
printf("thread 0x%x: %d\n", (unsigned int)pthread_self(), ++i);
pthread_rwlock_unlock(&rwlock);
}
int main()
{
pthread_t t1read;
pthread_t t2read;
pthread_t twrite;
pthread_rwlock_init(&rwlock, NULL);
pthread_create(&t1read, NULL, funcRead, NULL);
pthread_create(&t2read, NULL, funcRead, NULL);
pthread_create(&twrite, NULL, funcWrite, NULL);
pthread_join(t1read, NULL);
pthread_join(t2read, NULL);
pthread_join(twrite, NULL);
pthread_rwlock_destroy(&rwlock);
return 0;
}
thread 0xf7d9f700: 100
end funcRead
thread 0xf759e700: 100
end funcRead
thread 0xf6d9d700: 101
C语言实现主要利用POSIX标准的pthread_rwlock_t的读写锁API实现,输出与C++相类似。
自旋锁与互斥锁是类似的,实现方式也是基于互斥锁实现(上文提到,所有复杂的锁都是基于互斥锁实现的)。区别在于自旋锁是不断循环并测试锁的状态,这样会一直占用cpu资源。
C++没有提供自选锁的API,所以下面基于POSIX在C语言中实现自旋锁。
#include
#include
#include
int counter = 0;
pthread_spinlock_t spinlock;
void *func(void *val)
{
for (int i = 0; i < 10000; ++i)
{
usleep(1 * 1000);
pthread_spin_lock(&spinlock);
++counter;
pthread_spin_unlock(&spinlock);
}
}
int main()
{
pthread_spin_init(&spinlock, 0);
pthread_t t1;
pthread_t t2;
pthread_create(&t1, NULL, func, NULL);
pthread_create(&t2, NULL, func, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_spin_destroy(&spinlock);
printf("counter: %d\n", counter);
return 0;
}
输出和互斥锁的实现一样:
counter: 20000
将互斥锁和自旋锁的线程函数修改为以下状况,让线程一直等待锁。
/* 互斥锁 */
void *func(void *val)
{
for (int i = 0; i < 10000; ++i)
{
usleep(1 * 1000);
pthread_mutex_lock(&mutex);
++counter;
// pthread_mutex_unlock(&mutex);
}
}
/* 自旋锁 */
void *func(void *val)
{
for (int i = 0; i < 10000; ++i)
{
usleep(1 * 1000);
pthread_spin_lock(&spinlock);
++counter;
// pthread_spin_unlock(&spinlock);
}
}
互斥锁资源消耗情况:
自旋锁资源消耗情况:从两张图可以看出,当互斥锁等待锁时会将资源交还给CPU,然后等待下次内核调用在尝试获得锁。而自旋锁会一直占用CPU,也就是死循环。所以互斥锁是休眠等待(sleep-waiting),而自旋锁是忙等待(busy-waiting)。
递归锁就是在同一个线程中,不解锁的情况下,可以重复获得同一个递归锁,而不导致死锁。互斥锁为非递归锁。
#include
#include
#include
std::recursive_mutex mutex;
std::string shared;
void func1()
{
std::lock_guard<std::recursive_mutex> locker(mutex);
shared = "func1";
std::cout << "thread id: " << std::this_thread::get_id() << " func1: shared variable: " << shared << std::endl;
}
void func2()
{
std::lock_guard<std::recursive_mutex> locker(mutex);
shared = "func2";
std::cout << "thread id: " << std::this_thread::get_id() << " func2: shared variable: " << shared << std::endl;
func1();
std::cout << "thread id: " << std::this_thread::get_id() << " back in func2, shared variable: " << shared << std::endl;
}
int main()
{
std::thread t1(func1);
std::thread t2(func2);
t1.join();
t2.join();
}
输出如下:
thread id: 2 func1: shared variable: func1
thread id: 3 func2: shared variable: func2
thread id: 3 func1: shared variable: func1
thread id: 3 back in func2, shared variable: func1
在func2中,在线程t2重新上锁,递归锁再次获得锁,这就是递归锁的特性。
如果将递归锁换成非递归锁,程序如下:
#include
#include
#include
// std::recursive_mutex mutex;
std::mutex mutex;
std::string shared;
void func1()
{
// std::lock_guard locker(mutex);
std::lock_guard<std::mutex> locker(mutex);
shared = "func1";
std::cout << "thread id: " << std::this_thread::get_id() << " func1: shared variable: " << shared << std::endl;
}
void func2()
{
// std::lock_guard locker(mutex);
std::lock_guard<std::mutex> locker(mutex);
shared = "func2";
std::cout << "thread id: " << std::this_thread::get_id() << " func2: shared variable: " << shared << std::endl;
func1();
std::cout << "thread id: " << std::this_thread::get_id() << " back in func2, shared variable: " << shared << std::endl;
}
int main()
{
std::thread t1(func1);
std::thread t2(func2);
t1.join();
t2.join();
}
输出如下:
thread id: 2 func1: shared variable: func1
thread id: 3 func2: shared variable: func2
当线程t2运行到func1时无法获得锁,发生死锁的情况。这就是递归锁和非递归锁的区别。
#include
#include
#include
char shared[12] = {0};
pthread_mutex_t mutex;
void func1()
{
pthread_mutex_lock(&mutex);
strcpy(shared, "func1");
printf("thread id: 0x%x, func1 shared variable: %s\n", (unsigned int)pthread_self(), shared);
pthread_mutex_unlock(&mutex);
}
void func2()
{
pthread_mutex_lock(&mutex);
strcpy(shared, "func2");
printf("thread id: 0x%x, func2 shared variable: %s\n", (unsigned int)pthread_self(), shared);
func1();
printf("thread id: 0x%x, back in func2, shared variable: %s\n", (unsigned int)pthread_self(), shared);
pthread_mutex_unlock(&mutex);
}
int main()
{
pthread_t t1;
pthread_t t2;
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&mutex, &attr);
pthread_create(&t1, NULL, (void *)func1, NULL);
pthread_create(&t2, NULL, (void *)func2, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_mutex_destroy(&mutex);
return 0;
}
输出如下:
thread id: 0xf7d9f700, func1 shared variable: func1
thread id: 0xf759e700, func2 shared variable: func2
thread id: 0xf759e700, func1 shared variable: func1
thread id: 0xf759e700, back in func2, shared variable: func1
线程安全: 多线程同时操作同一片临界区时,可能会导致临界区资源改变。所以临界区资源在多线程操作时需要做保护,否则可能导致操作结果错误。解决线程安全的方法就是对资源进行加线程锁。
互斥锁: 互斥锁(mutex)是最基础的线程锁,其他的线程锁均在互斥锁的基础下实现。互斥锁是多个线程共享一个互斥量,当互斥量被设置时,其他线程只能等待互斥量被解放才能继续操作。互斥锁是睡眠等待机制,在等待过程不会占用系统资源。互斥锁是非递归锁。
条件锁: 条件锁(condition_variable)也就条件变量,当线程在条件不满足会进入休眠,一直等待条件变量满足再往下执行。条件锁需要通过notify_one和notify_all唤醒,再执行条件判断。条件锁可以实现线程同步。
读写锁: 读写锁是区分多线程中读操作和写操作的线程锁,也叫共享-独占锁,读锁共享,写锁独占。由于多线程间同时读临界区变量,不会造成线程安全问题,但要保证读操作时不会发生写操作。读写锁在上读锁时,其他线程也可以获取读锁,其他线程想获取写锁时就需要等待读锁解开。当读写锁上写锁时,其他线程无法获取写锁和读锁。
自旋锁: 自旋锁使用方式与互斥锁类似。自旋锁为忙等待机制,自旋锁在等待锁时,会占用CPU,因为自旋锁实际上是循环判断锁的状态。
递归锁: 递归锁的特点是在同一线程可以重复获得锁而不死锁。区分于非递归锁,非递归锁在同一线程重复尝试获得锁时,就会出现死锁。
以上均是个人愚见,如果有哪里说得不对的地方请大家指出。