一文快速学会C语言多线程相关使用

本文对多线程的常用操作用法进行简单介绍,适合给进行C/C++开发使用多线程的朋友了解。

介绍三个变量

分别是线程ID,互斥锁和条件变量,此处将其组合为一个结构体使用

typedef struct S1
{
	pthread_t id;//线程的ID,或者叫线程的标识符,作为传出参数使用
	pthread_mutex_t mtx;//互斥锁,用于保护内部共享资源
	pthread_cond_t cond;//条件变量,结合互斥锁或阻塞自身线程使用
    int num;//测试用的变量
}S1;

在使用条件变量和互斥锁前需要进行初始化,初始化的函数为:

pthread_cond_init()函数和pthread_mutex_init()函数

他们都需要传2个参数,如下举例

//第一个参数是mutex或者cond的指针,第二个是要使用变量的自定义属性,一般指定为空
pthread_mutex_init(&s1->mtx,NULL);
pthread_cond_init(&s1->cond,NULL);

在mutex和cond使用后都需要调用对应的destory()函数来销毁

pthread_mutex_destroy(&s1->mtx);
pthread_cond_destroy(&s1->cond);

创建线程的函数为pthread_create();

当线程被创建出来后就会执行,而多线程的执行任务顺序是不确定的,可能主线程先也可能子线程先

//函数需要四个参数,分别是线程ID,当线程创建成功后,会将id填入这个传入的id
//第二个是线程的属性,可以手动控制一些参数,比如线程的调度策略、栈大小等。一般指定为空
//第三个是创建的线程要执行什么函数,填入的是函数的地址,这个函数可以有返回值
//第四个是线程执行函数需要什么参数,此处传入的s1是一个上方定义的S1结构体变量地址
pthread_create(&s1->id,NULL,sub11,s1);

和pthread有关的两个操作函数

pthread_join()

//假设在线程1当中调用子线程join操作,则会等到子线程执行完毕后再继续往下执行,其中的threadID变量类型为之前定义结构体当中的thread_t,第二个传入参数是子线程工作完毕后的返回值
//因此只有当子线程执行完毕后才会输出下方的123
pthread_join(thread_id, &value_ptr)
printf("1234\n");

pthread_detach()

//对子线程detach操作,则当前的线程不会等待子线程结束,子线程直接分离由系统接管处理结果,意味着会直接输出接下来的123
pthread_detach(threadID)
printf("123\n");

如果你能够保证在main函数退出前,子线程操作的结果是无误的或者子线程操作的参数与其他线程或者main函数无关,那就可以不使用join和detach操作,子线程执行从被create的时机开始,并非从join或者detach开始。

函数操作目的是防止某条创建了子线程的线程退出时,销毁了某些数据,导致子线程操作的数据异常。

互斥锁的解锁和加锁用法,相关的三个常用函数

pthread_mutex_trylock(&s1->mtx);

trylock函数会对一个初始化好的mutex进行加锁尝试,对其尝试加锁成功会有返回值为0,尝试加锁失败返回值是非0数值

pthread_mutex_lock(&s1->mtx);

lock函数也是对一个初始化好的mutex进行加锁尝试,对其尝试加锁成功会有返回值为0,尝试加锁失败返回值是非0数值。
和trylock的区别在于,trylock加锁失败后会继续执行后续代码而非不断尝试加锁,而lock是会不断的尝试加锁。

假设A线程对mutex加锁,B线程也对mutex加锁,假设A线程先加锁成功,B线程直接阻塞,直到线程A解锁,B才可以加锁成功

//如果线程A对mutex进行lock加锁,之后A又lock加锁一次,此时就会发生死锁,无法解开,如果使用trylock则是发现无法加锁直接往下走了

pthread_mutex_unlock(&s1->mtx);

unlock函数会对mutex进行解锁尝试,解锁成功返回0,解锁失败返回非0

条件变量的相关使用

pthread_cond_wait()函数

该函数需要两个参数,一个是条件变量,一个是mutex

pthread_cond_wait(&s1->cond,&s1->mtx);

调用wait函数时,会对这一个mutex进行解锁操作,之后阻塞自身线程

举例:

A线程和B线程都要对mtx进行操作,A线程成功抢先执行

A线程对mtx进行加锁操作,那么B就阻塞在lock中,之后当A调用了wait函数,wait函数传入了cond和mtx参数

此时wait函数会先解锁mtx,条件变量会阻塞A线程,B线程加锁成功,B执行了某些操作后通过下方函数唤醒了条件变量,并进行解锁

pthread_cond_signal(&s1->cond);

此时的A被唤醒后,会对mtx进行加锁操作,加锁成功后继续执行代码。

假设线程B是先放信号再解锁,对线程A来说无非就是再等待一会,只要B进行了解锁操作,线程A就可以在wait唤醒后进行加锁,之后依旧正常运行。以下的代码可以论证

