Linux下多线程编程以及互斥锁

版权声明:转载请附带原博主的网址 https://blog.csdn.net/qq_43260665/article/details/88833136

在操作系统原理的术语中,线程是进程的一条执行路径。线程在Unix系统下,通常被称为轻量级的进程,线程虽然不是进 程,但却可以看作是Unix进程的表亲,所有的线程都是在同一进程空间运行,这也意味着多条线程将共享该进程中的全部系统资 源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境 (register context),自己的线程本地存储(thread-local storage)。 一个进程可以有很多线程,每条线程并行执行不同的任 务。

多线程相比多进程的优势:
线程可以提高应用程序在多核环境下处理诸如文件I/O或者socket I/O等会产生堵塞的情况的表现性能。在Unix系统中,一个 进程包含很多东西,包括可执行程序以及一大堆的诸如文件描述符地址空间等资源。在很多情况下,完成相关任务的不同代码间 需要交换数据。如果采用多进程的方式,进程的创建所花的时间片要比线程大些,另外进程间的通信比较麻烦,需要在用户空间 和内核空间进行频繁的切换,开销很大。但是如果使用多线程的方式,因为可以使用共享的全局变量,所以线程间的通信(数据 交换)变得非常高效。

一个进程创建后,会首先生成一个缺省的线程,通常称这个线程为主线程(或称控制线程),C/C++程序中,主线程就是通过 main函数进入的线程,由主线程调用pthread_create()创建的线程称为子线程,子线程也可以有自己的入口函数,该函数由用户 在创建的时候指定。每个线程都有自己的线程ID,可以通过pthread_self()函数获取。最常见的线程模型中,除主线程较为特殊 之外,其他线程一旦被创建,相互之间就是对等关系,不存在隐含的层次关系。每个进程可创建的最大线程数由具体实现决定。

无论在windows中还是Posix中,主线程和子线程的默认关系是:无论子线程执行完毕与否,一旦主线程执行完毕退出,所有 子线程执行都会终止。这时整个进程结束或僵死,部分线程保持一种终止执行但还未销毁的状态,而进程必须在其所有线程销毁 后销毁,这时进程处于僵死状态。线程函数执行完毕退出,或以其他非常方式终止,线程进入终止态,但是为线程分配的系统资 源不一定释放,可能在系统重启之前,一直都不能释放,终止态的线程,仍旧作为一个线程实体存在于操作系统中,什么时候销 毁,取决于线程属性。在这种情况下,主线程和子线程通常定义以下两种关系:
1.可会合(joinable):
这种关系下,主线程需要明确执行等待操作,在子线程结束后,主线程的等待操作执行完毕,子线 程和主线程会合,这时主线程继续执行等待操作之后的下一步操作。主线程必须会合可会合的子线程。在主线程的线程函 数内部调用子线程对象的wait函数实现,即使子线程能够在主线程之前执行完毕,进入终止态,也必须执行会合操作,否 则,系统永远不会主动销毁线程,分配给该线程的系统资源也永远不会释放。
2.相分离(detached):
表示子线程无需和主线程会合,也就是相分离的,这种情况下,子线程一旦进入终止状态,这种 方式常用在线程数较多的情况下,有时让主线程逐个等待子线程结束,或者让主线程安排每个子线程结束的等待顺序,是 很困难或不可能的,所以在并发子线程较多的情况下,这种方式也会经常使用。

线程的分离状态决定一个线程以什么样的方式来终止自己,在默认的情况下,线程是非分离状态的,这种情况下,原有的线程 等待创建的线程结束,只有当pthread_join函数返回时,创建的线程才算终止,释放自己占用的系统资源,而分离线程没有被其 他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。

接下来给出代码:

#include<stdio.h>
#include<pthread.h>
#include<string.h>
#include<errno.h>
#include<unistd.h>
void *worker_s(void *args)
{
    int *val=(int *)args;
    if(!val)
    {
        printf("argument error:%d:%s\n",errno,strerror(errno));
        pthread_exit(NULL);
    }
    printf("Thread %ld is running \n",pthread_self());
    int a=0;
    while(a<=10)
    {
        printf("++++++++++++++before val++ val=%d\n",*val);
        sleep(1);
        *val+=1;

        printf("++++++++++++++after val++ val=%d\n",*val);
        a++;
    }
    return NULL;
}

