多线程高级编程

多线程高级编程

1、线程同步(POSIX API)

多线程编程复杂度:并发异步机制带来了线程间资源竞争的无序性

为解决这一复杂度,因此引入线程同步机制,来实现线程间正确有序共享数据

线程同步:

概念:指一个线程发出某一功能调用时,在没有得到结果之前,该调用不返回。同时其它线程为保证数据一致性,不能调用该功能。

基本思想:同步各个线程对资源(如全局变量、文件)的访问。

所有“多个控制流,共同操作一个共享资源”的情况,都需要同步。

数据混乱原因:

  • 资源共享(独享资源则不会)
  • 调度随机(意味着数据访问会出现竞争)
  • 线程间缺乏必要的同步机制(可改)

死锁:

  • 线程试图对同一个互斥量A加锁两次(反复加锁)
  • 线程1拥有A锁,请求获得B锁;线程2拥有B锁,请求获得A锁

线程同步方法:

  • 互斥锁
  • 读写锁
  • 条件变量
  • 信号量

1)互斥锁mutex

概念:

互斥锁(也称互斥量)是线程同步的一种机制,用来保护多线程的共享资源。

工作流程:

  • 创建一个互斥锁
  • 初始化一个互斥锁
  • 在进入临界区前把互斥锁加锁(防止其他线程进入临界区)
  • 访问共享资源
  • 退出临界区时互斥锁解锁(让别的线程有机会进入临界区)
  • 最后不使用互斥锁时则销毁

注意:锁的粒度越小越好(访问共享数据前,加锁。访问结束立即解锁)

互斥锁,本质是结构体,我们可以看成初值为1 (pthread_mutex_init() 函数调用成功)

加锁:--操作 阻塞线程

结束:++操作 唤醒阻塞在锁上的线程

try锁:尝试加锁,成功--。失败,返回。同时设置错误号 EBUSY

操作函数:

  • pthread_mutex_init 函数
  • pthread_mutex_destroy 函数
  • pthread_mutex_lock 函数
  • pthread_mutex_trylock 函数
  • pthread_mutex_unlock 函数

以上5个函数的返回值都是:成功返回0,失败返回错误号

  • pthread_mutex_t 类型,其本质是一个结构体,但应用时可简单当成整数看待
  • pthread_mutex_t mutex 变量mutex只有两种取值1、0、

加锁和解锁

lock 尝试加锁,如果加锁不成功,线程阻塞,阻塞到持有该互斥量的其他线程解锁为止。

unlock 主动解锁函数,同时将阻塞在该锁上的所有线程全部唤醒,至于哪个线程先被唤醒,取决于优先级、调度。默认:先阻塞、先唤醒。

例如:T1 T2 T3 T4 使用一把 mutex 锁。T1 加锁成功,其他线程均阻塞,直至 T1 解锁。T1 解锁后,T2 T3 T4 均被唤醒,并自动再次尝试加锁。

可假想 mutex 锁 init 成功初值为 1。lock 功能是将 mutex--。而 unlock 则将 mutex++。

①pthread_mutex_init

动态初始化一个互斥锁 --> 初值可看作 1

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);

restrict关键字:用来限定指针变量。被该关键字限定的指针变量所指向的内存操作,必须由本指针完成

②pthread_mutex_lock

互斥锁上锁

int pthread_mutex_lock(pthread_mutex_t *mutex);

如果调用该函数时互斥锁已经被其它线程上锁了,则调用该函数的线程将阻塞

③pthread_mutex_trylock

互斥锁上锁

int pthread_mutex_lock(pthread_mutex_t *mutex);

如果调用该函数时互斥锁已经被其它线程上锁了,调用该函数的线程不会阻塞,而是立即返回,并且函数返回EBUSY

④pthread_mutex_unlock

互斥锁解锁

int pthread_mutex_unlock(pthread_mutex_t *mutex);

注意:pthread_mutex_unlockpthread_mutex_lock 要成对使用

⑤pthread_mutex_destroy

互斥锁销毁

int pthread_mutex_destroy(pthread_mutex_t *mutex);

//线程同步示例
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<pthread.h>
#include<unistd.h>

