线程同步和互斥(十三)

一 线程通信

  由于线程是共享内存的,故线程之间的通信其实也就是资源共享,不像进程之间的通信。其实更多的是线程之间的同步与互斥。
  1、使用全局变量:主要由于多个线程可能更改全局变量,因此全局变量最好声明为violate。
  2、使用消息实现通信。
  3、使用事件CEvent类实现线程间通信。

二 线程同步互斥

  互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
  同步:主要是流程上的概念,是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源。

三 互斥锁

  一般条件变量需要结合互斥量一起使用:
  1、初始化锁
  int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutex_attr_t *mutexattr);
  其中参数 mutexattr 用于指定锁的属性(见下),如果为NULL则使用缺省属性。
  互斥锁的属性在创建锁的时候指定,在LinuxThreads实现中仅有一个锁类型属性,不同的锁类型在试图对一个已经被锁定的互斥锁加锁时表现不同。当前有四个值可供选择:
  (1)PTHREAD_MUTEX_TIMED_NP,这是缺省值,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性。
  (2)PTHREAD_MUTEX_RECURSIVE_NP,嵌套锁,允许同一个线程对同一个锁成功获得多次,并通过多次unlock解锁。如果是不同线程请求,则在加锁线程解锁时重新竞争。
  (3)PTHREAD_MUTEX_ERRORCHECK_NP,检错锁,如果同一个线程请求同一个锁,则返回EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP类型动作相同。这样就保证当不允许多次加锁时不会出现最简单情况下的死锁。
  (4)PTHREAD_MUTEX_ADAPTIVE_NP,适应锁,动作最简单的锁类型,仅等待解锁后重新竞争。
  2、阻塞加锁
  int pthread_mutex_lock(pthread_mutex *mutex);
  3、非阻塞加锁
  int pthread_mutex_trylock( pthread_mutex_t *mutex);
该函数语义与 pthread_mutex_lock() 类似,不同的是在锁已经被占据时返回 EBUSY 而不是挂起等待。
  4、解锁(要求锁是lock状态,并且由加锁线程解锁)
  int pthread_mutex_unlock(pthread_mutex *mutex);
  5、销毁锁(此时锁必需unlock状态,否则返回EBUSY)
  int pthread_mutex_destroy(pthread_mutex *mutex);

#include <stdio.h>
#include <pthread.h>
 
void *ticketsell1(void *);
void *ticketsell2(void *);
int tickets = 20;
pthread_mutex_t mutex;
 
int main()
{
	pthread_t id1,id2;
	pthread_mutex_init(&mutex, NULL);//
	int error;
 
	error = pthread_create(&id1, NULL, ticketsell1, NULL);
	if(error != 0)
	{
		printf("pthread is not created!\n");
		return -1;
	}
 
	error = pthread_create(&id2, NULL, ticketsell2, NULL);
	if(error != 0)
	{
		printf("pthread is not created!\n");
		return -1;
	}
 
	pthread_join(id1,NULL);
	pthread_join(id2,NULL);
	
	return 0;
}
 
void *ticketsell1(void *arg)
{
	while(1)
	{
		pthread_mutex_lock(&mutex);//给互斥量上锁
		if(tickets > 0)
		{
			usleep(1000);
			printf("ticketse1 sells ticket:%d\n",tickets--);
			pthread_mutex_unlock(&mutex);//给互斥量解锁
			
		}
		else
		{
			pthread_mutex_unlock(&mutex);//给互斥量解锁
			break;
		}
		pthread_yield();//线程调度函数,使每个线程都有执行机会
	}
	return (void *)0;
}
 
void *ticketsell2(void *arg)
{
	while(1)
	{
		pthread_mutex_lock(&mutex);//给互斥量上锁
		if(tickets > 0)
		{
			usleep(1000);
			printf("ticketse2 sells ticket:%d\n",tickets--);
			pthread_mutex_unlock(&mutex);//给互斥量解锁
		}
		else
		{
			pthread_mutex_unlock(&mutex);//给互斥量解锁
			break;
		}
		pthread_yield();//线程调度函数,是两个线程都有执行机会
	}
 
	return (void *)0;
}