void* sub12(void* arg)
{
	printf("子线程被创建\n");
	S1 *s1 = (S1*)arg;
	pthread_mutex_lock(&s1->mtx);
	printf("子线程加锁了\n");
	pthread_cond_signal(&s1->cond);
	printf("子线程放信号了\n");
	printf("子线程睡眠3S\n");
	Sleep(3000);//假设进行了某些操作
	s1->num = 100;
	pthread_mutex_unlock(&s1->mtx);
	printf("子线程end\n");
	return NULL;
}

int main()
{
	S1* s1= (S1*)malloc(sizeof (S1));
	s1->num = 0;
	pthread_mutex_init(&s1->mtx,NULL);
	pthread_cond_init(&s1->cond,NULL);

	pthread_create(&s1->id,NULL,sub12,s1);
	
	pthread_mutex_lock(&s1->mtx);
	printf("主线程加锁了\n");
	printf("主线程睡眠5S\n");
	Sleep(5000);//假设进行了某些操作
	s1->num = 10;
	printf("主线程操作后的num:%d\n",s1->num);
	printf("主线程阻塞等待信号\n");
	pthread_cond_wait(&s1->cond,&s1->mtx);
	
	printf("主线程收到信号解除了阻塞\n");
	printf("主线程end\n");
	return 0;
}

需要注意的是,如果B先发送信号,而B不进行解锁,那A没有加锁机会,就直接造成程序死锁了

模拟工作流程

结合先前的结构体,对互斥锁,信号量的应用,这个函数不涉及join和detach操作,最后正常结束,也证明如果能保证子线程正常,可以不使用join或者detach操作

定义一个子线程调用的操作函数

void* sub11(void* arg)
{
	printf("子线程被创建\n");
	S1 *s1 = (S1*)arg;
	printf("子线程内部打印的线程id:%d\n",s1->id);
	pthread_mutex_lock(&s1->mtx);
	printf("子线程加锁了\n");
	printf("子线程睡眠3S\n");
	Sleep(3000);//假设进行了某些操作
	s1->num = 100;
	printf("子线程操作后的num:%d\n",s1->num);
	pthread_mutex_unlock(&s1->mtx);
	printf("子线程发出信号\n");
	pthread_cond_signal(&s1->cond);
	return NULL;
}

主函数如下

int main()
{
	S1* s1= (S1*)malloc(sizeof (S1));
	s1->num = 0;
	pthread_mutex_init(&s1->mtx,NULL);
	pthread_cond_init(&s1->cond,NULL);
	printf("创建子线程\n");
	pthread_create(&s1->id,NULL,sub11,s1);
	printf("主线程打印的子线程id:%d\n",s1->id);
	
	pthread_mutex_lock(&s1->mtx);
	printf("主线程加锁了\n");
	printf("主线程睡眠5S\n");
	Sleep(5000);//假设进行了某些操作
	s1->num = 10;
	printf("主线程操作后的num:%d\n",s1->num);
	printf("主线程阻塞等待信号\n");
	pthread_cond_wait(&s1->cond,&s1->mtx);
	
	printf("主线程收到信号解除了阻塞\n");
	//尝试加锁
	int res = pthread_mutex_trylock(&s1->mtx);
	printf("主线程调用了wait被唤醒后尝试加锁后的状态:%d\n",res);
	pthread_mutex_unlock(&s1->mtx);
	printf("主线程解锁了\n");
	
	res = pthread_mutex_trylock(&s1->mtx);
	printf("主线程解锁后尝试加锁后的状态:%d\n",res);
	res = pthread_mutex_unlock(&s1->mtx);
	printf("主线程解锁的状态:%d\n",res);
	
	printf("最终的num:%d\n",s1->num);
	return 0;
}

有兴趣研究的朋友可以尝试单步调试依次查看执行顺序

主函数对结构体成员初始化后,创建一条子线程,子线程的id为1,此时主线程和子线程同时进行工作

由于是主线程被先运行,因此主线程尝试了加锁操作,加锁成功继续向下执行代码,并睡眠5S模拟处理动作,之后对num进行赋值10。

在主线程加锁成功的时候,子线程也是处于工作状态,子线程尝试对同一个mutex加锁,加锁失败则会继续尝试加锁,直到成功加锁为止。

当主线程对num赋值10之后,调用了wait函数,wait函数先对mutex进行解锁,之后阻塞自身线程(在这里自身线程就是主线程),而子线程是处于对mutex的加锁状态,此时加锁成功,子线程执行相关的任务。

主线程处于阻塞的状态,当子线程执行相关任务结束后,解锁,释放了信号,通知主线程的条件变量解除阻塞,此时主线程会收到信号解除阻塞,解除阻塞后wait函数会对mutex进行加锁,由trylock的加锁失败可以验证,该mutex已经被锁过。之后往下执行再解锁,打印通过了子线程操作后的num。通过返回值得知,程序均正常结束,且没有调用join和detach操作

结语

上述是C语言常用的多线程使用方式,需要注意的问题无非是join或者detach,以及mutex的解锁问题,对于其他编程语言的多线程流程使用方式和上述类似,函数名会变动。

  • 24
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值