前言
本篇文章带大家学习线程的同步。
一、什么是线程同步
线程同步是指协调多个线程之间的执行顺序,以确保共享资源的正确访问和数据的一致性。
当多个线程同时操作共享数据时,如果没有适当的同步机制,就会出现数据竞争和不一致的情况。
线程同步的目的是为了保证共享资源在多线程环境下的安全访问,避免数据冲突和并发缺陷。通过使用同步机制,可以使得多个线程按照一定的顺序来访问共享资源,避免出现竞态条件(Race Condition)和不确定性的结果。
常用的线程同步机制包括:
1.互斥锁(Mutex):互斥锁是一种常见的同步机制,用于保护共享资源,一次只允许一个线程访问共享资源。当一个线程获取到互斥锁之后,其他线程必须等待该线程释放锁之后才能继续访问。
2.信号量(Semaphore):信号量是一种用于控制并发访问数量的同步机制。通过设置一个计数器,限制同时访问某个资源的线程数量。当计数器大于0时,线程可以获取许可进行访问,访问后计数器减少;当计数器为0时,线程等待其他线程释放许可。
3.条件变量(Condition Variable):条件变量用于线程之间的条件等待与信号通知。线程可以在某个条件成立时等待条件变量,而其他线程在满足条件时发出信号通知等待线程继续执行。
4.栅栏(Barrier):栅栏用于线程的同步和屏障等待。多个线程在某个位置等待,直到所有线程都到达屏障位置,然后才能继续执行后面的操作。
`线程同步的正确使用可以避免数据竞争和不一致性问题,提高并发程序的正确性和可靠性。然而,不正确或过度的同步机制也可能导致性能问题,因此需要根据具体的应用场景进行合理的同步策略和设计。``
二、不使用线程同步访问共享资源可能出现的问题
#include <pthread.h> #include <stdio.h> #include <unistd.h> int i = 0; void* Thread1(void *arg) { while (1) { i++; printf("Thread1 i = %d\n", i); sleep(1);//休眠1s } } void* Thread2(void *arg) { while (1) { i++; printf("Thread2 i = %d\n", i); sleep(1);//休眠1s } } int main(void) { pthread_t tid1;//线程ID pthread_t tid2;//线程ID int err = -1; err = pthread_create(&tid1, NULL, Thread1, NULL); if(err < 0) { printf("create thread1 is err\n"); } err = pthread_create(&tid2, NULL, Thread2, NULL); if(err < 0) { printf("create thread2 is err\n"); } while (1) { sleep(1);//休眠1s } return 0; }
运行结果:
由运行结果可以看出不使用线程同步访问共享资源的话可能导致数据的不一致性。
三、互斥锁概念
互斥锁(Mutex Lock)是一种并发编程中的同步机制,用于保护共享资源的访问,确保在任何给定时间只有一个线程可以执行受保护的代码段。
互斥锁是二进制锁,它有两个状态:锁定(locked)和解锁(unlocked)。当一个线程获取到互斥锁时,它将锁定状态设置为锁定,其他线程尝试获取锁时将被阻塞,直到锁被释放。
互斥锁主要用于解决并发环境下的竞争条件,例如多个线程试图同时访问和修改同一共享资源的情况。通过使用互斥锁,我们可以确保同一时间只有一个线程能够进入临界区(Critical Section),执行对共享资源的访问和操作,避免数据竞争和不一致的结果。
使用互斥锁的基本流程如下:
1.初始化互斥锁:在使用互斥锁前,需要对其进行初始化。
2.获取互斥锁:线程通过调用互斥锁的"锁定"操作,尝试获取互斥锁。如果互斥锁当前处于解锁状态,线程将获取到锁,并将其状态设置为锁定;否则,线程将被阻塞,直到锁被释放。
3.执行临界区代码:一旦线程成功获取到互斥锁,它就可以进入临界区,执行需要保护的代码,访问共享资源。
4.释放互斥锁:线程执行完临界区代码后,应该调用互斥锁的"解锁"操作来释放锁,将互斥锁的状态设置为解锁,以允许其他线程获取锁并访问共享资源。
互斥锁是一种常见的线程同步机制,用于确保共享资源在并发访问时的正确性和一致性。然而,不正确地使用互斥锁可能导致死锁和性能问题,因此,合理的设计和使用是很重要的。
四、互斥锁使用
1.初始化线程锁的方式
静态方法:
静态创建互斥锁意味着在编译时为互斥锁分配静态的内存,并在定义时进行初始化。
在C语言中,可以使用静态初始化宏PTHREAD_MUTEX_INITIALIZER来静态创建并初始化互斥锁。
pthread_mutex_t m_mutex = PTHREAD_MUTEX_INITIALIZER;
动态方法:
动态创建互斥锁意味着在运行时动态分配互斥锁的内存,并使用相关函数进行初始化。
在C语言中,可以使用pthread_mutex_init函数进行动态创建和初始化互斥锁。
示例代码如下:
pthread_mutex_t mutex; pthread_mutex_init(&mutex, NULL); // 销毁互斥锁 pthread_mutex_destroy(&mutex);
2.使用代码
示例代码:
#include <stdio.h> #include <pthread.h> #include <unistd.h> #include <stdlib.h> /*共享变量*/ int val = 0; pthread_mutex_t m_mutex = PTHREAD_MUTEX_INITIALIZER; void* Thread1(void* arg) { while (1) { /*加锁*/ pthread_mutex_lock(&m_mutex); val++; /*解锁*/ pthread_mutex_unlock(&m_mutex); printf("thread1 val : %d\n", val); sleep(1); } } void* Thread2(void* arg) { while (1) { /*加锁*/ pthread_mutex_lock(&m_mutex); val++; /*解锁*/ pthread_mutex_unlock(&m_mutex); printf("thread2 val : %d\n", val); sleep(1); } } int main(void) { pthread_t tid1; pthread_t tid2; pthread_create(&tid1, NULL, Thread1, NULL); pthread_create(&tid2, NULL, Thread1, NULL); while(1) { sleep(1); } return 0; }
五、死锁的产生和解决方法
1.什么是死锁
死锁是指在并发程序中,两个或多个线程或进程因为互相等待对方释放资源而无法继续执行的情况。在死锁中,每个线程都在等待其他线程释放资源,导致所有线程都被阻塞,无法进行下一步操作。
死锁通常发生在多线程或多进程环境中,涉及共享资源的竞争。当多个线程需要访问共享资源时,如果每个线程都持有一个资源并且等待其他线程释放它所需要的资源,就可能发生死锁。
2.为什么会产生死锁
死锁的产生需要满足以下四个必要条件,也被称为死锁的条件:
1.互斥条件(Mutual Exclusion):一个资源一次只能被一个线程或进程占用,即当一个线程或进程访问资源时,其他线程或进程必须等待。
2.请求与保持条件(Hold and Wait):一个线程或进程可以在保持已经获得的资源的同时请求新的资源。
3.不可抢占条件(No Preemption):已经分配给一个线程或进程的资源不能被强制性地抢占,只能在使用完之后自愿释放。
4.循环等待条件(Circular Wait):存在一种等待资源的循环链,即若干个线程或进程之间形成一种头尾相接的循环等待资源的关系。
要解决死锁问题,可以采用死锁预防、死锁避免、死锁检测和死锁恢复等策略。这些策略的目标是打破死锁产生的必要条件,从而避免或解决死锁的发生。
3.怎么解决死锁问题
1.资源有序分配:为了避免循环等待条件,可以对资源进行排序,并确保线程按照相同的顺序请求资源,从而避免产生循环等待。下面是一个示例代码:
#include <stdio.h> #include <stdlib.h> #include <pthread.h> pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER; void *thread1(void *arg) { pthread_mutex_lock(&mutex1); printf("Thread1: Holding mutex1...\n"); // 线程1等待一段时间,模拟资源竞争 sleep(1); printf("Thread1: Trying to acquire mutex2...\n"); pthread_mutex_lock(&mutex2); printf("Thread1: Holding mutex2...\n"); pthread_mutex_unlock(&mutex2); pthread_mutex_unlock(&mutex1); pthread_exit(NULL); } void *thread2(void *arg) { pthread_mutex_lock(&mutex1); printf("Thread2: Holding mutex1...\n"); // 线程2等待一段时间,模拟资源竞争 sleep(1); printf("Thread2: Trying to acquire mutex2...\n"); pthread_mutex_lock(&mutex2); printf("Thread2: Holding mutex2...\n"); pthread_mutex_unlock(&mutex2); pthread_mutex_unlock(&mutex1); pthread_exit(NULL); } int main() { pthread_t tid1, tid2; // 创建两个线程 pthread_create(&tid1, NULL, thread1, NULL); pthread_create(&tid2, NULL, thread2, NULL); // 等待线程结束 pthread_join(tid1, NULL); pthread_join(tid2, NULL); printf("Program completed.\n"); return 0; }
在这个示例中,我们确保线程1和线程2都按照相同的顺序访问互斥锁mutex1和mutex2,从而避免了循环等待的情况。
2.避免持有并等待:一种解决死锁问题的方法是要求线程在申请资源时,释放已持有的资源。这样,当线程请求新的资源时,它没有任何资源保持,从而避免了持有并等待的情况。以下是示例代码:
#include <stdio.h> #include <stdlib.h> #include <pthread.h> pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER; void *thread1(void *arg) { pthread_mutex_lock(&mutex1); printf("Thread1: Holding mutex1...\n"); pthread_mutex_unlock(&mutex1); // 线程1等待一段时间,模拟资源竞争 sleep(1); printf("Thread1: Trying to acquire mutex2...\n"); pthread_mutex_lock(&mutex2); printf("Thread1: Holding mutex2...\n"); pthread_mutex_unlock(&mutex2); pthread_exit(NULL); } void *thread2(void *arg) { pthread_mutex_lock(&mutex1); printf("Thread2: Holding mutex1...\n"); pthread_mutex_unlock(&mutex1); // 线程2等待一段时间,模拟资源竞争 sleep(1); printf("Thread2: Trying to acquire mutex2...\n"); pthread_mutex_lock(&mutex2); printf("Thread2: Holding mutex2...\n"); pthread_mutex_unlock(&mutex2); pthread_exit(NULL); } int main() { pthread_t tid1, tid2; // 创建两个线程 pthread_create(&tid1, NULL, thread1, NULL); pthread_create(&tid2, NULL, thread2, NULL); // 等待线程结束 pthread_join(tid1, NULL); pthread_join(tid2, NULL); printf("Program completed.\n"); return 0; }
在这个示例中,线程1和线程2在获取并释放mutex1之后立即释放它,然后才尝试获取mutex2。这种方法确保了线程在请求新资源之前不保持任何资源,从而避免了持有并等待的情况。
总结
本篇文章主要讲解了线程同步的概念和互斥锁的使用,以及死锁的产生和解决方法。