一、线程冲突
当我们使用多线程操作同一共享资源时,可能会因为操作次序等出现线程不一致的情况。
![](https://i-blog.csdnimg.cn/direct/485868b1993f447994f4881ac060f827.png)
例如在以上情况下不应该出现 count 被减至 -1,但在多线程操作同一共享资源的情况下,很有可能出现数据不一致的情况,也就是出现了线程冲突的情况。
在以上代码中可能出现的情况,假如此时 count = 1,当线程 t1 执行循环条件判断进入循环后,因为 usleep() 而被剥下CPU,线程 t2 执行时此时 count 并没有进行修改,因此线程 t2 依然进入循环。也就是当 count = 1 时可能存在两个线程都进入了循环,致使最后变量 count 执行了两次--,导致最后 count = -1。
二、重入与线程安全
(一)可重入函数
可重入指的指的是一个函数或代码块能够在中途被打断(比如同一线程内的递归调用或在多个线程中并发调用)而不出现异常或错误,也就是一个函数代码块不会因为同一线程多次调用或多个线程并发调用而产生错误或异常的行为。
以上函数 start_routine() 便是不可重入函数,当多个线程调用时就会出现错误。
以下是一些不可重入的情况:
1、调用了 malloc/free 函数,因为 malloc 函数是用全局链表来管理堆的;
2、调用了标准 I/O库 函数,标准 I/O的很多实现都是以不可重入的方式使用数据结构 ;
3、可重入函数使用静态的数据结构。
(二)线程安全
线程安全是指多线程环境下,多个线程访问共享资源时,不会导致数据不一致、竞争条件等问题。线程安全能够保证多个线程并发执行时不会发生冲突或错误。
以下是一些线程不安全的情况:
1、不保护共享变量的函数;
2、函数状态随着调用而变化的函数;
3、返回指向静态变量指针的函数;
4、调用线程不安全函数的函数。
(三)可重入与线程安全的联系
可重入与线程安全都有提到多线程环境下程序并发运行有关,可重入侧重于单个线程重复调用或多个线程并发调用不会出现错误;而线程安全指的是在多线程的环境下,程序的执行不会引发竞争条件,数据不一致的问题。
可重入不一定是线程安全的,但线程安全通常是可重入的。
(四)STL与智能指针
STL库为了追求高性能并没有保证线程安全,因此用户在使用STL容器时需要自行保证线程安全。
对于智能指针中的引用计数本身而言是线程安全的,但我们使用智能指针进行管理的资源对象并不是线程安全的,该资源对象的线程安全需要用户进行保证。
三、互斥锁
(一)概念
为了解决线程冲突问题,因此引入了锁的概念,其作用是为了保证互斥。
当我们使用锁对共享资源进行保护后,共享资源就变为了临界资源,即每次仅允许一个线程访问临界区(访问临界资源的那段代码),从而保证了线程安全。
换种角度而言,使用锁对共享资源进行保护后,使得多线程对该资源的访问由并发访问变为了串行访问。
(二)互斥锁的使用
cpp
NAME
pthread_mutex_destroy, pthread_mutex_init - destroy and initialize a mutex
SYNOPSIS
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
RETURN VALUE
If successful, the pthread_mutex_destroy() and pthread_mutex_init()
functions shall return zero; otherwise, an error number shall be returned to
indicate the error.
首先是对于互斥锁的初始化与销毁。当我们声明一个互斥锁后需要 pthread_mutex_inti() 进行初始化,当我们不需要使用互斥锁后需要 pthread_mutex_destory() 进行销毁。当该锁为全局变量或静态变量时也可以使用 PTHREAD_MUTEX_INITIALIZER 宏进行初始化。
cpp
NAME
pthread_mutex_lock, pthread_mutex_trylock, pthread_mutex_unlock - lock
and unlock a mutex
SYNOPSIS
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
RETURN VALUE
If successful, the pthread_mutex_lock() and pthread_mutex_unlock()
functions shall return zero; otherwise, an error number shall be returned
to indicate the error.
The pthread_mutex_trylock() function shall return zero if a lock on
the mutex object referenced by mutex is acquired. Otherwise, an error number
is returned to indicate the error.
这部分是对于锁的使用,当初始化好锁以后,我们可以使用 pthread_mutex_lock() 与 pthread_mutex_unlock() 对共享资源的访问进行保护。需要注意的一点是当已有一个线程访问临界区时,另一个线程访问临界区加锁时会被阻塞挂起,而使用 pthread_mutex_trylock() 申请锁失败时并不会被阻塞挂起,而是返回错误码。通过该特性我们就可以根据申请锁是否成功而执行不同的操作。
下面是针对本文线程冲突的解决方法:
cpp
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
int count = 1000;
pthread_mutex_t _mutex = PTHREAD_MUTEX_INITIALIZER;
void *start_coutine(void *arg)
{
while (1)
{
pthread_mutex_lock(&_mutex);
if (count > 0)
{
usleep(500);
--count;
cout << pthread_self() << " -> " << count << endl;
pthread_mutex_unlock(&_mutex);
}
else
{
pthread_mutex_unlock(&_mutex);
break;
}
usleep(500);
}
return nullptr;
}
int main()
{
pthread_t t1, t2;
pthread_create(&t1, nullptr, start_coutine, nullptr);
pthread_create(&t2, nullptr, start_coutine, nullptr);
pthread_join(t1, nullptr);
pthread_join(t2, nullptr);
return 0;
}
在本例中如果直接在原有代码循环外加锁解锁会退化为单线程运行,因此需要把 count 条件判断放在循环里,如果没有在循环体末尾使用 usleep() 进行休眠会导致一个线程重复进行加锁操作解锁。也就是即使时在多线程环境下运行该程序也会退化为单进程运行。因此在使用完锁之后进行休眠可以保证其他线程可以申请到锁。
(三)基于RAII风格的锁的封装
cpp
#include <iostream>
#include <pthread.h>
using namespace std;
class mutexGuard
{
public:
mutexGuard(pthread_mutex_t *mutex)
: _mutex(mutex)
{
pthread_mutex_lock(_mutex);
}
~mutexGuard()
{
pthread_mutex_unlock(_mutex);
}
private:
pthread_mutex_t *_mutex;
};
(四)其他锁
1、自旋锁
一般的互斥锁申请失败后都是阻塞挂起的,而自旋锁则是轮询申请锁。该锁在一定的场景下可以提高效率,例如在临界区很短或者访问很快时,使用轮询锁可以提高程序效率。但是一旦临界区的处理消耗时间长,自旋锁的长时间轮询会导致 CPU 效率极低,大部分的时间都在轮询申请所。
cpp
NAME
pthread_spin_lock, pthread_spin_trylock - lock a spin lock object
(ADVANCED REALTIME THREADS)
SYNOPSIS
#include <pthread.h>
int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);
RETURN VALUE
Upon successful completion, these functions shall return zero;
otherwise, an error number shall be returned to indicate the error.
2、读写锁
(1)概念
读写锁是专门应用于读者和写者的场景:大部分时间都在读取。很少对数据内容进行修改。
读者写者三种关系:
- 读者与写者之间的同步,互斥关系
- 读者之间没有任何关系
- 写者之间存在互斥关系
读写模型和生产消费模型最大的区别是:消费者会拿走消费数据,而读者并不会拿走数据。
(2)读写锁的使用
初始化与销毁:
cpp
NAME
pthread_rwlock_destroy, pthread_rwlock_init - destroy and initialize
a read-write lock object
SYNOPSIS
#include <pthread.h>
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
const pthread_rwlockattr_t *restrict attr);
RETURN VALUE
If successful, the pthread_rwlock_destroy() and pthread_rwlock_init()
functions shall return zero; otherwise, an error number shall be returned to
indicate the error.
读者:
cpp
NAME
pthread_rwlock_rdlock, pthread_rwlock_tryrdlock - lock a read-write
lock object for reading
SYNOPSIS
#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
RETURN VALUE
If successful, the pthread_rwlock_rdlock() function shall return zero;
otherwise, an error number shall be returned to indicate the error.
写者:
cpp
NAME
pthread_rwlock_trywrlock, pthread_rwlock_wrlock - lock a read-write
lock object for writing
SYNOPSIS
#include <pthread.h>
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
RETURN VALUE
The pthread_rwlock_trywrlock() function shall return zero if the lock
for writing on the read-write lock object referenced by rwlock is acquired.
Otherwise, an error number shall be returned to indicate the error.
(3)原理
|-------|------|------|
| 读写锁的行为 |||
| 当前锁状态 | 读锁请求 | 写锁请求 |
| 无锁 | 可以 | 可以 |
| 读锁 | 可以 | 不可以 |
| 写锁 | 不可以 | 不可以 |
在同一时间,读写模型仅仅允许一个写者进入,但支持多个读者读取。
![](https://i-blog.csdnimg.cn/direct/18b45fd8313f4280ba3c1ba843aefd8e.png)
上述情况实际是读者优先,当持续的读者到来时会造成写者饥饿。如果需要写者优先需要进行额外的操作,例如当写者到来时申请一个锁用于阻挡后续的读者,当临界区所有读者读取完成退出后,写者进入临界区进行写入,当写入完成后再释放锁使后续的读者可以继续进行访问。
四、死锁
(一)概念
多个线程或进程等待对方释放资源而陷入无限等待的状态,例如一个线程A自身持有一个锁,其还想得到线程B持有的一个锁,而线程B则是等待线程A持有的锁,如此便形成了死锁。
同一线程对同一个锁进行连续申请也会陷入死锁。
(二)四个必要条件
1、互斥:同一时间仅允许一个线程或进程使用该资源,如果在使用期间其他进程请求该资源必须进行等待;
2、请求与保持:一个进程必须至少持有一个资源,并且正在等待其他进程持有的资源;
3、不剥夺条件:资源不能被强行剥夺,只能由占有资源的进程在完成后自愿释放;
4、循环等待:存在一组进程 {P1, P2, ..., Pn},其中每个进程 Pi 都在等待着下一个进程 Pi+1 持有的资源,而最后一个进程 Pn 又在等待进程 P1 持有的资源。形成一个闭环。
(三)预防死锁与避免死锁
预防死锁本质也是破坏死锁的四个必要条件,例如所有进程申请资源的顺序相同,资源一次性分配等。
避免死锁本质是动态规划资源使得不会出现死锁,例如银行家算法。
除此之外当死锁发生时也可以检测到死锁,例如将系统中的进程和资源看作节点,将它们之间的关系看作边,构成一个图。通过检测图中是否存在环,来判断系统是否处于死锁状态。
五、线程同步
(一)概念
在保证数据安全的前提下,让多个线程能够按照某种特定的顺序访问临界资源,从而避免饥饿问题。
(二)条件变量
初始化与销毁:
cpp
NAME
pthread_cond_destroy, pthread_cond_init - destroy and initialize
condition variables
SYNOPSIS
#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
RETURN VALUE
If successful, the pthread_cond_destroy() and pthread_cond_init()
functions shall return zero; otherwise, an error number shall be returned to
indicate the error.
线程等待与线程唤醒:
cpp
NAME
pthread_cond_timedwait, pthread_cond_wait - wait on a condition
SYNOPSIS
#include <pthread.h>
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex,
const struct timespec *restrict abstime);
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
RETURN VALUE
Except in the case of [ETIMEDOUT], all these error checks shall act as
if they were performed immediately at the beginning of processing for the function
and shall cause an error return, in effect, prior to modifying the state of the
mutex specified by mutex or the condition variable specified by cond.
cpp
NAME
pthread_cond_broadcast, pthread_cond_signal - broadcast or signal a condition
SYNOPSIS
#include <pthread.h>
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
RETURN VALUE
If successful, the pthread_cond_broadcast() and pthread_cond_signal()
functions shall return zero; otherwise, an error number shall be returned to
indicate the error.
(三)原理
条件变量是配合互斥锁共同使用的,当条件变量不满足时就会去对应的条件变量进行等待。
![](https://i-blog.csdnimg.cn/direct/369cba1927914046bb45cd1ee0cfd5ad.png)
(四)使用例子
使用条件变量实现交替打印。
cpp
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
int count = 0;
pthread_mutex_t _mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t _cond = PTHREAD_COND_INITIALIZER;
void *start_coutine1(void *arg)
{
while (1)
{
pthread_mutex_lock(&_mutex);
while (count % 2 == 0)
pthread_cond_wait(&_cond, &_mutex);
cout << "start_coutine1" << endl;
++count;
pthread_cond_signal(&_cond);
pthread_mutex_unlock(&_mutex);
}
return nullptr;
}
void *start_coutine2(void *arg)
{
while (1)
{
pthread_mutex_lock(&_mutex);
while (count % 2 == 1)
pthread_cond_wait(&_cond, &_mutex);
cout << "start_coutine2" << endl;
++count;
pthread_cond_signal(&_cond);
pthread_mutex_unlock(&_mutex);
}
return nullptr;
}
int main()
{
pthread_t t1, t2;
pthread_create(&t1, nullptr, start_coutine1, nullptr);
pthread_create(&t2, nullptr, start_coutine2, nullptr);
pthread_join(t1, nullptr);
pthread_join(t2, nullptr);
return 0;
}
上述代码的分析:当程序刚开始执行时 count = 0,若线程 t1 先执行,此时 count 为偶数线程 t1 会申请锁成功后被阻塞在条件变量,同时将申请到的锁进行释放。当线程 t2 执行,此时 count 仍然为 0, 因此线程 t2 并不会阻塞在条件变量会继续向下执行。当线程 t2 执行到 pthread_cond_signal() 时线程 t1 会被唤醒,但线程 t1 被唤醒后会重新去竞争锁,但此时锁被线程 t2 所持有,因此线程 t1 处于竞争锁的状态。当线程 t2 释放锁后由线程 t1 获得锁并执行。
对于条件变量的条件判断如果使用 if 而不是 while,如果多个线程同时被阻塞在条件变量时,再使用 pthread_cond_broadcast() 全部唤醒会导致大量线程同时进入临界区,如果使用 while 进行条件判断,即使所有线程被唤醒,也只有一个线程会跳出循环访问临界区。