对于下面的代码:
#include
#include
#include
#include
using namespace std;
#define NUM_THREAD 100
long long num = 0;
void* thread_inc(void* arg){
for (int i = 0; i < 5e5; i++)
num++;
return nullptr;
}
void* thread_des(void* arg){
for (int i = 0; i < 5e5; i++)
num--;
return nullptr;
}
int main(int argc, char *argv[]){
pthread_t pthread_id[NUM_THREAD];
for (int i = 0; i < NUM_THREAD; i++){
if (i%2)
pthread_create(&pthread_id[i], NULL, thread_inc, NULL);
else
pthread_create(&pthread_id[i], NULL, thread_des, NULL);
}
for (int i = 0; i < NUM_THREAD; i++)
pthread_join(pthread_id[i], NULL);
cout << "result: " << num << endl;
return 0;
}
上述示例中共创建了100线程,其中一半执行 thread_inc
函数中的代码,另一半执行 thread_des
函数中的代码,全部变量经过增减过程后应该结果为0,但是实际的结果为:
sizeof long long : 8
result : -23731416 //可能每次运行完之后的结果都不相同
多个线程访问同一变量是问题
多个线程访问同一变量可能会导致以下问题:
- 竞争条件:当多个线程同时读取、写入同一变量时,可能会发生竞争条件,导致程序的行为出现不可预期的错误。例如,多个线程同时读取某个计数器变量的值,进行加一操作后再写回该变量,如果不加同步措施,就可能会导致计数器值的错误累加。
- 互斥问题:当多个线程需要对同一资源进行修改时,需要保证在某一时刻只有一个线程可以访问该资源,否则就可能会发生互斥问题。例如,多个线程需要向同一文件中写入数据,如果不加同步措施,就可能会导致数据被覆盖或者混乱。
- 死锁问题:当多个线程需要互相等待对方释放资源时,就可能会发生死锁问题,导致程序无法继续执行。例如,线程A需要获取资源1和资源2,线程B需要获取资源2和资源1,如果不加同步措施,就可能会导致A获取资源1后等待B释放资源2,B获取资源2后等待A释放资源1,从而导致死锁。
综上所述,多个线程访问同一变量可能会导致一系列问题,需要通过使用同步措施来避免这些问题的发生。常用的同步措施包括互斥锁、条件变量、信号量等,可以有效地避免多线程访问同一变量导致的问题。
上述代码的问题如下:
2 个线程正在同时访问全局变量 num
任何内存空间,只要被同时访问,都有可能发生问题。
因此,线程访问变量 num 时应该阻止其他线程访问,直到线程 1 运算完成。这就是同步(Synchronization)
临界区位置
临界区是指一段同时只能被一个线程执行的代码段。
那么在刚才代码中的临界区位置是:
函数内同时运行多个线程时引发问题的多条语句构成的代码块
全局变量 num 不能视为临界区,因为他不是引起问题的语句,只是一个内存区域的声明。下面是刚才代码的两个 main 函数
void *thread_inc(void *arg)
{
for (int i = 0; i < 50000000; i++)
num += 1; //临界区
return NULL;
}
void *thread_des(void *arg)
{
for (int i = 0; i < 50000000; i++)
num -= 1; //临界区
return NULL;
}
由上述代码可知,临界区并非 num 本身,而是访问 num 的两条语句,这两条语句可能由多个线程同时运行,也是引起这个问题的直接原因。
产生问题的原因可以分为以下三种情况:
thread_inc
函数thread_des
函数thread_inc
和 thread_des
函数比如发生以下情况:
线程 1 执行
thread_inc
的 num+=1 语句的同时,线程 2 执行thread_des
函数的 num-=1 语句
也就是说,两条不同的语句由不同的线程执行时,也有可能构成临界区。前提是这 2 条语句访问同一内存空间。
前面讨论了线程中存在的问题,下面就是解决方法,线程同步。
同步的两面性
线程同步用于解决线程访问顺序引发的问题。需要同步的情况可以从如下两方面考虑。
情况一之前已经解释过,下面讨论情况二。这是「控制线程执行的顺序」的相关内容。假设有 A B 两个线程,线程 A 负责向指定的内存空间内写入数据,线程 B 负责取走该数据。所以这是有顺序的,不按照顺序就可能发生问题。所以这种也需要进行同步。
互斥量(mutex)是一种用于实现多线程同步的机制,主要用于保护共享资源,防止多个线程同时访问和修改同一共享资源,从而避免产生数据竞争等问题。
互斥锁(英语:英语:Mutual exclusion,缩写 Mutex)是一种用于多线程编程中,防止两条线程同时对同一公共资源(比如全域变量)进行读写的机制。该目的通过将代码切片成一个一个的临界区域(critical section)达成。临界区域指的是一块对公共资源进行访问的代码,并非一种机制或是算法。一个程序、进程、线程可以拥有多个临界区域,但是并不一定会应用互斥锁。
通俗的说就互斥量就是一把优秀的锁,当临界区被占据的时候就上锁,等占用完毕然后再放开。
下面是互斥量的创建及销毁函数。
#include
int pthread_mutex_init(pthread_mutex_t *mutex,
const pthread_mutexattr_t *attr); //创建互斥量
int pthread_mutex_destroy(pthread_mutex_t *mutex); //销毁互斥量
/*
成功时返回 0,失败时返回其他值
mutex : 创建互斥量时传递保存互斥量的变量地址值,销毁时传递需要销毁的互斥量地址
attr : 传递即将创建的互斥量属性,没有特别需要指定的属性时传递 NULL
*/
从上述函数声明中可以看出,为了创建相当于锁系统的互斥量,需要声明如下 pthread_mutex_t
型变量:
pthread_mutex_t mutex
该变量的地址值传递给 pthread_mutex_init
函数,用来保存操作系统创建的互斥量(锁系统)。调用 pthread_mutex_destroy
函数时同样需要该信息。如果不需要配置特殊的互斥量属性,则向第二个参数传递 NULL 时,可以利用 PTHREAD_MUTEX_INITIALIZER 进行如下声明:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
推荐尽可能的使用 pthread_mutex_init
函数进行初始化,因为通过宏进行初始化时很难发现发生的错误。
下面是利用互斥量锁住或释放临界区时使用的函数。
#include
int pthread_mutex_lock(pthread_mutex_t *mutex); //互斥量加锁
int pthread_mutex_unlock(pthread_mutex_t *mutex); //互斥量解锁
//成功时返回 0 ,失败时返回其他值
函数本身含有 lock unlock 等词汇,很容易理解其含义。进入临界区前调用的函数就是 pthread_mutex_lock
。调用该函数时,发现有其他线程已经进入临界区,则 pthread_mutex_lock
函数不会返回,直到里面的线程调用 pthread_mutex_unlock
函数退出临界区位置。也就是说,其他线程让出临界区之前,当前线程一直处于阻塞状态。
接下来利用互斥量来解决示例[thread4.cpp]中遇到的问题:
#include
#include
#include
#include
using namespace std;
#define NUM_THREAD 100
long long num = 0;
pthread_mutex_t mutex; //互斥量
void* thread_inc(void *arg){
pthread_mutex_lock(&mutex); //加锁
for (int i = 0; i < 500000000; i++)
num++; //临界区
pthread_mutex_unlock(&mutex); //解锁
return NULL;
}
void* thread_des(void *arg){
for (int i = 0; i < 500000000; i++){
pthread_mutex_lock(&mutex);
num--; //临界区
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main(int argc, char* argv[]){
pthread_t pthread_id[NUM_THREAD];
pthread_mutex_init(&mutex, NULL); //互斥量的创建
cout << "sizeof long long : " << sizeof(long long) << endl;
for (int i = 0; i < NUM_THREAD; i++){
if (i%2)
pthread_create(&pthread_id[i], NULL, thread_inc, NULL);
else
pthread_create(&pthread_id[i], NULL, thread_des, NULL);
}
for (int i = 0; i < NUM_THREAD; i++)
pthread_join(pthread_id[i], NULL);
cout << "result : " << num << endl;
pthread_mutex_destroy(&mutex); //互斥量的销毁
return 0;
}
输出:
sizeof long long : 8
result : 0
从运行结果可以看出,通过互斥量机制得出了正确的运行结果。
在代码中:
void *thread_inc(void *arg)
{
int i;
pthread_mutex_lock(&mutex); //上锁
for (i = 0; i < 50000000; i++)
num += 1;
pthread_mutex_unlock(&mutex); //解锁
return NULL;
}
以上代码的临界区划分范围较大,但这是考虑如下优点所做的决定:
最大限度减少互斥量 lock unlock 函数的调用次数
Linux中的信号量是一种线程同步机制,它可以协调多个线程对共享资源的访问。
Linux中的信号量分为两种类型**:二进制信号量和计数信号量。**
二进制信号量是一种二元信号量,只有两个值:0和1。它用于解决对共享资源的独占访问问题。当一个线程对二进制信号量加锁时,如果该信号量的值为1,则该线程可以继续执行;如果该信号量的值为0,则该线程会被阻塞,直到其他线程释放该信号量。
计数信号量是一种计数型信号量,它可以有多个值,用于控制多个线程对共享资源的访问。当一个线程对计数信号量加锁时,如果该信号量的值大于0,则该线程可以继续执行;如果该信号量的值等于0,则该线程会被阻塞,直到其他线程释放该信号量。
Linux中的信号量使用系统调用
sem_init
、sem_wait
、sem_post
和sem_destroy
进行创建、加锁、解锁和销毁操作。其中,sem_init
用于初始化信号量,sem_wait
用于加锁操作,sem_post
用于解锁操作,sem_destroy
用于销毁信号量。
信号量(英语:Semaphore)又称为信号标,是一个同步对象,用于保持在0至指定最大值之间的一个计数值。当线程完成一次对该semaphore对象的等待(wait)时,该计数值减一;当线程完成一次对semaphore对象的释放(release、post)时,计数值加一。当计数值为0,则线程等待该semaphore对象不再能成功直至该semaphore对象变成signaled状态。semaphore对象的计数值大于0,为signaled状态;计数值等于0,为 nonsignaled
状态.
semaphore对象适用于控制一个仅支持有限个用户的共享资源,是一种不需要使用忙碌等待(busy waiting)的方法。
信号量的概念是由荷兰计算机科学家艾兹赫尔·戴克斯特拉(Edsger W. Dijkstra)发明的,广泛的应用于不同的操作系统中。在系统中,给予每一个进程一个信号量,代表每个进程当前的状态,未得到控制权的进程会在特定地方被强迫停下来,等待可以继续进行的信号到来。如果信号量是一个任意的整数,通常被称为计数信号量(Counting semaphore),或一般信号量(general semaphore);如果信号量只有二进制的0或1,称为二进制信号量(binary semaphore)。
在Linux系统中,二进制信号量(binary semaphore)又称互斥锁(Mutex)。
下面介绍信号量,在互斥量的基础上,很容易理解信号量。此处只涉及利用「二进制信号量」(只用 0 和 1)完成**「控制线程顺序」**为中心的同步方法。下面是信号量的创建及销毁方法:
#include
int sem_init(sem_t *sem, int pshared, unsigned int value); // 信号量的创建
int sem_destroy(sem_t *sem); // 信号量的销毁
/*
成功时返回 0 ,失败时返回其他值
sem : 创建信号量时保存信号量的变量地址值,销毁时传递需要销毁的信号量变量地址值
pshared : 传递其他值时,创建可由多个继承共享的信号量;传递 0 时,创建只允许 1 个进程内部使用的信号量。需要完成同一进程的线程同步,故为0. 如果pshared参数为非零,则信号量可以在不同进程之间共享。
value : 指定创建信号量的初始值
*/
上述的 shared 参数超出了我们的关注范围,故默认向其传递为 0 。下面是信号量中相当于互斥量 lock unlock 的函数。
#include
int sem_wait(sem_t *sem); // sem_wait函数用于获取一个信号量,信号量-1
int sem_post(sem_t *sem); // sem_post函数用于释放一个信号量,信号量+1
/*
成功时返回 0 ,失败时返回其他值
sem : 传递保存信号量读取值的变量地址值,传递给 sem_post 的信号量增1,传递给 sem_wait 时信号量减一
*/
调用 sem_init
函数时,操作系统将创建信号量对象,此对象中记录这「信号量值」(Semaphore Value)整数。该值在调用 sem_post
函数时增加 1 ,调用 wait_wait 函数时减一。但信号量的值不能小于 0 ,因此,在信号量为 0 的情况下调用 sem_wait
函数时,调用的线程将进入阻塞状态(因为函数未返回)。当然,此时如果有其他线程调用 sem_post
函数,信号量的值将变为 1 ,而原本阻塞的线程可以将该信号重新减为 0 并跳出阻塞状态。实际上就是通过这种特性完成临界区的同步操作,可以通过如下形式同步临界区(假设信号量的初始值为 1)
sem_wait(&sem); //信号量变为0...
/* 临界区的开始
...
临界区的结束*/
sem_post(&sem); //信号量变为1...
上述代码结构中,调用 sem_wait
函数进入临界区的线程在调用 sem_post
函数前不允许其他线程进入临界区。信号量的值在 0 和 1 之间跳转,因此,具有这种特性的机制称为「二进制信号量」。接下来的代码是信号量机制的代码。下面代码并非是同时访问的同步,而是关于控制访问顺序的同步,该场景为:
线程 A 从用户输入得到值后存入全局变量 num ,此时线程 B 将取走该值并累加。该过程一共进行 5 次,完成后输出总和并退出程序。
[semaphore.cpp]代码:
#include
#include
#include
using namespace std;
static int num;
static sem_t sem_one, sem_two; //使用两个信号量
void* read(void *arg){
for (int i = 0; i < 5; i++){
cout << "Input num : ";
sem_wait(&sem_two); //对sem_two信号量加锁
cin >> num;
sem_post(&sem_one); //对sem_one进行释放
}
return NULL;
}
void* accu(void *arg){
int sum = 0;
for (int i = 0; i < 5; i++){
sem_wait(&sem_one); //对sem_one进行加锁
sum += num;
sem_post(&sem_two); //对sem_two进行解锁
}
cout << "result : " << sum << endl;
return NULL;
}
int main(int argc, char *argv[]){
sem_init(&sem_one, 0, 0); //创建sem_one信号量只允许一个进程内部使用的信号量,初始值为0
sem_init(&sem_two, 0, 1);
pthread_t id_t1, id_t2;
pthread_create(&id_t1, NULL, read, NULL);
pthread_create(&id_t2, NULL, accu, NULL);
pthread_join(id_t1, NULL);
pthread_join(id_t2, NULL);
sem_destroy(&sem_one);
sem_destroy(&sem_two);
return 0;
}
输出:
Input num : 1
Input num : 2
Input num : 3
Input num : 4
Input num : 5
result : 15
如果将上述代码进行修改
sem_init(&sem_one, 0, 0); //两个信号量的初值都为0
sem_init(&sem_two, 0, 0);
//accu函数
sem_post(&sem_two); //对sem_two信号量加锁 0 -> 1
sum += num; //临界区
sem_wait(&sem_one); //对sem_one进行释放 1 -> 0
如果将
sem_init(&sem_one, 0, 0);
和sem_init(&sem_two, 0, 0);
中的信号量初始值都设置为0,那么这两个信号量在初始化时没有可用的资源,也就是说它们都被锁住了。在这种情况下,代码将会发生死锁。具体来说,当读取线程首先执行时,它会对
sem_two
进行加锁操作,由于sem_two
的初始值为0,所以该线程会被阻塞,一直等待sem_two
变为可用。然而,由于计算线程还没有开始执行,它也无法对sem_one
进行解锁操作,因此读取线程会一直等待,最终导致程序无法继续执行下去。因此,如果将
sem_init(&sem_one, 0, 0);
和sem_init(&sem_two, 0, 0);
中的信号量初始值都设置为0,应该在程序中进行必要的修改以避免死锁的发生。例如,可以将其中一个信号量的初始值设置为1,以确保程序一开始就有可用的资源。
会导致得不到正确的结果
Input num : 1
Input num : 2
Input num : 3
Input num : 4
Input num : 5
result : 10
结果为10不是我们想要的
条件变量类型:pthread_cond_t
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
pthread_cond_t
结构体对象与之关联。attr
参数通常可以设置为NULL,以使用默认属性。int pthread_cond_destroy(pthread_cond_t cond);
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t mutex);
pthread_cond_signal
或pthread_cond_broadcast
来通知条件已满足。在调用此函数之前,必须获取互斥锁(mutex)。int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
pthread_cond_wait
类似,但允许设置一个超时时间,如果超过指定时间条件仍未满足,则函数会返回。abstime
参数指定了等待的最长时间。int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
pthread_cond_signal
不同,此函数用于唤醒所有等待条件变量的线程,使它们都能继续执行。这些函数一起用于实现多线程程序中的条件同步。通常,它们与互斥锁一起使用,以确保在访问共享资源之前线程能够等待特定的条件满足。
#include
#include
#include
using namespace std;
pthread_cond_t cond;
pthread_mutex_t m_mutex; // 利用条件变量来实现线程同步
struct ListNode{
int val;
ListNode* next;
ListNode():val(-1), next(nullptr){}
ListNode(int v):val(v), next(nullptr){}
};
ListNode* head = nullptr;
void* producer(void* arg){
// 生产者
while (1){
pthread_mutex_lock(&m_mutex);
ListNode* temp = new ListNode(rand()% 100); // 生成一个随机数节点
temp->next = head;
head = temp;
cout << "add node, num: " << temp->val << " tid: " << pthread_self() << endl;
// 只要新建了一个节点,就可以通知消费者消费
pthread_cond_signal(&cond);
pthread_mutex_unlock(&m_mutex);
usleep(100);
}
return nullptr;
}
void* customer(void* arg){
while (1){
pthread_mutex_lock(&m_mutex);
ListNode* temp = head;
if (temp){
head = head->next;
cout << "delete node: " << temp->val << " tid: " << pthread_self() << endl;
free(temp);
usleep(100);
}else
pthread_cond_wait(&cond, &m_mutex);
pthread_mutex_unlock(&m_mutex);
return nullptr;
}
}
int main(int argc, char* argv[]){
pthread_mutex_init(&m_mutex, NULL);
pthread_cond_init(&cond, NULL);
pthread_t p_tids[5], c_tids[5];
for (int i = 0; i < 5; i++){
pthread_create(&p_tids[i], NULL, producer, NULL);
pthread_create(&c_tids[i], NULL, customer, NULL);
}
for (int i = 0; i < 5; i++){
pthread_detach(p_tids[i]);
pthread_detach(c_tids[i]);
}
while (1){
usleep(10);
}
return 0;
}