void *worker_t(void *args)
{
    int *val=(int *)args;
    if(!val)
    {
        printf("argument error:%d:%s\n",errno,strerror(errno));
        pthread_exit(NULL);
    }
    printf("Thread %ld is runnin\n",pthread_self());
    int a=0;
    while(a<=10)
    {
        printf("--------------before val++ val=%d\n",*val);
        sleep(1);
        *val+=1;
        printf("--------------after val++ val=%d\n",*val);
        a++;
    }
    return NULL;
}

int main(void)
{
    int                 val=1000;
    pthread_t           tid;
    pthread_attr_t      attr;

    if(pthread_attr_init(&attr)<0)
    {
        printf("pthread_attr_t error:%d:%s\n",errno,strerror(errno));
        return -1;
    }
    if(pthread_attr_setstacksize(&attr,120*1024)<0)
    {
        printf("pthread_attr_setstacksize error:%d:%s\n",errno,strerror(errno));
        return -1;
    }
    if(pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED)<0)
    {
        printf("pthread_attr_setstackstate error:%d:%s\n",errno,strerror(errno));
        return -1;
    }
    pthread_create(&tid,&attr,worker_s,&val);
    printf("Thread %ld create successfully\n",(long)tid);
    pthread_create(&tid,NULL,worker_t,&val);
    printf("Thread %ld create successfully\n",(long)tid);
    pthread_join(tid,NULL);
    printf("program will return ,now val is %d\n",val);
    pthread_attr_destroy(&attr);

    return 0;

}
                 

函数说明:

#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *),  void *arg);
//Compile and link with -pthread

说明: pthreand_create()用来创建一个线程,并执行第三个参数start_routine所指向的函数。
第三个参数start_routine是一个函数指针,它指向的函数原型是 void *func(void *),这是所创建的子线程要执行的任务 (函数); 第四个参数arg就是传给了所调用的函数的参数,如果有多个参数需要传递给子线程则需要封装到一个结构体里传进去; 第一个参数thread是一个pthread_t类型的指针,他用来返回该线程的线程ID。每个线程都能够通过pthread_self()来获取 自己的线程ID(pthread_t类型)。 第二个参数是线程的属性,其类型是pthread_attr_t类型,其定义如下:

typedef struct {      
   int                               detachstate;   线程的分离状态       
   int                               schedpolicy;  线程调度策略       
   struct sched_param                schedparam;  线程的调度参数       
   int                               inheritsched;  线程的继承性       
   int                               scope;       线程的作用域      
   size_t                            guardsize;   线程栈末尾的警戒缓冲区大小      
   int                               stackaddr_set;       
   void *                            stackaddr;   线程栈的位置       
   size_t                            stacksize;    线程栈的大小 }pthread_attr_t;

对于这些属性,我们需要设定的是线程的分离状态,如果有必要也需要修改每个线程的栈大小。每个线程创建后默认是joinable 状态,该状态需要主线程调用 pthread_join 等待它退出,否则子线程在结束时,内存资源不能得到释放造成内存泄漏。所以我 们创建线程时一般会将线程设置为分离状态,具体有两种方法:

  1. 线程里面调用 pthread_detach(pthread_self()) 这个方法最简单
  2. 在创建线程的属性设置里设置PTHREAD_CREATE_DETACHED属性

代码我们定义了创建线程的属性变量thread_attr ,在对该属性进行设置前,我们需要先调用pthread_attr_init 函数初 始化它,我们设置线程的栈大小为120K,同时设置线程的属性为分离状态。创建线程时使用了该属性创建线程,这时创建的子进程就是分离状态了。线程属性在使用完之后,我们应该调用pthread_attr_destroy把他摧毁释放。

代码创建子线程时并没有使用该线程,同时在thread_worker_t() 里并没有调用pthread_detach()设置线程为分离状态。这时就需要主线程调用pthread_join() 等待第二个子线程退出。当然主线程也就阻塞在这里不会往下继续执行 了。
在创建两个线程时,我们都通过第四个参数将主线程 栈中的val变量地址传给了子线程,因为所有线程都是在同一进 程空间中运行,而只是子线程有自己独立的栈空间,所以这时所有子线程都可以访问主线程空间的val变量。
编译运行:

zhanghang@Ubuntu-14:~/thread$ gcc thread.c -lpthread
zhanghang@Ubuntu-14:~/thread$ ./a.out 
Thread 139908441409280 create successfully
Thread 139908433106688 create successfully
Thread 139908433106688 is runnin
--------------before val++ val=1000
Thread 139908441409280 is running 
++++++++++++++before val++ val=1000
--------------after val++ val=1001
--------------before val++ val=1001
++++++++++++++after val++ val=1002
++++++++++++++before val++ val=1002
--------------after val++ val=1003
--------------before val++ val=1003
++++++++++++++after val++ val=1004
++++++++++++++before val++ val=1004
--------------after val++ val=1005
--------------before val++ val=1005
++++++++++++++after val++ val=1006
++++++++++++++before val++ val=1006
--------------after val++ val=1007
--------------before val++ val=1007
++++++++++++++after val++ val=1008
++++++++++++++before val++ val=1008
--------------after val++ val=1009
--------------before val++ val=1009
++++++++++++++after val++ val=1010
++++++++++++++before val++ val=1010
--------------after val++ val=1011
--------------before val++ val=1011
++++++++++++++after val++ val=1012
++++++++++++++before val++ val=1012
--------------after val++ val=1013
--------------before val++ val=1013
++++++++++++++after val++ val=1014
++++++++++++++before val++ val=1014
--------------after val++ val=1015
--------------before val++ val=1015
++++++++++++++after val++ val=1016
++++++++++++++before val++ val=1016
--------------after val++ val=1017
--------------before val++ val=1017
++++++++++++++after val++ val=1018
++++++++++++++before val++ val=1018
--------------after val++ val=1019
--------------before val++ val=1019
++++++++++++++after val++ val=1020
++++++++++++++before val++ val=1020
--------------after val++ val=1021
program will return ,now val is 1021

主线程创建子线程后究竟是子线程还是主线程先执行,或究竟哪个子线程先运行系统并没有规定,这个依赖操作系统的进程 调度策略。当然因为代码主线程调用了pthread_join会导致主线程阻塞,所以主线程不会往下继续执行while(1)循环。
从代码的云运行结果我们可以看到,主线程最先运行,然后子线程worker_t接着运行,最后运行子线程worker_s,然后就是循环的运行两子线程并打印,pthread_jon会阻塞,直到子进程worker_t返回,但是发现一个问题,对单个线程来说,val的值会在单个循环运行前后加2,而程序的设定只是加1。问题就在于,由于两线程同时对他们的公共资源(临界资源)的操作,使val的值发生阶跃。在多线程中,对凡是涉及到多线程对临界资源的操作,为避免临界资源被多个线程同时操作而产生不可预知的结果,需要引入互斥锁的概念:
代码优化后:


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

typedef struct mutex_s
{
    int val_s;
    pthread_mutex_t lock;
}mutex_t;
void *worker_s(void *args)
{
    mutex_t *mutex=(mutex_t *)args;
    if(!mutex)
    {
        printf("argument error:%d:%s\n",errno,strerror(errno));
        pthread_exit(NULL);
    }
    printf("Thread %ld is running \n",pthread_self());
    int a=0;
    while(a<=10)
    {
        pthread_mutex_lock(&mutex->lock);
        printf("++++++++++++++before val++ val=%d\n",mutex->val_s);
        sleep(1);
        mutex->val_s+=1;
        printf("++++++++++++++after val++ val=%d\n",mutex->val_s);
        pthread_mutex_unlock(&mutex->lock);
        sleep(1);
        a++;
    }
    return NULL;
}

