1. 程式人生 > >用訊號量(互斥鎖)實現兩個執行緒交替列印

用訊號量(互斥鎖)實現兩個執行緒交替列印

本文實現兩個執行緒交替列印,採用的是逐步新增程式碼,分析每一步程式碼的作用,若想要看最終版本可直接翻看後面的最終版本。(本文以訊號量為例,互斥鎖的實現只需將訊號量的函式換成相應的互斥鎖的函式,互斥鎖(訊號量)函式不知道的看https://blog.csdn.net/liqiao_418/article/details/83684347

首先實現兩個執行緒的列印,不加入訊號量的應用,程式碼如下:(注:以下執行結果均是多核環境下的執行結果

程式碼說明:在主執行緒裡建立一個函式執行緒,主執行緒和函式執行緒中各寫一個迴圈分別列印。主執行緒列印完呼叫函式pthread_join()用來等待函式執行緒的完成,等函式執行緒完成後再結束程序(否則可能主執行緒完成任務後,函式執行緒還沒開始執行,主執行緒就結束了程序)。

此時執行結果如下:

接下來加入訊號量的函式,實現兩個執行緒的交替列印,程式碼如下:

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

sem_t sem;//定義全域性變數

void* fun(void* arg)
{
	printf("fun thread start\n");

	int i=0;
	for(;i<5;i++)
	{
		sem_wait(&sem);//相當於P操作
		sleep(1);//只是表示迴圈內部需要執行時間的長短(假設迴圈內有很多內容,需要執行1s)
		printf("fun thread running\n");
		sem_post(&sem);//相當於V操作
	}

	printf("fun thread end\n");

	return NULL;
}
int main()
{
	pthread_t id;
	int res=pthread_create(&id,NULL,fun,NULL);//建立執行緒
	assert(res==0);//斷言執行緒建立成功

	int n=sem_init(&sem,0,1);//對訊號量進行初始化
	assert(n==0);//斷言訊號量初始化成功

	printf("main thread start\n");

	int i=0;
	for(;i<8;i++)
	{
		sem_wait(&sem);//相當於P操作
		sleep(1);
		printf("main thread running\n");
		sem_post(&sem);//相當於V操作
	}

	res=pthread_join(id,NULL);//用此函式的目的是等待函式執行緒結束再結束程序

	sem_destroy(&sem);//訊號量的銷燬

	printf("main thread end\n");
}

程式碼說明:在上面程式碼的基礎上,定義全域性的sem_t型別的sem,在主執行緒對訊號量進行初始化。主執行緒和函式執行緒在迴圈的最開始加“P操作”(-1操作),迴圈的最後加“V操作”(+1操作)。最後在主執行緒即將結束時銷燬訊號量。

此時執行結果如下:

我們可以看到,加入訊號量的使用並沒有達到我們所要的結果,函式執行緒進入後,並沒有執行迴圈裡邊的內容。這是什麼原因呢?

我們在主執行緒和函式執行緒迴圈內的最後一行加入一行程式碼:

sleep(1);

執行結果如下:

這看起來似乎達到了我們想要的結果,分析一下原因,為什麼在迴圈後面加一行sleep就解決問題了呢?原因是在沒有sleep之前主執行緒執行過程中,每一次主執行緒剛進行了-1操作,馬上就進行下一次迴圈,然後就進行了+1操作,這在計算機執行起來是很快的,這麼短的時間內“較遠”的函式執行緒還沒來得及喚醒就又被主執行緒把資源佔用了,所以函式執行緒只能等到主執行緒結束再執行。

雖然看起來我們的問題解決了,但是我們並不能依賴於sleep,因為我們並不知道真正的程式程式碼某一段能執行多長時間。其實,要達到我們的目的,需要用到兩個訊號量。一個訊號量用來控制主執行緒,一個訊號量用來控制函式執行緒。我們把主執行緒的訊號量初始值設為1,函式執行緒的訊號量初始值設為0,所以函式執行緒的列印處於阻塞狀態,主執行緒對自己的訊號量做“-1操作”,列印完後對函式執行緒的訊號量做“+1操作”;等到下個迴圈主執行緒的列印處於阻塞狀態,而函式執行緒此時可以進行列印,函式執行緒對自己的訊號量做“-1操作”,列印完對主執行緒的訊號量做“+1操作”,等到下個迴圈函式執行緒的列印處於阻塞狀態,主執行緒就可以列印……以此類推。這就做到了兩個程序執行時是序列的。程式碼如下:

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

sem_t sem;//控制函式執行緒的訊號量
sem_t sem1;//控制主執行緒的訊號量

void* fun(void* arg)
{
	printf("fun thread start\n");

	int i=0;
	for(;i<5;i++)
	{
		sem_wait(&sem);//對函式執行緒訊號量P操作
		printf("fun thread running\n");
		sem_post(&sem1);//對主執行緒訊號量V操作
	}

	printf("fun thread end\n");

	return NULL;
}
int main()
{
	pthread_t id;
	int res=pthread_create(&id,NULL,fun,NULL);//建立執行緒
	assert(res==0);//斷言執行緒建立成功


	int n=sem_init(&sem,0,0);//初始化函式執行緒的訊號量
	assert(n==0);//斷言函式執行緒訊號量初始化成功

	n=sem_init(&sem1,0,1);//初始化主執行緒的訊號量
	assert(n==0);//斷言主執行緒訊號量初始化成功

	printf("main thread start\n");

	int i=0;
	for(;i<8;i++)
	{
		sem_wait(&sem1);//對主執行緒訊號量P操作
		printf("main thread running\n");
		sem_post(&sem);//對函式執行緒訊號量V操作
	}

	res=pthread_join(id,NULL);//等待函式執行緒執行完畢

    sem_destroy(&sem);//銷燬函式執行緒訊號量
	sem_destroy(&sem1);//銷燬主執行緒訊號量

	printf("main thread end\n");
}

此時執行結果如下: 

可以看到函式執行緒執行完畢後主執行緒就阻塞了,原因是沒有程式為主執行緒進行“+1操作”了,所以對原始碼進行適當修改,使得函式執行緒結束後主執行緒依然能夠往下執行。

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<semaphore.h>
#include<pthread.h>
#include<stdbool.h>

sem_t sem;//控制函式執行緒的訊號量
sem_t sem1;//控制主執行緒的訊號量

bool flag=1;//用來判斷函式執行緒是否退出迴圈體,1表示沒有退出迴圈
bool flag1=1;//用來判斷主執行緒是否退出迴圈體,1表示沒有退出迴圈

void* fun(void* arg)
{
	printf("fun thread start\n");

	int i=0;
	for(;i<5;i++)
	{
		if(flag1)//只有主執行緒沒有退出迴圈體,才執行此操作
        {
		    sem_wait(&sem);//對函式執行緒訊號量P操作
        }
		printf("fun thread running\n");
		sem_post(&sem1);//對主執行緒的訊號量進行“V操作”
	}
	flag=0;
	sem_post(&sem1);//喚醒可能阻塞的主執行緒

	printf("fun thread end\n");

	return NULL;
}
int main()
{
	pthread_t id;
	int res=pthread_create(&id,NULL,fun,NULL);//建立執行緒
	assert(res==0);//斷言建立執行緒成功


	int n=sem_init(&sem,0,0);//初始化函式執行緒的訊號量
	assert(n==0);//斷言初始化函式執行緒訊號量成功

	n=sem_init(&sem1,0,1);//初始化主執行緒的訊號量
	assert(n==0);//斷言初始化主執行緒訊號量成功

	printf("main thread start\n");

	int i=0;
	for(;i<8;i++)
	{
		if(flag)//只有函式執行緒沒有退出迴圈體,才執行此操作
        {
		    sem_wait(&sem1);//對主執行緒訊號量P操作
        }
		printf("main thread running\n");
		sem_post(&sem);//對函式執行緒的訊號量進行“V操作”
	}
	flag1=0;
	sem_post(&sem);//喚醒可能阻塞的fun函式程序

	res=pthread_join(id,NULL);

	sem_destroy(&sem);//銷燬訊號量
	sem_destroy(&sem1);

	printf("main thread end\n");
}

此時執行結果正確:

但是我們發現每一次都是主執行緒在等待函式執行緒執行完畢,然後對訊號量進行銷燬,在現實中,我們並不知道主執行緒先結束還是函式執行緒先結束,如果主執行緒結束好長時間函式執行緒才結束的話,在等待函式執行緒結束的這段時間主執行緒佔用的資源還是沒有釋放這就造成資源的浪費。所以我們希望最後結束的執行緒來銷燬訊號量,這裡,引入註冊函式atexit(),atexit()函式在程序結束時呼叫,所以不管主執行緒先結束還是函式執行緒先結束,atexit()函式是在最後結束的程序中呼叫,對訊號量進行銷燬。只需對上述程式碼稍作修改,就得到:

最終版本

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<semaphore.h>
#include<pthread.h>
#include<stdbool.h>

sem_t sem;//控制函式執行緒的訊號量
sem_t sem1;//控制主執行緒的訊號量

bool flag=1;//用來判斷函式執行緒是否退出迴圈體,1表示沒有退出迴圈
bool flag1=1;//用來判斷主執行緒是否退出迴圈體,1表示沒有退出迴圈

void* fun(void* arg)
{
	printf("fun thread start\n");

	int i=0;
	for(;i<5;i++)
	{
		if(flag1)//只有主執行緒沒有退出迴圈體,才執行此操作
        {
		    sem_wait(&sem);//對函式執行緒訊號量P操作
        }
		printf("fun thread running\n");
		sem_post(&sem1);//對主執行緒的訊號量進行“V操作”
	}
	flag=0;
	sem_post(&sem1);//喚醒可能阻塞的主執行緒

	printf("fun thread end\n");

	return NULL;
}

 void destroy()//新增程式碼
{
	printf("destroy\n");
	sem_destroy(&sem);//銷燬訊號量
	sem_destroy(&sem1);
}

int main()
{
        atexit(destroy);//註冊函式
	pthread_t id;
	int res=pthread_create(&id,NULL,fun,NULL);//建立執行緒
	assert(res==0);//斷言建立執行緒成功


	int n=sem_init(&sem,0,0);//初始化函式執行緒的訊號量
	assert(n==0);//斷言初始化函式執行緒訊號量成功

	n=sem_init(&sem1,0,1);//初始化主執行緒的訊號量
	assert(n==0);//斷言初始化主執行緒訊號量成功

	printf("main thread start\n");

	int i=0;
	for(;i<8;i++)
	{
		if(flag)//只有函式執行緒沒有退出迴圈體,才執行此操作
        {
		    sem_wait(&sem1);//對主執行緒訊號量P操作
        }
		printf("main thread running\n");
		sem_post(&sem);//對函式執行緒的訊號量進行“V操作”
	}
	flag1=0;
	sem_post(&sem);//喚醒可能阻塞的fun函式程序

	/*res=pthread_join(id,NULL);

	sem_destroy(&sem);
	sem_destroy(&sem1);*/

	printf("main thread end\n");
}

執行結果如下:

這就完成了兩個執行緒的交替列印,用互斥鎖實現兩個執行緒的交替列印也是一樣的,只需將訊號量的函式換成相應的互斥鎖的函式就行。