四 信号量

  如同进程一样,线程也可以通过信号量来实现通信,虽然是轻量级的。
  线程使用的基本信号量函数有四个:
  #include <semaphore.h>
  1、初始化信号量
  int sem_init (sem_t *sem , int pshared, unsigned int value);
  参数:
  sem - 指定要初始化的信号量;
  pshared - 信号量 sem 的共享选项,linux只支持0,表示它是当前进程的局部信号量;
  value - 信号量 sem 的初始值。
  2、信号量值加1
  给参数sem指定的信号量值加1。
  int sem_post(sem_t *sem);
  3、信号量值减1
  给参数sem指定的信号量值减1。
  int sem_wait(sem_t *sem);
  如果sem所指的信号量的数值为0,函数将会等待直到有其它线程使它不再是0为止。
  4、销毁信号量
  销毁指定的信号量。
  int sem_destroy(sem_t *sem);

#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
 
void *ticketsell1(void *);
void *ticketsell2(void *);
int tickets = 20;
sem_t mutex,full;
 
int main()
{
	pthread_t id1,id2;
	int error;
	int ret;
 
	ret = sem_init(&mutex, 0 ,1);//初始化mutex信号量为1
	ret += sem_init(&full, 0 ,0);//初始化full信号量为0
 
	if(ret != 0)
	{
		printf("sem_init fails!\n");
	}
 
	error = pthread_create(&id1, NULL, ticketsell1, NULL);
	if(error != 0)
	{
		printf("pthread is not created!\n");
		return -1;
	}
 
	error = pthread_create(&id2, NULL, ticketsell2, NULL);
	if(error != 0)
	{
		printf("pthread is not created!\n");
		return -1;
	}
 
	pthread_join(id1,NULL);
	pthread_join(id2,NULL);
	
	return 0;
}
 
void *ticketsell1(void *arg)
{
	while(1)
	{
		sem_wait(&mutex);//mutex信号量进行P操作
		if(tickets > 0)
		{
			usleep(1000);
			printf("ticketse1 sells ticket:%d\n",tickets--);
			sem_post(&full);//full信号量进行V操作
		}
		else
		{
			sem_post(&full);//full信号量进行V操作
			break;
		}
	}
	return (void *)0;
}
 
void *ticketsell2(void *arg)
{
	while(1)
	{
		sem_wait(&full);//full信号量进行P操作
		if(tickets > 0)
		{
			usleep(1000);
			printf("ticketse2 sells ticket:%d\n",tickets--);
			sem_post(&mutex);//mutex信号量进行V操作
		}
		else
		{
			sem_post(&mutex);//mutex信号量进行V操作
			break;
		}
	}
 
	return (void *)0;
}

  上面的sem_init函数用来初始化两个信号量的初始化值,这里一个设为1,一个设为0,sem_wait类似于P操作,让信号量减1,如果小于结果小于0,线程阻塞,否则线程继续执行,sem_post类似于V操作,提升信号量的值,加1,通过这两个信号量之间的互相“救对方”,就可以实现这两个线程的同步执行。

五 条件变量

  条件变量是利用线程间共享全局变量进行同步的一种机制。条件变量上的基本操作有:触发条件(当条件变为 true 时);等待条件,挂起线程直到其他线程触发条件。
  1、初始化条件变量
  int pthread_cond_init(pthread_cond_t *cond,pthread_condattr_t *cond_attr);
  尽管POSIX标准中为条件变量定义了属性,但在Linux中没有实现,因此cond_attr值通常为NULL,且被忽略。
  2、有两个等待函数
  (1)无条件等待
  int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);
  (2)计时等待
  int pthread_cond_timewait(pthread_cond_t *cond,pthread_mutex *mutex,const timespec *abstime);
  如果在给定时刻前条件没有满足,则返回ETIMEOUT,结束等待,其中abstime以与time()系统调用相同意义的绝对时间形式出现,0表示格林尼治时间1970年1月1日0时0分0秒。