pthread_mutex_t mutex;  //定义一把互斥锁,mutex可以想象成一个整数,锁的值为1

void *tfn(void *arg)
{
	srand(time(NULL));
	while(1){
		pthread_mutex_lock(&mutex);    //互斥锁加锁,可以想象成 锁-- (1 --> 0)
		printf("hello");
		sleep(rand() % 3);  //模拟长时间操作共享资源,导致cpu易主,产生与时间有关的错误
		printf("world\n");
		pthread_mutex_unlock(&mutex);    //互斥锁解锁,可以想象成 锁++ (0 --> 1)

		sleep(rand() % 3);
	}
	return NULL;
}

int main()
{
	pthread_t tid;
	srand(time(NULL));
	int ret = pthread_mutex_init(&mutex, NULL);  //初始化互斥锁
	if(ret != 0){
		fprintf(stderr, "mutex init error:%s\n", strerror(ret));
		exit(1);
	}

	pthread_create(&tid, NULL, tfn, NULL);
	while(1){
		pthread_mutex_lock(&mutex);    //互斥锁加锁
		printf("HELLO");
		sleep(rand() % 3);  
		printf("WORLD\n");
		sleep(rand() % 3);
		pthread_mutex_unlock(&mutex);    //互斥锁解锁
	}
	pthread_join(tid, NULL);

	pthread_mutex_destroy(&mutex);   //销毁互斥锁
	
	return 0;
}

2)读写锁

与互斥量类似,但读写锁允许更高的并行性(当读线程多的时候,提高访问效率)。

对资源的访问会存在两种情况(两种状态):

  • 独占(写操作—写锁)
  • 共享(读操作—读锁)

读写锁特性

  • “写锁“时,解锁前,所有对该锁加锁的线程会被阻塞
  • ”读锁时“,若线程以读模式对其加锁会成功;若以写模式加锁会阻塞
  • ”读锁时“,既有试图以写模式加锁的线程,也有试图以读模式加锁的线程,那么读写锁会阻塞随后的读锁请求。优先满足写锁。读锁、写锁并行阻塞,写锁优先级高

读写锁也叫共享—独占锁。当读写锁以读模式锁住时,它是以共享模式锁住的;当它以写模式锁住时,它是以独占模式锁住的。写独占、读共享。

读写锁非常适合于对数据结构读的次数远大于写的情况。

主要应用函数
  • pthread_rwlock_init
  • pthread_rwlock_destroy 函数
  • pthread_rwlock_rdlock 函数
  • pthread_rwlock_wrlock 函数
  • pthread_rwlock_tryrdlock 函数
  • pthread_rwlock_trywrlock 函数
  • pthread_rwlock_unlock 函数

以上7个函数的返回值:成功返回0,失败返回错误号

  • pthread_rwlock_t 类型 用于定义一个读写锁变量
  • pthread_rwlock_t rwlock;
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>

int counter;
pthread_rwlock_t rwlock;    //全局的读写锁

//3个线程不定时写同一全局资源,5个线程不定时读同一全局资源
void *th_write(void *arg)
{
	int t;
	int i = (int)arg;   //long i = (long)arg
	while(1){
		pthread_rwlock_wrlock(&rwlock);   //以写模式加锁,写独占
		t = counter;
		usleep(1000);
		printf("======write %d: %lu: counter=%d ++counter=%d\n", i, pthread_self(), t, ++counter);
		pthread_rwlock_unlock(&rwlock);
		usleep(10000);
	}
	return NULL;
}

void *th_read(void *arg)
{
	int i = (int)arg;   //long i = (long)arg
	while(1){
		pthread_rwlock_rdlock(&rwlock);  //读线程间,读写共享
		printf("------read %d: %lu: %d\n", i, pthread_self(),counter);
		pthread_rwlock_unlock(&rwlock);
		usleep(2000);
	}
	return NULL;
}

