linux线程同步
互斥锁
同步与互斥概述**
现代操作系统基本都是多任务操作系统,即同时有大量可调度实体在运行。在多任务操作系统中,同时运行的多个任务可能:
-
都需要访问/使用同一种资源
-
多个任务之间有依赖关系,某个任务的运行依赖于另一个任务
这两种情形是多任务编程中遇到的最基本的问题,也是多任务编程中的核心问题,同步和互斥就是用于解决这两个问题的。
互斥:是指散步在不同任务之间的若干程序片断,当某个任务运行其中一个程序片段时,其它任务就不能运行它们之中的任一程序片段,只能等到该任务运行完这个程序片段后才可以运行。最基本的场景就是:一个公共资源同一时刻只能被一个进程或线程使用,多个进程或线程不能同时使用公共资源。
同步:是指散步在不同任务之间的若干程序片断,它们的运行必须严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。最基本的场景就是:两个或两个以上的进程或线程在运行过程中协同步调,按预定的先后次序运行。比如 A 任务的运行依赖于 B 任务产生的数据。
显然,同步是一种更为复杂的互斥,而互斥是一种特殊的同步。也就是说互斥是两个任务之间不可以同时运行,他们会相互排斥,必须等待一个线程运行完毕,另一个才能运行,而同步也是不能同时运行,但他是必须要按照某种次序来运行相应的线程(也是一种互斥)!因此互斥具有唯一性和排它性,但互斥并不限制任务的运行顺序,即任务是无序的,而同步的任务之间则有顺序关系。
为什么需要互斥锁
在多任务操作系统中,同时运行的多个任务可能都需要使用同一种资源。这个过程有点类似于,公司部门里,我在使用着打印机打印东西的同时(还没有打印完),别人刚好也在此刻使用打印机打印东西,如果不做任何处理的话,打印出来的东西肯定是错乱的。
下面我们用程序模拟一下这个过程,线程一需要打印“ hello ”,线程二需要打印“ world ”,不加任何处理的话,打印出来的内容会错乱:
测试程序:
#include <stdio.h>
#include <string.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h> /* * 模拟一下这个过程,线程一需要打印“ hello ”,线程二需要打印“ world ”,* 不加任何处理的话,打印出来的内容会错乱:*/void printer(char *str)
{while (*str != '\0'){putchar(*str);fflush(stdout);str++;sleep(1);}printf("\n");
}//线程一
void *thread_fun_1(void *arg)
{char *str = "hello";printer(str); //打印
}//线程二
void *thread_fun_2(void *arg)
{char *str = "world";printer(str); //打印
}int main()
{pthread_t tid1, tid2;//创建 2 个线程pthread_create(&tid1, NULL, thread_fun_1, NULL);pthread_create(&tid2, NULL, thread_fun_2, NULL);//等待线程结束,回收其资源pthread_join(tid1, NULL);pthread_join(tid2, NULL);return 0;
}
运行结果如下:
实际上,打印机是有做处理的,我在打印着的时候别人是不允许打印的,只有等我打印结束后别人才允许打印。这个过程有点类似于,把打印机放在一个房间里,给这个房间安把锁,这个锁默认是打开的。当 A 需要打印时,他先过来检查这把锁有没有锁着,没有的话就进去,同时上锁在房间里打印。而在这时,刚好 B 也需要打印,B 同样先检查锁,发现锁是锁住的,他就在门外等着。而当 A 打印结束后,他会开锁出来,这时候 B 才进去上锁打印。
互斥锁Mutex介绍
而在线程里也有这么一把锁:互斥锁(mutex),也叫互斥量,互斥锁是一种简单的加锁的方法来控制对共享资源的访问,互斥锁只有两种状态,即加锁( lock )和解锁( unlock )。
互斥锁的操作流程如下:
1)在访问共享资源后临界区域前,对互斥锁进行加锁。
2)在访问完成后释放互斥锁导上的锁。
3)对互斥锁进行加锁后,任何其他试图再次对互斥锁加锁的线程将会被阻塞,直到锁被释放。
互斥锁的数据类型是: pthread_mutex_t。
安装对应帮助手册:
sudo apt-get install manpages-posix-dev
pthread_mutex_init 函数
初始化互斥锁:
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
功能:
初始化一个互斥锁。
参数:
mutex:互斥锁地址。类型是 pthread_mutex_t 。
attr:设置互斥量的属性,通常可采用默认属性,即可将 attr 设为 NULL。
可以使用宏 PTHREAD_MUTEX_INITIALIZER 静态初始化互斥锁,比如:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
这种方法等价于使用 NULL 指定的 attr 参数调用 pthread_mutex_init() 来完成动态初始化,不同之处在于 PTHREAD_MUTEX_INITIALIZER 宏不进行错误检查。
返回值:
成功:0,成功申请的锁默认是打开的。
失败:非 0 错误码
restrict,C语言中的一种类型限定符(Type Qualifiers),用于告诉编译器,对象已经被指针所引用,不能通过除该指针外所有其他直接或间接的方式修改该对象的内容。
pthread_mutex_destroy函数
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
功能:
销毁指定的一个互斥锁。互斥锁在使用完毕后,必须要对互斥锁进行销毁,以释放资源。
参数:
mutex:互斥锁地址。
返回值:
成功:0
失败:非 0 错误码
pthread_mutex_lock函数
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
功能:
对互斥锁上锁,若互斥锁已经上锁,则调用者阻塞,直到互斥锁解锁后再上锁。
参数:
mutex:互斥锁地址。
返回值:
成功:0
失败:非 0 错误码
int pthread_mutex_trylock(pthread_mutex_t *mutex);
- 调用该函数时,若互斥锁未加锁,则上锁,返回 0;
- 若互斥锁已加锁,则函数直接返回失败,即 EBUSY。
pthread_mutex_unlock函数
#include <pthread.h>
int pthread_mutex_unlock(pthread_mutex_t *mutex);
功能:
对指定的互斥锁解锁。
参数:
mutex:互斥锁地址。
返回值:
成功:0
失败:非0错误码
测试程序
#include <stdio.h>
#include <string.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h> /* * 模拟一下这个过程,线程一需要打印“ hello ”,线程二需要打印“ world ”,* 加锁的代码,此时打印的结果不会错乱*/pthread_mutex_t mutex; //互斥锁//打印机
void printer(char *str)
{pthread_mutex_lock(&mutex); //上锁while (*str != '\0'){putchar(*str);fflush(stdout);str++;sleep(1);}printf("\n");pthread_mutex_unlock(&mutex); //解锁
}//线程一
void *thread_fun_1(void *arg)
{char *str = "hello";printer(str); //打印
}//线程二
void *thread_fun_2(void *arg)
{char *str = "world";printer(str); //打印
}int main(void)
{pthread_t tid1, tid2;pthread_mutex_init(&mutex, NULL); //初始化互斥锁//创建 2 个线程pthread_create(&tid1, NULL, thread_fun_1, NULL);pthread_create(&tid2, NULL, thread_fun_2, NULL);//等待线程结束,回收其资源pthread_join(tid1, NULL);pthread_join(tid2, NULL);pthread_mutex_destroy(&mutex); //销毁互斥锁return 0;
}
结果:
死锁(DeadLock)
1)什么是死锁
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
2)死锁引起的原因
竞争不可抢占资源引起死锁
也就是我们说的第一种情况,而这都在等待对方占有的不可抢占的资源。
竞争可消耗资源引起死锁
有p1,p2,p3三个进程,p1向p2发送消息并接受p3发送的消息,p2向p3发送消息并接受p1的消息,p3向p1发送消息并接受p2的消息,如果设置是先接到消息后发送消息,则所有的消息都不能发送,这就造成死锁。
进程推进顺序不当引起死锁
有进程p1,p2,都需要资源A,B,本来可以p1运行A --> p1运行B --> p2运行A --> p2运行B,但是顺序换了,p1运行A时p2运行B,容易发生第一种死锁。互相抢占资源。
3)死锁的必要条件
互斥条件
- 某资源只能被一个进程使用,其他进程请求该资源时,只能等待,直到资源使用完毕后释放资源。
请求和保持条件
- 程序已经保持了至少一个资源,但是又提出了新要求,而这个资源被其他进程占用,自己占用资源却保持不放。
不可抢占条件
- 进程已获得的资源没有使用完,不能被抢占。
循环等待条件
- 必然存在一个循环链。
4)处理死锁的思路
预防死锁
破坏死锁的四个必要条件中的一个或多个来预防死锁。
避免死锁
和预防死锁的区别就是,在资源动态分配过程中,用某种方式防止系统进入不安全的状态。
检测死锁
运行时出现死锁,能及时发现死锁,把程序解脱出来
解除死锁
发生死锁后,解脱进程,通常撤销进程,回收资源,再分配给正处于阻塞状态的进程。
5)预防死锁的方法
破坏请求和保持条件
协议1:
所有进程开始前,必须一次性地申请所需的所有资源,这样运行期间就不会再提出资源要求,破坏了请求条件,即使有一种资源不能满足需求,也不会给它分配正在空闲的资源,这样它就没有资源,就破坏了保持条件,从而预防死锁的发生。
协议2:
允许一个进程只获得初期的资源就开始运行,然后再把运行完的资源释放出来。然后再请求新的资源。
破坏不可抢占条件
当一个已经保持了某种不可抢占资源的进程,提出新资源请求不能被满足时,它必须释放已经保持的所有资源,以后需要时再重新申请。
破坏循环等待条件
对系统中的所有资源类型进行线性排序,然后规定每个进程必须按序列号递增的顺序请求资源。假如进程请求到了一些序列号较高的资源,然后有请求一个序列较低的资源时,必须先释放相同和更高序号的资源后才能申请低序号的资源。多个同类资源必须一起请求。
读写锁
读写锁概述
当有一个线程已经持有互斥锁时,互斥锁将所有试图进入临界区的线程都阻塞住。但是考虑一种情形,当前持有互斥锁的线程只是要读访问共享资源,而同时有其它几个线程也想读取这个共享资源,但是由于互斥锁的排它性,所有其它线程都无法获取锁,也就无法读访问共享资源了,但是实际上多个线程同时读访问共享资源并不会导致问题。
在对数据的读写操作中,更多的是读操作,写操作较少,例如对数据库数据的读写应用。为了满足当前能够允许多个读出,但只允许一个写入的需求,线程提供了读写锁来实现。
读写锁的特点如下:
1)如果有其它线程读数据,则允许其它线程执行读操作,但不允许写操作。
2)如果有其它线程写数据,则其它线程都不允许读、写操作。
读写锁分为读锁和写锁,规则如下:
1)如果某线程申请了读锁,其它线程可以再申请读锁,但不能申请写锁。
2)如果某线程申请了写锁,其它线程不能申请读锁,也不能申请写锁。
POSIX 定义的读写锁的数据类型是: pthread_rwlock_t。
pthread_rwlock_init函数
#include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
const pthread_rwlockattr_t *restrict attr);
功能:
用来初始化 rwlock 所指向的读写锁。
参数:
rwlock:指向要初始化的读写锁指针。
attr:读写锁的属性指针。如果 attr 为 NULL 则会使用默认的属性初始化读写锁,否则使用指定的 attr 初始化读写锁。
可以使用宏 PTHREAD_RWLOCK_INITIALIZER 静态初始化读写锁,比如:
pthread_rwlock_t my_rwlock = PTHREAD_RWLOCK_INITIALIZER;
这种方法等价于使用 NULL 指定的 attr 参数调用 pthread_rwlock_init() 来完成动态初始化,不同之处在于PTHREAD_RWLOCK_INITIALIZER 宏不进行错误检查。
返回值:
成功:0,读写锁的状态将成为已初始化和已解锁。
失败:非 0 错误码。
pthread_rwlock_destroy函数
#include <pthread.h>
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
功能:
用于销毁一个读写锁,并释放所有相关联的资源(所谓的所有指的是由 pthread_rwlock_init() 自动申请的资源) 。
参数:
rwlock:读写锁指针。
返回值:
成功:0
失败:非 0 错误码
pthread_rwlock_rdlock函数
#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
功能:
以阻塞方式在读写锁上获取读锁(读锁定)。
如果没有写者持有该锁,并且没有写者阻塞在该锁上,则调用线程会获取读锁。
如果调用线程未获取读锁,则它将阻塞直到它获取了该锁。一个线程可以在一个读写锁上多次执行读锁定。
线程可以成功调用 pthread_rwlock_rdlock() 函数 n 次,但是之后该线程必须调用 pthread_rwlock_unlock() 函数 n 次才能解除锁定。
参数:
rwlock:读写锁指针。
返回值:
成功:0
失败:非 0 错误码
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
- 用于尝试以非阻塞的方式来在读写锁上获取读锁。
- 如果有任何的写者持有该锁或有写者阻塞在该读写锁上,则立即失败返回。
pthread_rwlock_wrlock函数
#include <pthread.h>
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
功能:
在读写锁上获取写锁(写锁定)。
如果没有写者持有该锁,并且没有写者读者持有该锁,则调用线程会获取写锁。
如果调用线程未获取写锁,则它将阻塞直到它获取了该锁。
参数:
rwlock:读写锁指针。
返回值:
成功:0
失败:非 0 错误码
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
- 用于尝试以非阻塞的方式来在读写锁上获取写锁。
- 如果有任何的读者或写者持有该锁,则立即失败返回。
pthread_rwlock_unlock函数
#include <pthread.h>
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
功能:
无论是读锁或写锁,都可以通过此函数解锁。
参数:
rwlock:读写锁指针。
返回值:
成功:0
失败:非 0 错误码
测试程序示例
下面是一个使用读写锁来实现 4 个线程读写一段数据是实例。
在此示例程序中,共创建了 4 个线程,其中两个线程用来写入数据,两个线程用来读取数据。当某个线程读操作时,其他线程允许读操作,却不允许写操作;当某个线程写操作时,其它线程都不允许读或写操作。
#include <stdio.h>
#include <string.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h> /* * 创建了 4 个线程,其中两个线程用来写入数据,两个线程用来读取数据。* 当某个线程读操作时,其他线程允许读操作,却不允许写操作;当某个线程写操作时,* 其它线程都不允许读或写操作。*/pthread_rwlock_t rwlock; //读写锁
int num = 1;//读操作,其他线程允许读操作,却不允许写操作
void *fun1(void *arg)
{while (1){pthread_rwlock_rdlock(&rwlock);printf("read num first===%d\n", num);pthread_rwlock_unlock(&rwlock);sleep(1);}
}//读操作,其他线程允许读操作,却不允许写操作
void *fun2(void *arg)
{while (1){pthread_rwlock_rdlock(&rwlock);printf("read num second===%d\n", num);pthread_rwlock_unlock(&rwlock);sleep(2);}
}//写操作,其它线程都不允许读或写操作
void *fun3(void *arg)
{while (1){pthread_rwlock_wrlock(&rwlock);num++;printf("write thread first\n");pthread_rwlock_unlock(&rwlock);sleep(2);}
}//写操作,其它线程都不允许读或写操作
void *fun4(void *arg)
{while (1){pthread_rwlock_wrlock(&rwlock);num++;printf("write thread second\n");pthread_rwlock_unlock(&rwlock);sleep(1);}
}int main()
{pthread_t ptd1, ptd2, ptd3, ptd4;pthread_rwlock_init(&rwlock, NULL);//初始化一个读写锁//创建线程pthread_create(&ptd1, NULL, fun1, NULL);pthread_create(&ptd2, NULL, fun2, NULL);pthread_create(&ptd3, NULL, fun3, NULL);pthread_create(&ptd4, NULL, fun4, NULL);//等待线程结束,回收其资源pthread_join(ptd1, NULL);pthread_join(ptd2, NULL);pthread_join(ptd3, NULL);pthread_join(ptd4, NULL);pthread_rwlock_destroy(&rwlock);//销毁读写锁return 0;
}
结果:
条件变量
条件变量概述
与互斥锁不同,条件变量是用来等待而不是用来上锁的,条件变量本身不是锁!
条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。通常条件变量和互斥锁同时使用。
条件变量的两个动作:
-
条件不满, 阻塞线程
-
当条件满足, 通知阻塞的线程开始工作
条件变量的类型: pthread_cond_t。
pthread_cond_init函数
#include <pthread.h>
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
功能:
初始化一个条件变量
参数:
cond:指向要初始化的条件变量指针。
attr:条件变量属性,通常为默认值,传NULL即可
也可以使用静态初始化的方法,初始化条件变量:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
返回值:
成功:0
失败:非0错误号
pthread_cond_destroy函数
#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
功能:
销毁一个条件变量
参数:
cond:指向要初始化的条件变量指针
返回值:
成功:0
失败:非0错误号
pthread_cond_wait函数
#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
功能:
阻塞等待一个条件变量
a) 阻塞等待条件变量cond(参1)满足
b) 释放已掌握的互斥锁(解锁互斥量)相当于pthread_mutex_unlock(&mutex);
a) b) 两步为一个原子操作。
c) 当被唤醒,pthread_cond_wait函数返回时,解除阻塞并重新申请获取互斥锁pthread_mutex_lock(&mutex);
参数:
cond:指向要初始化的条件变量指针
mutex:互斥锁
返回值:
成功:0
失败:非0错误号
限时等待一个条件变量:
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex,
const struct *restrict abstime);
功能:
限时等待一个条件变量
参数:
cond:指向要初始化的条件变量指针
mutex:互斥锁
abstime:绝对时间
返回值:
成功:0
失败:非0错误号
abstime补充说明:
struct timespec {
time_t tv_sec; /* seconds */ // 秒
long tv_nsec; /* nanosecondes*/ // 纳秒
}
time_t cur = time(NULL); //获取当前时间。
struct timespec t; //定义timespec 结构体变量t
t.tv_sec = cur + 1; // 定时1秒
pthread_cond_timedwait(&cond, &t);
pthread_cond_signal函数
唤醒至阻塞在条件变量上的线程
#include <pthread.h>
int pthread_cond_signal(pthread_cond_t *cond);
功能:
唤醒至少一个阻塞在条件变量上的线程
参数:
cond:指向要初始化的条件变量指针
返回值:
成功:0
失败:非0错误号
唤醒全部阻塞在条件变量上的线程:
int pthread_cond_broadcast(pthread_cond_t *cond);
功能:
唤醒全部阻塞在条件变量上的线程
参数:
cond:指向要初始化的条件变量指针
返回值:
成功:0
失败:非0错误号
生产者消费者条件变量模型
线程同步典型的案例即为生产者消费者模型,而借助条件变量来实现这一模型,是比较常见的一种方法。
假定有两个线程,一个模拟生产者行为,一个模拟消费者行为。两个线程同时操作一个共享资源(一般称之为汇聚),生产向其中添加产品,消费者从中消费掉产品。
/*借助条件变量模拟 生产者-消费者 问题*/
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <stdio.h> /*链表作为公享数据,需被互斥量保护*/
struct msg { struct msg *next; int num;
}; struct msg *head; /* 静态初始化 一个条件变量 和 一个互斥量*/
pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; void *consumer(void *p)
{ struct msg *mp; for (;;) { pthread_mutex_lock(&lock); while (head == NULL) { //头指针为空,说明没有节点 可以为if吗 pthread_cond_wait(&has_product, &lock); // 解锁,并阻塞等待 } mp = head; head = mp->next; //模拟消费掉一个产品 pthread_mutex_unlock(&lock); printf("-Consume %lu---%d\n", pthread_self(), mp->num); free(mp); sleep(rand() % 5); }
} void *producer(void *p)
{ struct msg *mp; for (;;) { mp = malloc(sizeof(struct msg)); mp->num = rand() % 1000 + 1; //模拟生产一个产品 printf("-Produce ---------------------%d\n", mp->num); pthread_mutex_lock(&lock); mp->next = head; //头插法 head = mp; pthread_mutex_unlock(&lock); pthread_cond_signal(&has_product); //将等待在该条件变量上的一个线程唤醒 sleep(rand() % 5); }
} int main(int argc, char *argv[])
{ pthread_t pid, cid; srand(time(NULL)); pthread_create(&pid, NULL, producer, NULL); pthread_create(&cid, NULL, consumer, NULL); pthread_join(pid, NULL); pthread_join(cid, NULL); return 0;
}
结果:
一个生产者多个消费者模型
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h> void err_thread(int ret, char *str)
{ if (ret != 0) { fprintf(stderr, "%s:%s\n", str, strerror(ret)); pthread_exit(NULL); }
} struct msg { int num; struct msg *next;
}; struct msg *head; pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 定义/初始化一个互斥量
pthread_cond_t has_data = PTHREAD_COND_INITIALIZER; // 定义/初始化一个条件变量 void *produser(void *arg)
{ while (1) { struct msg *mp = malloc(sizeof(struct msg)); mp->num = rand() % 1000 + 1; // 模拟生产一个数据` printf("--produce %d\n", mp->num); pthread_mutex_lock(&mutex); // 加锁 互斥量 mp->next = head; // 写公共区域 head = mp; pthread_mutex_unlock(&mutex); // 解锁 互斥量 pthread_cond_signal(&has_data); // 唤醒阻塞在条件变量 has_data上的线程. sleep(rand() % 3); } return NULL;
} void *consumer(void *arg)
{ while (1) { struct msg *mp; pthread_mutex_lock(&mutex); // 加锁 互斥量 while (head == NULL) { pthread_cond_wait(&has_data, &mutex); // 阻塞等待条件变量, 解锁 } // pthread_cond_wait 返回时, 重写加锁 mutex mp = head; head = mp->next; pthread_mutex_unlock(&mutex); // 解锁 互斥量 printf("---------consumer id = %lu :%d\n", pthread_self(), mp->num); free(mp); sleep(rand()%3); } return NULL;
} int main(int argc, char *argv[])
{ int ret; pthread_t pid, cid; srand(time(NULL)); ret = pthread_create(&pid, NULL, produser, NULL); // 生产者 if (ret != 0) err_thread(ret, "pthread_create produser error"); ret = pthread_create(&cid, NULL, consumer, NULL); // 消费者 if (ret != 0) err_thread(ret, "pthread_create consuer error"); ret = pthread_create(&cid, NULL, consumer, NULL); // 消费者 if (ret != 0) err_thread(ret, "pthread_create consuer error"); ret = pthread_create(&cid, NULL, consumer, NULL); // 消费者 if (ret != 0) err_thread(ret, "pthread_create consuer error"); pthread_join(pid, NULL); pthread_join(cid, NULL); return 0;
}
结果:
条件变量的优缺点
相较于mutex而言,条件变量可以减少竞争。
如直接使用mutex,除了生产者、消费者之间要竞争互斥量以外,消费者之间也需要竞争互斥量,但如果汇聚(链表)中没有数据,消费者之间竞争互斥锁是无意义的。
有了条件变量机制以后,只有生产者完成生产,才会引起消费者之间的竞争。提高了程序效率。
信号量
信号量概述
信号量广泛用于进程或线程间的同步和互斥,信号量本质上是一个非负的整数计数器,它被用来控制对公共资源的访问。
编程时可根据操作信号量值的结果判断是否对公共资源具有访问的权限,当信号量值大于 0 时,则可以访问,否则将阻塞。
PV 原语是对信号量的操作,一次 P 操作使信号量减1,一次 V 操作使信号量加1。
信号量主要用于进程或线程间的同步和互斥这两种典型情况。
信号量数据类型为:sem_t。
信号量用于互斥:
信号量用于同步:
sem_init函数
初始化信号量:
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
功能:
创建一个信号量并初始化它的值。一个无名信号量在被使用前必须先初始化。
参数:
sem:信号量的地址。
pshared:等于 0,信号量在线程间共享(常用);不等于0,信号量在进程间共享。
value:信号量的初始值。
返回值:
成功:0
失败: - 1
sem_destroy函数
销毁信号量:
#include <semaphore.h>
int sem_destroy(sem_t *sem);
功能:
删除 sem 标识的信号量。
参数:
sem:信号量地址。
返回值:
成功:0
失败: - 1
信号量P操作(减1)
int sem_wait(sem_t *sem):
#include <semaphore.h>
int sem_wait(sem_t *sem);
功能:
将信号量的值减 1。操作前,先检查信号量(sem)的值是否为 0,若信号量为 0,此函数会阻塞,直到信号量大于 0 时才进行减 1 操作。
参数:
sem:信号量的地址。
返回值:
成功:0
失败: - 1
int sem_trywait(sem_t *sem):
int sem_trywait(sem_t *sem);
- 以非阻塞的方式来对信号量进行减 1 操作。
- 若操作前,信号量的值等于 0,则对信号量的操作失败,函数立即返回。
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout):
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
- 限时尝试将信号量的值减 1
- abs_timeout:绝对时间
abs_timeout补充说明:
struct timespec {
time_t tv_sec; /* seconds */ // 秒
long tv_nsec; /* nanosecondes*/ // 纳秒
}
time_t cur = time(NULL); //获取当前时间。
struct timespec t; //定义timespec 结构体变量t
t.tv_sec = cur + 1; // 定时1秒
sem_timedwait(&cond, &t);
信号量V操作(加1)
#include <semaphore.h>
int sem_post(sem_t *sem);
功能:
将信号量的值加 1 并发出信号唤醒等待线程(sem_wait())。
参数:
sem:信号量的地址。
返回值:
成功:0
失败:-1
获取信号量的值
#include <semaphore.h>
int sem_getvalue(sem_t *sem, int *sval);
功能:
获取 sem 标识的信号量的值,保存在 sval 中。
参数:
sem:信号量地址。
sval:保存信号量值的地址。
返回值:
成功:0
失败:-1
程序示例
/*信号量实现 生产者 消费者问题*/ #include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <stdio.h>
#include <semaphore.h> #define NUM 5 int queue[NUM]; //全局数组实现环形队列
sem_t blank_number, product_number; //空格子信号量, 产品信号量 void *producer(void *arg)
{ int i = 0; while (1) { sem_wait(&blank_number); //生产者将空格子数--,为0则阻塞等待 queue[i] = rand() % 1000 + 1; //生产一个产品 printf("----Produce---%d\n", queue[i]); sem_post(&product_number); //将产品数++ i = (i+1) % NUM; //借助下标实现环形 sleep(rand()%1); }
} void *consumer(void *arg)
{ int i = 0; while (1) { sem_wait(&product_number); //消费者将产品数--,为0则阻塞等待 printf("-Consume---%d\n", queue[i]); queue[i] = 0; //消费一个产品 sem_post(&blank_number); //消费掉以后,将空格子数++ i = (i+1) % NUM; sleep(rand()%3); }
} int main(int argc, char *argv[])
{ pthread_t pid, cid; sem_init(&blank_number, 0, NUM); //初始化空格子信号量为5, 线程间共享 -- 0 sem_init(&product_number, 0, 0); //产品数为0 pthread_create(&pid, NULL, producer, NULL); pthread_create(&cid, NULL, consumer, NULL); pthread_join(pid, NULL); pthread_join(cid, NULL); sem_destroy(&blank_number); sem_destroy(&product_number); return 0;
}
结果:
相关文章:
linux线程同步
互斥锁 同步与互斥概述** 现代操作系统基本都是多任务操作系统,即同时有大量可调度实体在运行。在多任务操作系统中,同时运行的多个任务可能: 都需要访问/使用同一种资源 多个任务之间有依赖关系,某个任务的运行依赖于另一个任…...
大内存对电脑性能有哪些提升
在科技飞速发展的今天,电脑已经成为我们生活和工作中不可或缺的伙伴。无论是日常办公、追剧娱乐,还是进行复杂的游戏和专业设计,电脑的性能都至关重要。而在影响电脑性能的众多因素中,内存大小常常被人们忽视。 多任务处理更流畅…...
什么是“微博养铁粉”以及如何增加微博铁粉
发了个发微博养铁工具_微博养铁粉的定义 微博养铁粉是指粉丝通过与博主的互动,成为博主的铁粉。铁粉是微博推出的一种反映粉丝与博主之间亲密度的互动产品。成为铁粉后,粉丝的评论权重增加,更容易上前排,点赞和评论的效果也会更好…...
华为和H3C服务器配置远控管理地址
1、华为RH2288_V3服务器 1.1、启动服务器按DEL按键进入服务器bios 1.2、选择Advanced菜单中的 IPMI iBMC Configuration配置项回车进入。 1.3、IPMI iBMC Configuration配置界面中选择IBMC Configuration配置项回车进入。 1.4、IBMC Configuration 配置项中配置IPV4 Configura…...
Git 查询与切换分支的完整指南
Git 查询与切换分支的完整指南 1. 查询分支列表 查看本地分支 git branch当前分支会以绿色显示并带有 * 标记添加 -v 或 -vv 查看更详细的信息(最后一次提交和跟踪关系) git branch -v # 或者 git branch -vv查看所有分支(包括远程分支&a…...
Spring 中的依赖注入(DI)详解
📌 摘要 在现代 Java 开发中,依赖注入(Dependency Injection, DI) 是 Spring 框架最核心的功能之一。它通过解耦对象之间的依赖关系,提高了代码的可维护性、可测试性和可扩展性。 本文将全面讲解 Spring 中依赖注入的…...
Bytebase 3.7.1 - 数据库变更功能全免费!
🔔 重大变更 所有数据库变更相关功能现已在社区版中完全免费开放!详情请查看我们的最新定价。 🎄 改进 文档网站全面升级,改进导航、搜索功能,以及与 AI 集成自助回答问题。SQL 编辑器现在会高亮光标所在的语句。SQ…...
深度学习笔记27-LSTM实现糖尿病探索与预测(Pytorch)
🍨 本文为🔗365天深度学习训练营中的学习记录博客🍖 原作者:K同学啊 一、前期准备 1.数据导入 import torch.nn as nn import torch.nn.functional as F import torchvision,torch import numpy as np import pandas as pd impo…...
3DS中文游戏全集下载 任天堂3DS简介3DS第一方独占游戏推荐
任天堂3DS 的详细介绍,涵盖其硬件特性、核心功能、游戏阵容及历史地位: 3DS游戏全集下载 https://pan.quark.cn/s/dd40e47387e7 https://sink-698.pages.dev/3ds CIA CCA 等格式可用于3DS模拟器和3DS实体机 3DS 是什么? 全称:Nin…...
vue3 reactive重新赋值
在 Vue 3 中,如果你想使用 reactive API 来创建一个响应式对象,并且之后需要更新这个对象中的属性,你可以按照以下步骤进行: 1. 使用 reactive 创建响应式对象 首先,你需要从 Vue 的 reactive API 中创建一个响应式对…...
全面掌握 C++ 基础:关键特性与进化
文章目录 全面掌握 C 基础:关键特性与进化1. C 关键字2. 命名空间(namespace)⚠️ 示例 2.1 定义命名空间2.2 使用成员的方法 3. C 输入/输出(iostream)4. 缺省参数(Default Parameter)4.1 定义…...
HTML一键打包EXE串口API介绍
HTML一键打包EXE软件(HTML转EXE) 支持将Web前端项目转换为Windows平台下的独立可执行程序(EXE),适用于Windows 7及以上系统,无需额外配置系统环境, 软件包含多种内核, 包括IE内核, Chrome内核, 以及WebView2(永久免费), 适用于不同…...
.docx 和 .doc 都是 Word 文档格式的区别
.docx 和 .doc 都是 Word 文档格式,但有区别: .docx 是新版 Word 格式(推荐使用) 从 Microsoft Word 2007 起引入的格式全名是:Office Open XML Document实际是一个 压缩包(ZIP)结构࿰…...
如何轻松地将音乐从 iPhone 传输到 Mac?
想把音乐从 iPhone 传输到 Mac 吗?这很常见,无论你是想更换设备、备份收藏,还是只想在更大的屏幕上欣赏喜爱的歌曲。幸运的是,有 6 种有效的方法可以完成这项工作,具体取决于你喜欢使用的工具。让我们开始吧。 第 1 部…...
Qwen3 Embedding 结构-加载-训练 看透模型设计哲学
看透一个顶级AI句向量模型的设计秘密,从文件结构到加载原理,再到其背后的训练哲学。 1 Qwen3-Embedding模型结构拆解 说明:目录包含了运行一个基于 Transformer 的句向量模型所需的所有组件 文件类别核心文件作用核心模型model.safetensors…...
AT8548双通道 H 桥电机驱动芯片
AT8548 是一种双通道低饱和电压的正、反向电机驱动芯片,为玩具、打印机和其它电机一体化应用提供一种双通道电机驱动方案。 特点: 双通道H桥电机驱动器; 驱动两个直流有刷电机或者一个步进电机; 低RDS(ON)电阻,1.06Ω(…...
kubeadm worker节点加入master失败
文章目录 1、操作2、问题现象3、问题原因4、问题解决4.1、重新生成token4.2、重新生成hash值 5、验证 1、操作 执行以下命令,让worker节点加入到master节点 kubeadm join 103.123.222.241:6443 --token vxe3v1.wzpnks8v1vbbtsu0 --discovery-token-ca-cert-hash s…...
Maven 之工程化开发核心指南:插件配置、pom 文件与依赖管理
目录 1. 🧩Maven插件 2. 🏗️构建Maven工程 3. 📄 pom文件配置 3.1. ⚙️ 中定义一些配置信息 3.2. 📦 中定义依赖的jar包坐标 3.3. 🔌 中定义第三方插件 4. ✍️编写代码 5. 🔗依赖范…...
分布式系统中的 Kafka:流量削峰与异步解耦(二)
Kafka 在分布式系统中的应用案例 电商订单系统 在电商领域,订单系统是核心业务模块之一,涉及多个复杂的业务环节和系统组件之间的交互。以常见的电商购物流程为例,当用户在电商平台上下单后,订单创建服务会首先接收到用户的订单…...
从服务器收到预料之外的响应。此文件可能已被成功上传。请检查媒体库或刷新本页
如果php.ini已经加入了如下的内容还是报错 : upload_max_filesize 1024M post_max_size 1024M 那就是因为阿帕奇导致:...
FramePack 安装指南(中文)
FramePack 安装指南(中文) -Windows FramePack 是最前沿的 AI 视频生成框架,以极小的硬件需求颠覆视频创作!它能在仅 6GB 笔记本 GPU 内存上,驱动 13B 模型以 30 FPS 生成超长 120 秒视频,几乎无内容限制&…...
【NLP入门系列三】NLP文本嵌入(以Embedding和EmbeddingBag为例)
🍨 本文为🔗365天深度学习训练营 中的学习记录博客🍖 原作者:K同学啊 博主简介:努力学习的22级本科生一枚 🌟;探索AI算法,C,go语言的世界;在迷茫中寻找光芒…...
电子电气架构 --- 软件供应商如何进入OEM体系
我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 简单,单纯,喜欢独处,独来独往,不易合同频过着接地气的生活,除了生存温饱问题之外,没有什么过多的欲望,表面看起来很高冷,内心热情,如果你身…...
检索增强生成(RAG)领域关键数据集综述:分类、挑战与展望
检索增强生成(RAG)领域关键数据集综述:分类、挑战与展望 摘要 检索增强生成(RAG)通过融合外部知识库与大型语言模型,已成为解决知识密集型自然语言处理(NLP)任务的关键范式。高质量…...
CFD仿真计算革命:基于GPU的格子玻尔兹曼方法(LBM)算子优化——利用Tensor Core加速碰撞核计算(性能提升3倍实测)
点击 “AladdinEdu,同学们用得起的【H卡】算力平台,注册即送H800算力”,H卡级别算力,按量计费,灵活弹性,顶级配置,学生专属优惠。 在计算流体动力学领域,格子玻尔兹曼方法正以介观模…...
【蓝牙】Qt4中向已配对的手机发送PDF文件
在Qt 4中实现通过蓝牙向已配对的设备发送文件(例如PDF文件),你可以使用Qt Bluetooth模块。Qt 4的蓝牙模块提供了基本的蓝牙功能,包括设备发现、配对管理和数据传输。下面是一些步骤和示例代码,帮助你通过蓝牙发送PDF文…...
Vue + AbortController 请求取消弹窗 hook 封装
背景 实际业务开发场景中,往往存在有些大数据请求的需求,一旦请求发起加载遮罩后用户就无法操作了,直接尬住,所以提供一个支持取消查询的功能还是很有必要的,为了在全业务接口都能使用封装一个hook。 ✋为什么要用 A…...
在小程序中实现上下左右拖动表格
在小程序的开发中,不可避免会出现上下左右拖动表格的类似需求,下面将把这个简单实现一下 其中主要使用到了overflow: scroll;来使得横向和纵向可以滚动,并且使用负边距 父容器截断的方法来同时隐藏横向和纵向滚动条,从而实现该效…...
Spark 以及 spark streaming 核心原理及实践
导语 spark 已经成为广告、报表以及推荐系统等大数据计算场景中首选系统,因效率高,易用以及通用性越来越得到大家的青睐,我自己最近半年在接触spark以及spark streaming之后,对spark技术的使用有一些自己的经验积累以及心得体会&…...
数据融合平台是什么?如何搭建数据融合平台?
目录 一、数据融合是什么 1. 定义 2. 作用 二、数据融合平台的功能是什么 1. 数据抽取 2. 数据清洗 3. 数据转换 4. 数据关联 5. 数据存储 三、如何让搭建数据融合平台 1. 需求分析 2. 选择合适的技术和工具 3. 设计平台架构 4. 开发和部署平台 5. 数据迁移和融…...
Linux之线程同步与互斥
目录 一、线程互斥 1.1、进程线程间的互斥相关背景概念 1.2、互斥量mutex 1.2.1、互斥量的接⼝ 1.3、互斥量实现原理探究 1.4、互斥量的封装 二、线程同步 2.1、条件变量 2.2、同步概念与竞态条件 2.3、条件变量函数 2.4、⽣产者消费者模型 2.4.1、为何要使⽤⽣产者…...
uniapp开发小程序,导出文件打开并保存,实现过程downloadFile下载,openDocument打开
uniapp开发小程序,导出文件打开并保存 实现思路 1、调用请求获取到后端接口返回的下载文件的url路径 (注意必须是https的路径,域名需要配置在微信小程序后台的合法域名里面) 2、使用 uni.downloadFile 方法 (下载文件…...
腾讯云COS“私有桶”下,App如何安全获得音频调用流程
流程图 #mermaid-svg-Phy4VCltBRZ90UH8 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-Phy4VCltBRZ90UH8 .error-icon{fill:#552222;}#mermaid-svg-Phy4VCltBRZ90UH8 .error-text{fill:#552222;stroke:#552222;}#me…...
简单的 Flask 后端应用
from flask import Flask, request, jsonify, session import os app Flask(__name__) app.secret_key os.urandom(24) users { 123: admin, admin: admin } # 登录接口 app.route(/login, methods[POST]) def login(): data request.get_json() username data.get(usern…...
Android 中 解析 XML 字符串的几种方式
在 Android 开发中,解析 XML 文件有多种方式,每种方式都有其特点和适用场景。常见的 XML 解析方式有 DOM 解析、SAX 解析 和 XmlPullParser 解析。 1、DOM 解析 DOM(Document Object Model)解析是一种基于树结构的解析方式&#…...
git commit
git commit 是版本控制的核心操作之一,用于将暂存区的修改记录为新的版本提交。以下是关键步骤和最佳实践: 基础操作 提交单个文件: bash Copy Code git commit -m “提交信息” 提交多个文件: bash Copy Code …...
【新手向】GitHub Desktop 的使用说明(含 GitHub Desktop 和 Git 的功能对比)
GitHub Desktop 是 GitHub 公司推出的一款桌面应用程序,旨在帮助开发人员更轻松地使用 GitHub,以下是其简单的使用说明: 安装与登录 下载 GitHub Desktop |GitHub 桌面 访问GitHub Desktop 官方网站,根据自己的操作系统下载对应的…...
Tomcat项目本地部署(Servlet为例)
在Windows上部署 在idea中打开项目 首先我们需要准备一个Servlet项目,我之前的Servlet项目是用eclipse写的,这种情况下如果用idea直接打开的话会出现左侧目录无法显示的情况,这个时候我们就需要用别的方法打开 打开项目管理 如下图&#…...
Linux——linux的基本命令
目录 一、linux的目录结构 二、绝对路径和相对路径 三、文件类型(linux下所有东西都可看作文件) 四、文件的权限 五、文件权限的修改(chmod) 六、linux常用的命令 七、文件查看命令 八、文件编辑命令 九、文件压缩与解压…...
wireshark过滤显示rtmp协议
wireshark中抓包显示的数据报文中,明明可以看到有 rtmp 协议的报文,但是过滤的时候却显示一条都没有 查看选项中的配置,已经没有 RTMP 这个协议了,已经被 RTMPT 替换了,过滤框中输入 rtmpt 过滤即可...
Fiddler抓包工具使用技巧:如何结合Charles和Wireshark提升开发调试效率
在开发过程中,网络调试工具是每个程序员的必备利器,特别是当涉及到Web应用和移动应用的调试时,抓包工具的作用尤为突出。无论是处理复杂的API调用、分析性能瓶颈,还是排查网络通信问题,抓包工具都能够帮助开发者精准地…...
LVS负载均衡群集
这里写目录标题 案例:部署Tomcat案例分析案例概述案例前置知识点Tomcat 简介应用场景 案例环境 案例实施实施准备关闭 firewalld 防火墙在安装Tomcat之前必须先安装JDK 查看JDK是否安装安装配置 TomcatTomcat 的安装和配置步骤如下:解压后生成 apache-tomcat-9.0.8文件夹&#…...
【unitrix】 3.5 类型级别的比较系统(cmp.rs)
一、源码 这段代码定义了一个类型级别的比较系统,主要用于在编译时比较类型并得出比较结果。它使用了 Rust 的类型系统和标记特征(trait)来实现这一功能。 use crate::sealed::Sealed; use crate::number::{Z0, P1, N1}; use core::cmp::Ordering;// 比较结果类型…...
防御式编程:防止 XSS 攻击
对用户输入进行编码和过滤是防止 XSS 攻击的关键。以下是改进后的代码示例: from flask import Flask, request, escape from markupsafe import Markup app Flask(__name__) app.route(/comment, methods[POST]) def comment(): user_comment escape(re…...
【Java项目设计】基于Springboot+Vue的OA办公自动化系统
介绍: 基于Springboot为后端,vue为前端的企业综合性OA办公自动化平台,涵盖九大核心模块,全方位解决企业日常办公需求,提升工作效率和管理水平。系统采用模块化设计,功能全面且易于扩展,从基础登…...
WebServer实现:muduo库的主丛Reactor架构
前言 作为服务器,核心自然是高效的处理来自client的多个连接啦,那问题在于,如何高效的处理client的连接呢?这里就介绍两种架构:单Reactor架构和主丛Reactor架构。 单Reactor架构 单Reactor架构的核心为,由一个主线程监…...
每天一个前端小知识 Day 7 - 现代前端工程化与构建工具体系
现代前端工程化与构建工具体系 1. 为什么要工程化?(面试高频问题) 问题痛点: 模块太多、无法组织;代码冗长、性能差;浏览器兼容性差;团队协作混乱,缺少规范与自动化。 工程化目标…...
nginx的下载与安装 mac
1. 下载 方法一:本地下载 链接:https://nginx.org/en/download.html(可直接搜官网) 下载到本地后,上传到linux的某个文件夹中 方法二:直接linux上下载(推荐) wget -c http://ngi…...
[持续集成]
学习目标 能够使用 Git 代码托管平台管理代码能够实现 jenkinspostman 的持续集成能够实现 jenkins代码 的持续集成 持续集成 概念 : 将自己工作成果持续不断地把代码聚集在一起,成员可以每天集成一次或多次相关工具 : git : 代码管理工具,自带本地仓库gitee : 远程代码管理…...
Spring Aop @AfterThrowing (异常通知): 使用场景
核心定义 AfterThrowing 是 Spring AOP 中专门用于处理异常场景的**通知(Advice)**类型。它的核心作用是: 仅在目标方法(连接点)的执行过程中抛出异常时,执行一段特定的逻辑。如果目标方法成功执行并正常…...