linux信号量进程线程同步

Aki 发布于 2023-03-07 288 次阅读


互斥和信号量、

进程间通信的方式有管道、消息队列、共享内存这些都是进程间的信息通信,而信号量可以理解为进程使用的临界资源的状态说明,信号量主要用于保证同步与互斥。

  • 临界资源:两个进程看到的一份公共资源称之为临界资源
  • 临界区:各个进程中访问临界资源的代码叫做临界区
  • 互斥:每个进程访问临界资源的时候必须是独占式的(排他式的),只能自己一个人访问
  • 同步:防止不间断的占有资源和释放资源,这样的话其他进程就会长时间得不到资源,这样会造成进程的饥饿问题

由此可见我们之前用于进程间通信的管道,消息队列,共享内存都是临界资源,管道是内核已经提供了同步与互斥,但是消息队列和共享内存都是不保证同步与互斥的。

Linux中的信号量和互斥锁都是用于多进程/线程同步的机制,但它们之间有一些差异:

  1. 作用范围:信号量可以用于多进程之间和一个进程间不同线程之间的同步,而互斥锁只能用于同一进程内的线程同步。
  2. 用法:使用信号量时,需要先创建一个信号量并初始化,然后使用sem_wait()和sem_post()函数来操作信号量。使用互斥锁时,需要先创建一个互斥锁并初始化,然后使用pthread_mutex_lock()和pthread_mutex_unlock()函数来操作互斥锁。
  3. 实现方式:信号量可以有多个值,可以用于控制并发访问的数量,而互斥锁只有两个状态,用于控制对临界区的访问。
  4. 应用场景:信号量通常用于控制共享资源的访问,例如共享内存,而互斥锁则用于保护临界区,以避免多个线程同时访问共享数据。
  5. 优点和缺点:信号量的优点是可以控制并发访问的数量,但它可能会导致死锁,特别是在多个进程之间使用时。互斥锁的优点是可以保护临界区,但它可能会导致线程饥饿,特别是在有多个线程时。

如果有多个进程,每个进程之间又有多个线程,并且这些线程需要共享某些资源,可以考虑使用共享内存加上信号量和互斥锁的方式来实现跨进程和线程的同步。共享内存可以让多个进程共享同一块物理内存,而信号量和互斥锁则可以控制对这个共享内存的访问。

具体来说,可以在共享内存中设置一些共享变量,然后使用信号量和互斥锁来保证多个线程和进程对这些共享变量的访问顺序和正确性。例如,您可以使用一个互斥锁来保证同一时间只有一个线程能够访问共享内存,然后使用一个信号量来控制线程的执行顺序,以避免出现数据竞争等问题。

当进程在等待一个信号量时,如果信号量的值为 0,那么进程会被阻塞并被放到信号量的等待队列中等待。因此,可以说进程在获取不到信号量时,是被调度到信号量的等待队列中等待。

当一个进程获取到信号量的资源时,如果此时有多个进程在等待该信号量,那么内核会从等待队列中选择一个进程来唤醒,并将信号量的值加 1,从而使被唤醒的进程可以继续执行。

需要注意的是,当进程被阻塞时,它不会占用 CPU 资源,而是被从 CPU 上移除,并被加入到阻塞队列中等待。当有信号到达、计时器到期或者其他条件满足时,内核会将进程从阻塞队列中唤醒,并将其重新加入到就绪队列中等待调度。因此,使用信号量等同步机制时,可以避免因忙等待等问题而导致的 CPU 占用过高和性能下降等问题。

linux信号量使用、

头文件 #include<semaphore>

(1) sem_t 类型,信号量类型

信号量类型,规定信号量 sem 不能 < 0。

(2) sem_init 函数,初始化一个信号量

int sem_init(sem_t *sem, int pshared, unsigned int value);

参1:sem 信号量
参2:pshared 取 0 用于线程间同步,取非 0(一般为 1)用于进程间同步,也可以使用宏 PTHREAD_PROCESS_SHARED(进程间) 和 PTHREAD_PROCESS_PRIVATE(线程间) 来指定。
参3:value 指定信号量初值
成功返回0,失败返回-1

(3) sem_destroy 函数,销毁一个信号量

int sem_destroy(sem_t *sem);

成功返回0,失败返回-1

(4) sem_wait 函数,给信号量加锁,信号量的值大于0时,信号量的值-1,当信号量的值等于0时,继续调用将造成线程阻塞。类比 pthread_mutex_lock。

int sem_wait(sem_t *sem);

成功返回0,失败返回-1

(5) sem_post 函数,给信号量解锁,将信号量值+1,同时唤醒阻塞在信号量上的线程。类比pthread_mutex_unlock。

int sem_post(sem_t *sem);

成功返回0,失败返回-1

(6) sem_trywait 函数,尝试对信号量加锁(与 sem_wait 的区别类比 lock 和 trylock)

int sem_trywait(sem_t *sem);

成功返回0,失败返回-1

sem_trywait 的工作方式与 sem_wait 类似,都是尝试获取一个信号量的资源,但是不同的是,如果信号量的值为 0,那么 sem_trywait 会立即返回一个错误代码 EAGAIN,表示无法获取信号量的资源。如果信号量的值大于 0,那么 sem_trywait 会将信号量的值减 1,并返回 0。

使用 sem_trywait 的好处是可以避免程序在等待信号量时被阻塞,从而提高程序的响应性能。通常情况下,sem_trywait 会与其他同步机制结合使用,例如在多线程编程中,可以使用 sem_trywait 来非阻塞地等待一个互斥锁的资源,从而避免死锁和竞态条件等问题。

(7) sem_timedwait 函数,限时尝试对信号量加锁

int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);

参2:abs_timeout 采用的是绝对时间
成功返回0,失败返回-1

sem_timedwait 的工作方式与 sem_wait 类似,都是尝试获取一个信号量的资源,但是不同的是,如果在指定的时间内信号量的值仍然为 0,那么 sem_timedwait 会返回一个错误代码 ETIMEDOUT,表示等待超时。如果在指定的时间内信号量的值变为了非零,那么 sem_timedwait 会将信号量的值减 1,并返回 0。

使用 sem_timedwait 的好处是可以避免程序无限期地等待一个信号量,从而提高程序的响应性能。通常情况下,sem_timedwait 会与定时器结合使用,例如使用 clock_gettime 函数获取当前时间,然后计算出一个未来的绝对时间,在这个时间内等待信号量的值变为非零。

#include<iostream>
#include<thread>
using namespace std;
#include<unistd.h>
#include<mutex>
#include<semaphore>
#include<vector>

//信号量
sem_t sem;

//临界资源
size_t n = 0;

//线程函数入口
void func()noexcept
{
	sem_wait(&sem);
        //临界区
	for(int i = 0;i < 100000000;++i)
	{
		++n;
	}
        //临界区
	sem_post(&sem);
}


int main()
{
        //初始化信号量,表明是线程间同步,同时只能允许一个线程得到锁
	sem_init(&sem,0,1);

        //创建5个线程
	vector<thread> pool;
	for(int i = 0;i < 5;++i)
	{
		pool.push_back(thread(func));
	}

	for(auto& c : pool)
	{
		c.join();
	}


	cout << "n = " << n << endl;

        //释放信号量
	sem_destroy(&sem);

	return 0;
}