int main(void)
{
	int i;
	pthread_t tid[8];
	
	pthread_rwlock_init(&rwlock, NULL);    //实际中注意检查返回值

	for(i = 0; i < 3; i++){
		pthread_create(&tid[i], NULL, th_write, (void *)i);
	}

	for(i = 0; i < 5; i++){
		pthread_create(&tid[i+3], NULL, th_read, (void *)i);
	}

	for(i = 0; i < 8; i++){
		pthread_join(tid[i], NULL);
	}

	pthread_rwlock_destroy(&rwlock);   //销毁读写锁
	
	return 0;
}

3)条件变量

条件变量本身不是锁。但它也可以造成线程阻塞。

通常与互斥锁配合使用。给多线程提供一个会合的场所。

为什么要使用条件变量

​ 线程抢占互斥锁时,线程A抢到了互斥锁,但是条件不满足,线程A就会让出互斥锁让给其他线程,然后等待其他线程唤醒他;一旦条件满足,线程就可以被唤醒,并且拿互斥锁去访问共享区。经过这中设计能让进程运行更稳定

主要应用函数

  • pthread_cond_init 函数
  • pthread_cond_destroy 函数
  • pthread_cond_wait 函数
  • pthread_cond_timedwait 函数
  • pthread_cond_signal 函数
  • pthread_cond_broadcast 函数

以上6个函数返回值:成功返回0,失败返回错误号

  • pthread_cond_t 类型 用于定义条件变量
  • pthread_cond_t cond;
①pthread_cond_init
//静态初始化:
pthread_cond_t cond=PTHREAD_COND_INITIALIZER
//动态初始化:
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
②pthread_cond_destroy

作用:
销毁一个条件变量

int pthread_cond_destroy(pthread_cond_t *cond);
③pthread_cond_wait 函数(重)

作用:(非常重要 三点)

1.阻塞等待条件变量 cond(参 1)满足
2.释放已掌握的互斥锁(解锁互斥量)相当于 pthread_mutex_unlock(&mutex);
3.当被唤醒,pthread_cond_wait 函数返回时,解除阻塞并重新申请获取互斥锁pthread_mutex_lock(&mutex);

1和2 两步为一个原子操作。

int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);

image-20230308155809480

④pthread_cond_timedwait

作用:
限时等待一个条件变量

int pthread_cond_timedwait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
⑤pthread_cond_signal

作用:
唤醒(至少)一个阻塞在条件变量上的线程

int pthread_cond_signal(pthread_cond_t *cond);
⑥pthread_cond_broadcast 函数

作用:
唤醒全部阻塞在条件变量上的线程

int pthread_cond_broadcast(pthread_cond_t *cond);

image-20230308161040746

条件变量使用步骤

消费者:

  • 创建锁 pthread_mutex_t mutex
  • 初始化锁 pthread_mutex_init(&mutex, NULL);
  • 加锁 pthread_mutex_lock(&mutex);
  • 等待条件满足 pthread_cond_wait(&cond, &mutex)
    • 阻塞条件变量
    • 解锁unlock(mutex)----- 10s
    • 加锁lock(mutex)
  • 访问共享数据
  • 解锁、释放条件变量。销毁锁

生产者:

  • 生产数据
  • 加锁pthread_mutex_lock(&mutex)
  • 将数据放置到 公共区域
  • 解锁 pthread_mutex_unlock(&mutex);
  • 通知阻塞在条件变量上的线程
    • pthread_cond_signal()
    • pthread_cond_broadcast()
  • 循环生产后面的数据
/*借助条件变量模拟  生产者-消费者  问题*/

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

void err_thread(int ret, char *str)
{
	if(ret != 0){
		fprintf(stderr, "%s:%s\n", str, strerror(ret));
		pthread_exit(NULL);
	}
}

/*链表作为共享数据,需被互斥量保护*/
struct msg
{
	struct msg *next;
	int num;
};

struct msg *head;

/*静态初始化 一个条件变量 和 一个互斥量*/
pthread_cond_t has_data = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void *producer(void *arg)
{
	struct msg *mp;

	while (1){	
		mp=(struct msg*)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);  //将等待在该条件变量上的一个线程唤醒
	
		sleep(rand() % 3);
	}
		return NULL;
}