无论哪种等待方式,都必须和一个互斥锁配合,以防止多个线程同时请求(用 pthread_cond_wait() 或 pthread_cond_timedwait() 请求)竞争条件(Race Condition)。mutex互斥锁必须是普通锁(PTHREAD_MUTEX_TIMED_NP)或者适应锁(PTHREAD_MUTEX_ADAPTIVE_NP),且在调用pthread_cond_wait()前必须由本线程加锁(pthread_mutex_lock()),而在更新条件等待队列以前,mutex保持锁定状态,并在线程挂起进入等待前解锁。在条件满足从而离开pthread_cond_wait()之前,mutex将被重新加锁,以与进入pthread_cond_wait()前的加锁动作对应。

  3、激发条件
  (1)激活一个等待该条件的线程(存在多个等待线程时按入队顺序激活其中一个)
  int pthread_cond_signal(pthread_cond_t *cond);
  (2)激活所有等待线程
  int pthread_cond_broadcast(pthread_cond_t *cond);
  4、销毁条件变量
  int pthread_cond_destroy(pthread_cond_t *cond);
  只有在没有线程在该条件变量上等待的时候才能销毁这个条件变量,否则返回EBUSY。
  说明
  1. pthread_cond_wait 自动解锁互斥量(如同执行了pthread_unlock_mutex),并等待条件变量触发。这时线程挂起,不占用CPU时间,直到条件变量被触发(变量为ture)。在调用 pthread_cond_wait之前,应用程序必须加锁互斥量。pthread_cond_wait函数返回前,自动重新对互斥量加锁(如同执行了pthread_lock_mutex)。
  2. 互斥量的解锁和在条件变量上挂起都是自动进行的。因此,在条件变量被触发前,如果所有的线程都要对互斥量加锁,这种机制可保证在线程加锁互斥量和进入等待条件变量期间,条件变量不被触发。条件变量要和互斥量相联结,以避免出现条件竞争——个线程预备等待一个条件变量,当它在真正进入等待之前,另一个线程恰好触发了该条件(条件满足信号有可能在测试条件和调用pthread_cond_wait函数(block)之间被发出,从而造成无限制的等待)。
  3. 条件变量函数不是异步信号安全的,不应当在信号处理程序中进行调用。特别要注意,如果在信号处理程序中调用 pthread_cond_signal 或 pthread_cond_boardcast 函数,可能导致调用线程死锁

#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
 
void *ticketsell1(void *);
void *ticketsell2(void *);
int tickets = 20;
pthread_mutex_t mutex;
pthread_cond_t qready = PTHREAD_COND_INITIALIZER;//静态初始化条件变量;
		
int main()
{
	pthread_t id1,id2;
	pthread_mutex_init(&mutex, NULL);
	int error;
 
	error = pthread_create(&id1, NULL, ticketsell1, NULL);
	if(error != 0)
	{
		printf("pthread is not created!\n");
		return -1;
	}
 
	error = pthread_create(&id2, NULL, ticketsell2, NULL);
	if(error != 0)
	{
		printf("pthread is not created!\n");
		return -1;
	}
 
	pthread_join(id1,NULL);
	pthread_join(id2,NULL);
	
	return 0;
}
 
void *ticketsell1(void *arg)
{
	pthread_mutex_lock(&mutex);
	while(tickets > 0)
	{
		if(tickets%2 == 1)
		{
			usleep(1000);
			printf("ticketse1 sells ticket:%d\n",tickets--);
			pthread_cond_signal(&qready);//条件改变,发送信号,通知ticketse2
		}
		else
		{
			pthread_cond_wait(&qready,&mutex);//解开Mutex,并等待qready改变
		}
		pthread_mutex_unlock(&mutex);//给互斥量解锁
	}
	return (void *)0;
}
 