void *worker_t(void *args)
{
    mutex_t *mutex=(mutex_t *)args;
    if(!mutex)
    {
        printf("argument error:%d:%s\n",errno,strerror(errno));
        pthread_exit(NULL);
    }
    printf("Thread %ld is runnin\n",pthread_self());
    int a=0;
    while(a<=10)
    {
        if(pthread_mutex_trylock(&mutex->lock)!=0)
            continue;
        printf("--------------before val++ val=%d\n",mutex->val_s);
        sleep(1);
        mutex->val_s+=1;
        printf("--------------after val++ val=%d\n",mutex->val_s);
        pthread_mutex_unlock(&mutex->lock);
        sleep(1);
        a++;
    }
    return NULL;
}

int main(void)
{
    int                 val=1000;
    pthread_t           tid;
    pthread_attr_t      attr;
    mutex_t  mutex;
    mutex.val_s=val;
    pthread_mutex_init(&mutex.lock,NULL);
    if(pthread_attr_init(&attr)<0)
    {
        printf("pthread_attr_t error:%d:%s\n",errno,strerror(errno));
        return -1;
    }
    if(pthread_attr_setstacksize(&attr,120*1024)<0)
    {
        printf("pthread_attr_setstacksize error:%d:%s\n",errno,strerror(errno));
        return -1;
    }
    if(pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED)<0)
    {
        printf("pthread_attr_setstackstate error:%d:%s\n",errno,strerror(errno));
        return -1;
    }
    pthread_create(&tid,&attr,worker_s,&mutex);
    printf("Thread %ld create successfully\n",(long)tid);
    pthread_create(&tid,NULL,worker_t,&mutex);
    printf("Thread %ld create successfully\n",(long)tid);
    pthread_join(tid,NULL);
    printf("program will return ,now val is %d\n",mutex.val_s);
    pthread_attr_destroy(&attr);
    pthread_mutex_destroy(&mutex.lock);

    return 0;

}

编译运行:

zhanghang@Ubuntu-14:~/thread$ ./a.out      
Thread 140511693022976 create successfully
Thread 140511684720384 create successfully
Thread 140511684720384 is runnin
--------------before val++ val=1000
Thread 140511693022976 is running 
--------------after val++ val=1001
++++++++++++++before val++ val=1001
++++++++++++++after val++ val=1002
--------------before val++ val=1002
--------------after val++ val=1003
++++++++++++++before val++ val=1003
++++++++++++++after val++ val=1004
--------------before val++ val=1004
--------------after val++ val=1005
++++++++++++++before val++ val=1005
++++++++++++++after val++ val=1006
--------------before val++ val=1006
--------------after val++ val=1007
++++++++++++++before val++ val=1007
++++++++++++++after val++ val=1008
--------------before val++ val=1008
--------------after val++ val=1009
++++++++++++++before val++ val=1009
++++++++++++++after val++ val=1010
--------------before val++ val=1010
--------------after val++ val=1011
++++++++++++++before val++ val=1011
++++++++++++++after val++ val=1012
--------------before val++ val=1012
--------------after val++ val=1013
++++++++++++++before val++ val=1013
++++++++++++++after val++ val=1014
--------------before val++ val=1014
--------------after val++ val=1015
++++++++++++++before val++ val=1015
++++++++++++++after val++ val=1016
--------------before val++ val=1016
--------------after val++ val=1017
++++++++++++++before val++ val=1017
++++++++++++++after val++ val=1018
--------------before val++ val=1018
--------------after val++ val=1019
++++++++++++++before val++ val=1019
++++++++++++++after val++ val=1020
--------------before val++ val=1020
--------------after val++ val=1021
++++++++++++++before val++ val=1021
++++++++++++++after val++ val=1022
program will return ,now val is 1022

可以看到,加了互斥锁后,不存在临界资源val值跃变的情况了。

代码分析:
因为在创建线程给线程执行函数传参的时候只能传一个参数,而我们要传递共享的变量shared_var和它相 应的互斥锁lock,所以在这里需要用结构体(worker_ctx_t, ctx: context)将它们封装在一块传进去。
使用work_ctx_t结构体类型定义了传给子线程的变量参数;
互斥锁在使用之前,需要先调用 pthread_mutex_init() 函数来初始化互斥锁;
互斥锁在使用完之后,我们应该调用pthread_mutex_destroy()将他摧毁释放;
这里调用pthread_mutex_lock() 来申请锁,这里是阻塞锁,如果锁被别的线程持有则该函数不会返回;
在访问临界资源(val)完成退出临界区时,我们调用pthread_mutex_unlock来释放锁,这样其他线程才能 再次访问;
第二个线程我们使用pthread_mutex_trylock 来申请锁,这里使用的是非阻塞锁;如果锁现在被别的线程占用 则返回非0值,如果没有被占用则返回0;
这里都要加上延时,否则一个线程拿到锁之后会一直占有该锁;另外一个线程则不能获取到锁;