void *consumer(void *arg)
{
	struct msg *mp;
	
	while(1){
		pthread_mutex_lock(&mutex);
		if(head == NULL){         //头指针为空,说明没有节点  可以为if吗
			pthread_cond_wait(&has_data, &mutex);  //阻塞等待条件变量
		}                                          //pthread_cond_wait 返回时,重写加锁mutex
		mp = head;
		head = mp->next;              //模拟消费掉一个产品
		
		pthread_mutex_unlock(&mutex);  //解锁 互斥量

		printf("-Consume %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, producer, NULL);   //生产者
	if(ret != 0){
		err_thread(ret, "pthread_create producer eoor");
	}

	ret = pthread_create(&cid, NULL, consumer, NULL);    //消费者
	if(ret != 0){
		err_thread(ret, "pthread_create consumer eoor");
	}

	pthread_join(pid, NULL);
	pthread_join(cid, NULL);

	return 0;
}

4)信号量semaphore

应用于线程、进程间同步

相当于进化版的互斥量——初始化值为N,可以允许N个线程同时访问共享资源

  • 简单的说就是进化版的互斥锁(1~N)计数器
  • 记录当前可利用的资源数,当资源数量<=0时会阻塞,当资源数量>时才开始进行操作,另外信号量的操作均为原子操作

信号量是相对折中的一种处理方式,既能保证同步,数据不混乱,又能提高线程并发

主要应用函数:

  • sem_init 函数
  • sem_destroy 函数
  • sem_wait 函数
  • sem_trywait 函数
  • sem_timedwait 函数
  • sem_post 函数

以上6个函数的返回值:

成功返回0,失败返回-1;同时设置errno。(注意:它们没有pthread前缀)

  • sem_t 类型,本质仍是结构体。但应用期间可简单看作为整数,忽略实现细节(类似于使用文件描述符)
  • sem_t sem; 规定信号量不能<0。头文件<semaphore.h>

信号量基本操作:

sem_wait: (类比pthread_mutex_lock

一次调用,做一次--操作,当信号量的值为0时,再次--,就会阻塞

sem_post: (类比pthread_mutex_unlock

一次调用,做一次++操作,当信号量的值为N时,再次++就会阻塞

但由于sem_t的实现对用户隐藏,所以所谓的++、--操作只能通过函数来实现,而不能直接++、--符号。

①sem_init 函数

作用:
初始化一个信号量

int sem_init(sem_t *sem, int pshared, unsigned int value);
  • 参 1:sem 信号量
  • 参 2:pshared 取 0 用于线程间;取非 0(一般为 1)用于进程间
  • 参 3:value 指定信号量初值(N值)
  • 无静态初始化

信号量的初值,决定了占用信号量的线程(进程)的个数。

②sem_destroy 函数

作用:

销毁一个信号量

int sem_destroy (sem_t *sem);
③sem_wait 函数

作用:
给信号量加锁 –

int sem_wait(sem_t *sem);
④sem_post 函数

作用:
给信号量解锁 ++

int sem_post(sem_t *sem);
⑤sem_trywait 函数

作用:
尝试对信号量加锁 (与 sem_wait 的区别类比 locktrylock)

int sem_trywait(sem_t *sem);
⑥sem_timedwait 函数

作用:
限时尝试对信号量加锁

int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
  • 参 2:abs_timeout 采用的是绝对时间。
  • 定时1秒:

image-20230308200152341

信号量—>生产者消费者问题:

用两个信号量来控制,一个初始为0,表示一开始生产的数量为0。另外一个初始的数量是可以存放商品的空格数,初始化就是格子数N。

image-20230308202834551

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

#define NUM 5

int queue[NUM];
sem_t blank_number, product_number;   //两个信号量控制

void *producer(void *arg)  //生产
{
	int p = 0;
	while (1)
	{
		sem_wait(&blank_number); //空格位置--
		queue[p] = rand() % 1000 + 1; //放入队列
		printf("-----product---- %d \n", queue[p]);
		sem_post(&product_number); //生产数量++

		p = (p + 1) % NUM;  //借助下标 实现循环放入 模拟队列
		sleep(rand() % 1);
	}
}

void *consumer(void *arg)  //消费者
{
	int c = 0;
	while (1)
	{
		sem_wait(&product_number); //产品数量--
		printf("-Consumer--- %d \n", queue[c]);
		queue[c] = 0;
		sem_post(&blank_number); //空格数量++
		c = (c + 1) % NUM;
		sleep(rand() % 3);
	}
}

int main()
{
	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;
}

2、C++11/14中的线程同步

C++11/14提供两种方式进行线程同步

  • 互斥锁(较多)
  • 条件变量(较少,自行了解)

头文件:

#include < mutex >

C++11中互斥锁有4种,并对应着4种不同的类

  • 基本互斥锁,对应类为std::mutex
  • 递归互斥锁,对应类为std::recursive_mutex
  • 定时互斥锁,对应类为 std::time_mutex
  • 定时递归互斥锁,对应类为 std::time_mutex

1)基本互斥锁

std::mutex的成员函数

成员函数 说明
mutex 构造函数
lock 互斥锁上锁
Try_lock 如果互斥锁没有上锁,则上锁
native_handle 得到本地互斥锁句柄

上锁—解锁

void lock();
void unlock();      //两者都要被调用线程配对使用

函数lock尝试锁住互斥锁:

  • 如果互斥锁当前没有被上锁,则当前线程可以成功上锁,即当前线程拥有互斥锁,直到当前线程调用解锁函数unlock

  • 如果互斥锁已经被其他线程上锁了,则当前线程挂起,直到互斥锁被其它线程解锁。

  • 如果当前互斥锁被上锁,再次调用当前线程上锁,则会产生死锁

//多线程统计计数器到10万

#include<iostream>
#include<thread>
#include<mutex>
using namespace std;

volatile int counter(0);   //定义一个全局变量,当作计数器,用于累加
mutex mtx;

void thrfunc()
{
	for(int i = 0; i < 10000; i++){
		mtx.lock();  //互斥锁上锁
		++counter;   //计数器累加
		mtx.unlock();  //互斥锁解锁
	}
}

int main(int argc, char *argv[])
{
	thread threads[10];

	for(int i = 0; i < 10; i++)
		threads[i] = thread(thrfunc);  //启动10个线程

	for(auto& th:threads) th.join();  //等待10个线程结束
	cout << "count to " << counter << " successfully \n";

	return 0;
}

2)定时互斥锁

std::time_mutex成员函数

成员函数 说明
mutex 构造函数
lock 互斥锁上锁
try_lock 若互斥锁没有上锁,则努力上锁,但不阻塞
try_lock_for 若互斥锁没有上锁,则努力一段时间上锁,这段时间内阻塞,过了这段时间就退出
try_lock_until 努力上锁,知道某个时间点,时间点到达之前将一直阻塞
native_handle 得到本地互斥锁句柄

函数try_lock尝试锁住互斥锁,若互斥锁被其他线程占有,则当前线程也不会被阻塞,线程调用会出现3种情况:

  • 如果当前互斥锁没有被其它线程锁住,则该线程锁住互斥锁,知道调用unlock释放互斥锁
  • 如果当前互斥锁被其他线程锁住,则当前调用线程返回false,而并不会被阻塞掉
  • 如果当前互斥锁被当前调用线程锁住,则会产生死锁
bool try_lock();  //注意有下画线

如果函数成功上锁,则返回true,否则返回false。该函数不会阻塞,不能上锁时立即返回false

//用非阻塞上锁版本改写——多线程统计计数器到10万

#include<iostream>
#include<thread>
#include<mutex>
using namespace std;

volatile int counter(0);   //定义一个全局变量,当作计数器,用于累加
mutex mtx;                 //用户保护counter的互斥锁

void thrfunc()
{
	if(mtx.try_lock())   //互斥锁上锁
	{
		++counter;
		mtx.unlock();    //互斥锁解锁
	}
	else
	{
		cout << "try_lock false\n";
	}
}

int main(int argc, char *argv[])
{
	thread threads[10];

	for(int i = 0; i < 10; i++)
		threads[i] = thread(thrfunc);  //启动10个线程

	for(auto& th:threads) th.join();  //等待10个线程结束
	cout << "count to " << counter << " successfully \n";

	return 0;
}
posted @ 2023-08-22 17:57  洋綮  阅读(20)  评论(0)    收藏  举报