void *ticketsell2(void *arg)
{
	pthread_mutex_lock(&mutex);
	while(tickets > 0)
	{
		if(tickets%2 == 0)
		{
			usleep(1000);
			printf("ticketse2 sells ticket:%d\n",tickets--);
			pthread_cond_signal(&qready);//条件改变,发送信号,通知ticketse1
		}
		else
		{
			pthread_cond_wait(&qready,&mutex);//解开mutex,并等待qready改变
		}
		pthread_mutex_unlock(&mutex);//给互斥量解锁
	}
 
	return (void *)0;
}

  条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,它常和互斥锁一起使用。使用时,条件变量被用来阻塞一个线程,当条件不满足时,线程往往解开相应的互斥锁并等待条件变量发生变化。一旦其它的某个线程改变了条件变量,它将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。这些线程将重新锁定互斥锁并重新测试条件是否满足。一般说来,条件变量被用来进行线程间的同步。
  函数pthread_cond_wait使线程阻塞在一个条件变量上,而函数pthread_cond_signal是用来释放被阻塞在条件变量上的一个线程。但是要注意的是,条件变量只是起到阻塞和唤醒线程的作用,具体的判断条件还需用户给出,我这里给出的是tickets是否是偶数这个条件。

六 读写锁

  说明:

  1、如果一个线程用读锁锁定了临界区,那么其他线程也可以用读锁来进入临界区,这样就可以多个线程并行操作。但这个时候,如果再进行写锁加锁就会发生阻塞,写锁请求阻塞后,后面如果继续有读锁来请求,这些后来的读锁都会被阻塞!这样避免了读锁长期占用资源,防止写锁饥饿!
  2、如果一个线程用写锁锁住了临界区,那么其他线程不管是读锁还是写锁都会发生阻塞!

  1、初始化:
  int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
  2、读写加锁
  int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
  int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
  int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

  int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
  int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

  int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock, const struct timespec *restrict abs_timeout);
  int pthread_rwlock_timedwrlock(pthread_rwlock_t *restrict rwlock, const struct timespec *restrict abs_timeout);
  3、销毁锁
  int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

/* 初始化读写锁 */
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
/* 全局资源 */
int global_num = 10;

void err_exit(const char *err_msg)
{
 printf("error:%s\n", err_msg);
 exit(1);
}

/* 读锁线程函数 */
void *thread_read_lock(void *arg)
{
 char *pthr_name = (char *)arg;

 while (1)
 {
     /* 读加锁 */
     pthread_rwlock_rdlock(&rwlock);

     printf("线程%s进入临界区,global_num = %d\n", pthr_name, global_num);
     sleep(1);
     printf("线程%s离开临界区...\n", pthr_name);

     /* 读解锁 */
     pthread_rwlock_unlock(&rwlock);

     sleep(1);
 }

 return NULL;
}

/* 写锁线程函数 */
void *thread_write_lock(void *arg)
{
 char *pthr_name = (char *)arg;

 while (1)
 {
     /* 写加锁 */
     pthread_rwlock_wrlock(&rwlock);

     /* 写操作 */
     global_num++;
     printf("线程%s进入临界区,global_num = %d\n", pthr_name, global_num);
     sleep(1);
     printf("线程%s离开临界区...\n", pthr_name);

     /* 写解锁 */
     pthread_rwlock_unlock(&rwlock);

     sleep(2);
 }

 return NULL;
}

int main(void)
{
 pthread_t tid_read_1, tid_read_2, tid_write_1, tid_write_2;

 /* 创建4个线程,2个读,2个写 */
 if (pthread_create(&tid_read_1, NULL, thread_read_lock, "read_1") != 0)
     err_exit("create tid_read_1");

 if (pthread_create(&tid_read_2, NULL, thread_read_lock, "read_2") != 0)
     err_exit("create tid_read_2");

 if (pthread_create(&tid_write_1, NULL, thread_write_lock, "write_1") != 0)
     err_exit("create tid_write_1");

 if (pthread_create(&tid_write_2, NULL, thread_write_lock, "write_2") != 0)
     err_exit("create tid_write_2");

 /* 随便等待一个线程,防止main结束 */
 if (pthread_join(tid_read_1, NULL) != 0)
     err_exit("pthread_join()");

 return 0;
}

“锁”事连篇