相关涉及:死锁

使用互斥锁解决文件IO的争夺:

#include<stdio.h>
#include<pthread.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
typedef struct mutex_t
{
	int fds;
	pthread_mutex_t lock;
}mutex_s;
void *worker(void *args)
{
	mutex_s *fd=(mutex_s *)args;
	int s=0;
	while(pthread_mutex_trylock(&fd->lock)!=0)
		continue;
	while(s!=4)//在线程空间中定义的变量为该线程独有,脱离线程体无效
	{
		//if(pthread_mutex_trylock(&fd->lock)!=0)
	//		continue;
		s++;
		printf("child thread is running...\n");
		lseek(fd->fds,0,SEEK_END);
		if(write(fd->fds,"hello",strlen("hello"))<0)
		{
			printf("error\n");
			exit(-1);
		}
	//	sleep(1);
	//	pthread_mutex_unlock(&fd->lock);
		sleep(1);//buffer
	}
	pthread_mutex_unlock(&fd->lock);
	return NULL;
}
int main(int argc,char **argv)
{
	int			fd;
	int 			i=0;

	pthread_t		tid;
	mutex_s				mutex;	
	pthread_mutex_init(&mutex.lock,NULL);
	mutex.fds=open("test.txt",O_CREAT|O_RDWR,0644);
	if(mutex.fds<0)
	{
		printf("open error\n");
		return -1;
	}
//	pthread_mutex_lock(&mutex.lock);
	pthread_create(&tid,NULL,worker,&mutex);
	sleep(2);//let child pthread run first
	pthread_mutex_lock(&mutex.lock);
	while(i!=4)
	{
		sleep(1);
		i++;
		printf("main thread is running...\n");
		lseek(mutex.fds,0,SEEK_END);
		if(write(mutex.fds,"?????",strlen("?????"))<0)
		{
			printf("error\n");
			break;
		}
	}
	pthread_mutex_unlock(&mutex.lock);
	pthread_join(tid,NULL);
	close(mutex.fds);
	pthread_mutex_destroy(&mutex.lock);
}

compile and run:

iot@Public_RPi:~/zhanghang/thread $ gcc thread.c -lpthread
iot@Public_RPi:~/zhanghang/thread $ ./a.out 
child thread is running...
child thread is running...
child thread is running...
child thread is running...
main thread is running...
main thread is running...
main thread is running...
main thread is running...

由于多线程共同使用同一文件描述符,故不存在光标刷新数据。

多线程用于socket服务器,会有比多进程更好的效果:占用更少的资源,不用再消耗时间创建一个进程,提高了时间。
下面给出多线程socket服务器的伪代码

#include<>
int socket_init(port,ip)
{
fd=socket();
bind(fd);
listen(fd);
return fd;
}
void * worker(void *args)
{
int fds=(int )args;
while(1)
{
read(fds),write(fds);
}
return NULL;
}
int thread_worker(tid,(void *)(*worker)(void *),void *args)
{
pthread_attr_t   atrr;
pthread_attr_init(attr);
pthread_attr_setstacksize(attr);
pthread_attr_setdetachstate(attr);
pthread_create(&tid,&attr,worker,args);
pthread_attr_destroy(&attr);
return 1;
}
int main()
{
pthread_t tid;
int conn_fd;
int fd=socket_init();
while(1)
{
conn_fd=accept();//program will block here,when client come,process will create a thread to deal wih the client
thread_worker(tid,worker,(void *)connfd);//pass conn_fd not &conn_fd
//pthread_join(tid,NULL);no need for join because of detach
}
close(fd);
return 0;
}

有关Linux多线程编程知识就这些,后续有新知识再补充。

猜你喜欢

转载自blog.csdn.net/qq_43260665/article/details/88833136