mutex(互斥锁):

  互斥锁主要用于实现内核中的互斥访问功能。对它的访问必须遵循一些规则:同一时间只能有一个任务持有互斥锁,而且只有这个任务可以对互斥锁进行解锁。互斥锁不能进行递归锁定或解锁。

semaphore (信号量):

  信号量在创建时需要设置一个初始值,表示同时可以有几个任务可以访问该信号量保护的共享资源,初始值为1就变成互斥锁(Mutex),即同时只能有一个任务可以访问信号量保护的共享资源。一个任务要想访问共享资源,首先必须得到信号量,获取信号量的操作将把信号量的值减1,若当前信号量的值为负数,表明无法获得信号量,该任务必须挂起在该信号量的等待队列等待该信号量可用;若当前信号量的值为非负数,表示可以获得信号量,因而可以立刻访问被该信号量保护的共享资源。当任务访问完被信号量保护的共享资源后,必须释放信号量,释放信号量通过把信号量的值加1实现,如果信号量的值为非正数,表明有任务等待当前信号量,因此它也唤醒所有等待该信号量的任务。

rw_semaphore (读写信号量):

  读写信号量对访问者进行了细分,或者为读者,或者为写者,读者在保持读写信号量期间只能对该读写信号量保护的共享资源进行读访问,如果一个任务除了需要读,可能还需要写,那么它必须被归类为写者,它在对共享资源访问之前必须先获得写者身份,写者在发现自己不需要写访问的情况下可以降级为读者。读写信号量同时拥有的读者数不受限制,也就说可以有任意多个读者同时拥有一个读写信号量。如果一个读写信号量当前没有被写者拥有并且也没有写者等待读者释放信号量,那么任何读者都可以成功获得该读写信号量;否则,读者必须被挂起直到写者释放该信号量。如果一个读写信号量当前没有被读者或写者拥有并且也没有写者等待该信号量,那么一个写者可以成功获得该读写信号量,否则写者将被挂起,直到没有任何访问者。因此,写者是排他性的,独占性的。

Spanlock(自旋锁):

  自旋锁与互斥锁有点类似,只是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,”自旋”一词就是因此而得名。由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁的效率远高于互斥锁。
  信号量和读写信号量适合于保持时间较长的情况,它们会导致调用者睡眠,因此只能在进程上下文使用(_trylock的变种能够在中断上下文使用),而自旋锁适合于保持时间非常短的情况,它可以在任何上下文使用。如果被保护的共享资源只在进程上下文访问,使用信号量保护该共享资源非常合适,如果对共巷资源的访问时间非常短,自旋锁也可以。但是如果被保护的共享资源需要在中断上下文访问(包括底半部即中断处理句柄和顶半部即软中断),就必须使用自旋锁。
自旋锁保持期间是抢占失效的,而信号量和读写信号量保持期间是可以被抢占的。自旋锁只有在内核可抢占或SMP的情况下才真正需要,在单CPU且不可抢占的内核下,自旋锁的所有操作都是空操作。

  跟互斥锁一样,一个执行单元要想访问被自旋锁保护的共享资源,必须先得到锁,在访问完共享资源后,必须释放锁。如果在获取自旋锁时,没有任何执行单元保持该锁,那么将立即得到锁;如果在获取自旋锁时锁已经有保持者,那么获取锁操作将自旋在那里,直到该自旋锁的保持者释放了锁。
  无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。

参考文章

linux c 线程间同步(通信)的几种方法–互斥锁,条件变量,信号量,读写锁https://blog.csdn.net/vertor11/article/details/55657619
Linux中的各种锁https://blog.csdn.net/shenwansangz/article/details/50450769
自旋锁、阻塞锁、可重入锁、悲观锁、乐观锁、读写锁、偏向所、轻量级锁、重量级锁、锁膨胀、对象锁和类锁https://blog.csdn.net/a314773862/article/details/54095819
深入理解各种锁https://www.jianshu.com/p/5725db8f07dc

发布了67 篇原创文章 · 获赞 26 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/essity/article/details/86524131