精华内容
下载资源
问答
  • 【Linux】Linux多线程技术

    万次阅读 2018-09-05 15:57:23
    Linux多线程概念 线程的概念 线程是计算机科学中的一个术语,是指运行中的程序的调度单位。一个线程指的是进程中一个单一顺序的控制流,也称为轻量进程。它是系统独立调度和分配的基本单位。同一进程中的多个线程...

    Linux多线程概念

    线程的概念

    线程是计算机科学中的一个术语,是指运行中的程序的调度单位。一个线程指的是进程中一个单一顺序的控制流,也称为轻量进程。它是系统独立调度和分配的基本单位。同一进程中的多个线程将共享该进程中的全部系统资源,例如文件描述符和信号处理等。一个进程可以有很多线程,每个线程并行执行不同的任务。

    线程与进程的区别

    • 根本区别:进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位;
    • 在开销方面:每个进程都必须给它分配独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段。而运行于同一进程的线程使用相同的地址空间,共享大部分数据,启动一个线程的代价也远远小于进程的代价;
    • 在通信机制方面:对于不同的进程之间,它们具有独立的数据空间,数据进行传递只能通过通信的方式进行,这种方式不仅耗时,而且不方便。但同一进程下的线程之间共享数据空间,通信很方便且安全;
    • 所处环境:在操作系统中能同时运行多个进程(程序);而在同一个进程(程序)中有多个线程同时执行(通过CPU调度,在每个时间片中只有一个线程执行);
    • 内存分配方面:系统在运行的时候会为每个进程分配不同的内存空间;而对线程而言,除了CPU外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源;
    • 包含关系:没有线程的进程可以看做是单线程的,如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。

     

    Linux的线程实现

    Linux系统下的多线程遵循POSIX线程接口,称为pthread。编写Linux下的多线程程序,需要使用头文件pthread.h,连接时需要使用库libpthread.a。Linux下pthread是通过系统调用clone()来实现的。clone()是Linux所特有的系统调用,它的使用方式类似于fork()。

    线程创建

    int pthread_create(pthread_t * restrict tidp, 
                       const pthread_attr_t * restrict attr, 
                       void *(* start_rm)(void *), 
                       void *restrict arg );

    函数说明:tidp参数是一个指向线程标识符的指针,当线程创建成功后,用来返回创建的线程ID;attr参数用于指定线程的属性,NULL表示使用默认属性;start_rtn参数为一个函数指针,指向线程创建后要调用的函数,这个函数也称为线程函数;arg参数指向传递给线程函数的参数。

    返回值:线程创建成功则返回0,发生错误时返回错误码。

    因为pthread不是Linux系统的库,所以在进行编译时要加上-lpthread,例如:

    # gcc filename -lpthread

    在代码中获得当前线程标识符的函数为:pthread_self()。

    例子:

    #include <pthread.h>
    
    void * run(void){
            printf("pthread id = %d\n", pthread_self());
            return NULL;
    }
    
    int main()
    {
            int ret;
            pthread_t tid;
            ret = pthread_create(&tid, NULL, run, NULL);
            if(ret){
                    printf("pthread create error");
                    return 1;
            }
    }

    线程退出

    void pthread_exit(void * rval_ptr);

    函数说明:rval_ptr参数是线程结束时的返回值,可由其他函数如pthread_join()来获取。

    如果进程中任何一个线程调用exit()或_exit(),那么整个进程都会终止。线程的正常退出方式有线程从线程函数中返回、线程可以被另一个线程终止以及线程自己调用pthread_exit()函数。

    线程等待

    在调用pthread_create()函数后,就会运行相关的线程函数了。pthread_join()是一个线程阻塞函数,调用后,则一直等待指定的线程结束才返回函数,被等待线程的资源就会被收回。

    int pthread_join(pthread_t tid, void ** rval_ptr);

    函数说明:阻塞调用函数,直到指定的线程终止。tid参数是等待退出的线程id;rval_ptr是用户定义的指针,用来存储被等待线程结束时的返回值(该参数不为NULL时)。

    例子:

    #include <pthread.h>
    
    void * run(void){
            int i=0;
            while(i<10){
                    i++;
                    printf("pthread id = %d\n", pthread_self());
                    if(i==5)
                            pthread_exit(i);        //相当于return i;
            }
            return NULL;
    }
    
    int main()
    {
            int ret;
            pthread_t tid;
            ret = pthread_create(&tid, NULL, run, NULL);
            if(ret){
                    printf("pthread create error");
                    return 1;
            }
            void *val;
            pthread_join(tid, &val);            //此时的val的值就是5
    }

    可以看出,pthread_exit(5)实际上就相当于return 5,也就是说,线程函数为run()函数,线程退出就是run()函数运行完。这时候就能明白pthread_join()的真正意义了。

    线程函数运行结束是可以有返回值的,这个函数的返回值怎么返回呢?可以通过return语句进行返回,也可以通过pthread_exit()函数进行返回。函数的这个返回值怎么来接收呢?就通过pthread_join()函数来接受。

    当然也可以选择不接受该线程的返回值,只阻塞该线程:

    pthread_join(tid, NULL);

    线程清除

    线程终止有两种情况:正常终止和非正常终止。线程主动调用pthread_exit()或者从线程函数中return都将使线程正常退出,这是可预见的退出方式;非正常终止是线程在其他线程的干预下,或者由于自身运行错误(比如访问非法地址)而退出,这种退出方式是不可预见的。

    不论是可预见的线程终止还是异常终止,都会存在资源释放的问题,如何保证线程终止时能顺利地释放自己所占用的资源,是一个必须考虑和解决的问题。

    从pthread_cleanup_push的调用点到pthread_cleanip_pop之间的程序段中的终止动作(包括调用pthread_exit()和异常终止,不包括return)都将执行pthread_cleanup_push()所指定的清理函数。

    void pthread_cleanup_push(void (* rtn)(void *), void * arg);

    函数说明:将清除函数压入清除栈。rtn是清除函数,arg是清除函数的参数。

    void pthread_cleanup_pop(int execute);

    函数说明:将清除函数弹出清除栈。执行到pthread_cleanup_pop()时,参数execute决定是否在弹出清除函数的同时执行该函数,execute非0时,执行;execute为0时,不执行。

    int pthread_cancel(pthread_t thread);

    函数说明:取消线程,该函数在其他线程中调用,用来强行杀死指定的线程。

    例子1:

    #include <stdio.h>
    #include <pthread.h>
    #include <unistd.h>
     
    void *clean(void *arg)
    {
    	printf("clearup: %s\n", (char*)arg);
    	return (void*)0;
    }
     
    void *thr_fn1(void *arg)
    {
    	printf("thread 1 start  \n");
    	pthread_cleanup_push((void*)clean, "thread 1 first handler");
    	pthread_cleanup_push((void*)clean, "thread 1 second handler");
    	printf("thread 1 push complete \n");
    	
    	if(arg)
    	{
    		return ((void*)1);
    	}
    	
    	pthread_cleanup_pop(0);
    	pthread_cleanup_pop(0);
    	return (void *)1;
    }
     
    void *thr_fn2(void *arg)
    {
    	printf("thread 2 start \n");
    	
    	pthread_cleanup_push((void*)clean, "thread 2 first handler");
    	pthread_cleanup_push((void*)clean, "thread 2 second handler");
    	printf("thread 2 push complete \n");
    	
    	if(arg)
    	{
    		pthread_exit((void *)2);
    	}
    	pthread_cleanup_pop(0);
    	pthread_cleanup_pop(0);
    	pthread_exit((void*)2);
    }
     
    int main(void)
    {
    	int err;
    	pthread_t tid1, tid2;
    	void *tret;
    	
    	err = pthread_create(&tid1, NULL, thr_fn1, (void *)1);
    	
    	if(err != 0)
    	{
    		printf("main1:error...\n");
    		return -1;
    	}
     
    	err = pthread_create(&tid2, NULL, thr_fn2, (void *)1);
    	
    	if(err != 0)
    	{
    		printf("main2:error...\n");
    		return -1;
    	}
    	
    	err = pthread_join(tid1, &tret);
    	if(err != 0)
    	{
    		printf("main3:error ... \n");
    		return -1;
    	}
    	printf("thread 1 exit code %d \n", (int)tret);
    	
    	err = pthread_join(tid2, &tret);
    	
    	if(err != 0)
    	{
    		printf("main4:error ...\n");
    		return -1;
    	}
    	printf("thread 2 exit code %d \n", (int)tret);
    	return 1;
    }
    

    程序运行结果为:

    [root@localhost gcc]# ./a.out 
    thread 2 start 
    thread 2 push complete 
    clearup: thread 2 second handler
    clearup: thread 2 first handler
    thread 1 start  
    thread 1 push complete 
    thread 1 exit code 1 
    thread 2 exit code 2 
    [root@localhost gcc]# 

    例子2:

    #include <pthread.h>
    #include <stdio.h>
    #include <unistd.h>
    
    void * run(void){
            while(1){
                    printf("pthread id = %d\n", pthread_self());
            }
            return NULL;
    }
    
    int main()
    {
            int ret;
            pthread_t tid;
            ret = pthread_create(&tid, NULL, run, NULL);
            if(ret){
                    printf("pthread create error");
                    return 1;
            }
            pthread_cancel(tid);
            pthread_join(tid, NULL);
    }

    也就是说,pthread_exit()用于本线程自己调用,pthread_cancel()用于本线程来终结其他线程。

    同时这里也区分一下线程返回的return和pthread_exit:

    • pthread_exit()用于线程退出,可以指定返回值,以便其他线程通过pthread_join()函数获取该线程的返回值。return,是函数返回,不一定是线程函数哦! 只有线程函数中return,线程才会退出;
    • pthread_exit()、return都可以用pthread_join()来接收返回值的,也就是说,对于pthread_join()函数来说是没有区别的;
    • pthread_cleanup_push()所指定的清理函数支持调用pthread_exit()退出线程和异常终止,不支持return;
    • pthread_exit()为直接杀死/退出当前进程,return则为退出当前函数,但是在g++编译器中,main中的return会被自动优化成exit(),所以在主函数中使用return会退出该进程所有线程的运行;
    • return会调用局部对象的析构函数,而pthread_exit()不会(线程本来就不建议用pthread_exit()这类方法自杀的,正确的方法是释放所申请的内存后return)。

     

    线程函数传递及修改线程的属性

    线程函数参数传递

    在函数pthread_create()中,arg参数会被传递到start_rnt线程函数中。其中,线程函数的形参为void *类型,该类型为任意类型的指针。所以任意一种类型都可以通过地址将数据传送给线程函数中。

    例子:

    #include <pthread.h>
    #include <stdio.h>
    #include <unist.d>
    
    void * run(void * buf){
            char *str=(char *)buf;
            printf("pthread id = %d,%s\n", pthread_self(), str);
            return NULL;
    }
    
    int main()
    {
            int ret;
            pthread_t tid;
            char buf[256]="abcdef";
            ret = pthread_create(&tid, NULL, run, buf);
            if(ret){
                    printf("pthread create error");
                    return 1;
            }
    }

    数组作实参时,传入的是数组的首地址,即传入多个相同类型数据的首地址;结构体作实参时,传入的是结构体的地址,即传入多个不同数据类型的结构地址。

    也就是说,如果线程函数中需要传入多个不同数据类型的参数,但是依照pthread_create()的定义,仅可以传入void *的类型的数据,参数数量为一个。这个时候就需要将不同数据类型的参数封装成一个结构体,将这个结构体的地址传入。

    例子:

    #include <pthread.h>
    #include <stdio.h>
    #include <unist.d>
    
    struct STU{
            int runn;
            int num;
            char name[32];
    };
    
    void * run(void * buf){
            struct STU *p=buf;
            printf("pthread id = %d,%d,%d,%s\n", pthread_self(), p->runn, p->num, p->name);
            return NULL;
    }
    
    int main()
    {
            int ret;
            pthread_t tid;
            struct STU st={10,12,"aa"};
            ret = pthread_create(&tid, NULL, run, &st);
            if(ret){
                    printf("pthread create error");
                    return 1;
            }
    }

    需要注意一下,线程函数和普通的函数一样,每调用一次,局部变量都会配分一次内存,并且各自之间互不干扰。

    线程属性

    之前线程创建函数pthread_create()函数的第二个参数都设置为了NULL,也就是说,都是采用的默认的线程属性。对于大多数的程序来说,使用默认属性就够了,但还是有必要来了解一下相关的属性。

    属性结构为pthread_attr_t,属性值不能直接设置,必须使用相关的函数进行操作,初始化函数为pthread_attr_init(),这个函数必须在pthread_create()函数调用之前调用。

    属性对象主要包括是否绑定、是否分离、堆栈地址、堆栈大小、优先级。默认的属性为非绑定、非分离、默认1M的堆栈、与父进程同样级别的优先级。

    线程绑定属性

    关于绑定属性,涉及到另外一个概念:轻进程(Light Weight Process,LWP)。轻进程可以理解为内核进程,它位于用户层和内核层之间。系统对线程资源的分配和对线程的控制时通过轻进程来实现的,一个轻进程可以控制一个或多个线程。默认情况下,启动多少轻进程、哪些轻进程来控制哪些线程是由系统来控制的,这种状况即称为非绑定。绑定状况下,则顾名思义,即某个线程固定地绑在一个轻进程之上。被绑定的线程具有较高的响应速度,这是因为CPU时间片的调度是面向轻进程的,绑定的线程可以保证在需要的时候它总有一个轻进程可用。通过设置被绑定的轻进程的优先级和调度级可以使得绑定的线程满足诸如实时反应之类的要求。

    设置线程绑定状态的函数为pthread_attr_setscope,函数原型为:

    int pthread_attr_setscope(pthread_attr_t * tattr, int scope);

    函数说明:tattr参数为指向属性结构的指针,scope参数为绑定类型,通常有两个取值PTHREAD_SCOPE_SYSTEM(绑定)、PTHREAD_SCOPE_PROCESS(非绑定)。

    返回值,pthread_sttr_setscope()成功完成后会返回0,其他任何返回值都表示出现了错误。

    例子:

    #include <pthread.h>
    #include <stdio.h>
    #include <unist.d>
    
    void * run(void){
            printf("pthread id = %d\n", pthread_self());
            return NULL;
    }
    
    int main()
    {
            int ret;
            pthread_t tid;
    
            pthread_attr_t attr;
            pthread_attr_init( &attr );
            pthread_attr_setscope( &attr, PTHREAD_SCOPE_SYSTEM );
    
            ret = pthread_create(&tid, NULL, run, NULL);
            if(ret){
                    printf("pthread create error");
                    return 1;
            }
    }
    

    线程分离属性

    线程的是否可结合状态决定线程以什么样的方式来终止自己。在任何一个时间点上,线程是可结合的(或非分离的,joinable)或者是分离的(detached)。 

    • 可结合属性:创建线程时,线程的默认属性是可结合的, 如果一个可结合线程结束运行但没有被pthread_join(),则它的状态类似于进程中的Zombie(僵死),即它的存储器资源(例如栈)是不释放的,所以创建线程者应该调用pthread_join()来等待线程运结束,并得到线程的退出码,回收其资源;
    • 可分离属性:通过调用pthread_detach()函数该线程的可结合属性将被修改为可分离属性。一个分离的线程是不能被其他线程回收或杀死的,它的存储器资源在它终止时由系统自动释放。 

    设置线程是否分离的函数为pthread_attr_setdatachstate(),其原型为:

    int pthread_sttr_setdetachstate(pthread_sttr_t * tattr, int detachstate);

    函数说明:tattr参数为指向属性结构的指针,detachstate参数为分离类型,通常有两个取值PTHREAD_CREATE_DETACHED(分离)、PTHREAD_CREATE_JOINABLE(非分离、结合)。

    返回值,pthread_attr_setdatachstate()成功完成后会返回0,其他任何返回值都表示出现了错误。

    例子:

    #include <pthread.h>
    #include <stdio.h>
    #include <unist.d>
    
    void * run(void){
            printf("pthread id = %d\n", pthread_self());
            return NULL;
    }
    
    int main()
    {
            int ret;
            pthread_t tid;
    
            pthread_attr_t attr;
            pthread_attr_init( &attr );
            pthread_attr_setdetachstate( &attr, PTHREAD_CREATE_JOINABLE );
    
            ret = pthread_create(&tid, NULL, run, NULL);
            if(ret){
                    printf("pthread create error");
                    return 1;
            }
    
            pthread_join(tid, NULL);
    }
    

    注意,如果使用PTHREAD_CREATE_JOINABLE创建非分离线程(默认),则假设应用程序将等待线程完成。也就是说,在费线程终止后,必须要有一个线程用pthread_join()来等待它,否则就不会释放线程的资源,这将会导致内存泄漏。无论是创建的分离线程还是非分离线程,在所有线程都退出之前,进程都不会退出。

    这与进程的wait()函数类似。

    线程优先级属性

    线程优先级存放在结构sched_param中,设置线程优先级的接口是pthread_attr_setschedparam(),它的完整定义是:

    struct sched_param {
        int sched_priority;
    }
    
    int pthread_attr_setschedparam(pthread_attr_t *attr, struct sched_param *param);

    例子:

    #include <pthread.h>
    #include <stdio.h>
    #include <unist.d>
    
    void * run(void){
            printf("pthread id = %d\n", pthread_self());
            return NULL;
    }
    
    int main()
    {
            int ret;
            pthread_t tid;
    
            pthread_attr_t attr;
            pthread_attr_init( &attr );
            struct sched_param param;
            param.sched_priority=50;
            pthread_attr_setschedparam(&attr, &param);
    
            ret = pthread_create(&tid, NULL, run, NULL);
            if(ret){
                    printf("pthread create error");
                    return 1;
            }
            pthread_join(tid, NULL);
    }
    

     

    线程的互斥

    线程间的互斥是为了避免对共享资源或临界资源的同时使用,从而避免因此而产生的不可预料的后果。临界资源一次只能被一个线程使用。线程互斥关系是由于对共享资源的竞争而产生的间接制约。

    互斥锁

    假设各个线程向同一个文件顺序写入数据,最后得到的结果一定是灾难性的。互斥锁用来保证一段时间内只有一个线程在执行一段代码,实现了对一个共享资源的访问进行排队等候。互斥锁是通过互斥锁变量来对访问共享资源排队访问。

    互斥量

    互斥量是pthread_mutex_t类型的变量。互斥量有两种状态:lock(上锁)、unlock(解锁)。

    当对一个互斥量加锁后,其他任何试图访问互斥量的线程都会被堵塞,直到当前线程释放互斥锁上的锁。如果释放互斥量上的锁后,有多个堵塞线程,这些线程只能按一定的顺序得到互斥量的访问权限,完成对共享资源的访问后,要对互斥量进行解锁,否则其他线程将一直处于阻塞状态。

    操作函数

    pthread_mutex_t是锁类型,用来定义互斥锁。

    互斥锁的初始化

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

    restrict,C语言中的一种类型限定符(Type Qualifiers),用于告诉编译器,对象已经被指针所引用,不能通过除该指针外所有其他直接或间接的方式修改该对象的内容。

    函数说明:mutex为互斥量,由pthread_mutex_init调用后填写默认值;attr属性通常默认为NULL。

    上锁

    int pthread_mutex_lock(pthread_mutex_t * mutex);

    函数说明:mutex为互斥量。

    解锁

    int pthread_mutex_unlock(pthread_mutex_t * mutex);

    函数说明:mutex为互斥量。

    判断是否上锁

    int pthread_mutex_trylock(pthread_mutex_t * mutex);

    返回值:0表示已上锁,非0表示未上锁。

    销毁互斥锁

    int pthread_mutex_destory(pthread_mutex_t * mutex);

    例子:

    #include <stdio.h>
    #include <pthread.h>
    
    char str[1024];
    pthread_mutex_t mutex;
    
    void *run1(void){
            while(1){
                    pthread_mutex_lock(&mutex);
                    sprintf(str,"run1");
                    sleep(5);
                    pthread_mutex_unlock(&mutex);
                    usleep(1);
            }
    }
    
    void *run2(void){
            while(1){
                    pthread_mutex_lock(&mutex);
                    sprintf(str,"run2");
                    sleep(5);
                    pthread_mutex_unlock(&mutex);
                    usleep(1);
            }
    }
    
    int main()
    {
            pthread_mutex_init(&mutex, NULL);
            pthread_t tid1,tid2;
    
            pthread_create(&tid1,NULL,run1,NULL);
            pthread_create(&tid2,NULL,run2,NULL);
    
            pthread_join(tid1,NULL);            //默认属性joinable下,必须join()函数释放资源
            pthread_join(tid2,NULL);
    
            pthread_mutex_destory(&mutex);
    }

    这里的互斥量的用处就是在sleep(5)之间的时间内,不会切换到另一个线程的线程函数中,因为已经用互斥量锁定了。

    自旋锁

    自旋锁是一种用于保护多线程共享资源的锁,与一般互斥锁不同之处在于:当自旋锁尝试获取锁时以忙等待的形式不断地循环检查锁是否可用。在多CPU的环境中,对持有锁较短的程序来说,使用自旋锁代替一般的互斥锁往往能够提高程序的性能。

    自旋锁和互斥锁的区别

    • 从实现原理上来讲,互斥锁属于sleep-waiting类型的锁,而自旋锁属于busy-waiting类型的锁。也就是说:pthread_mutex_lock()操作,如果没有锁成功的话就会调用system_wait()的系统调用并将当前线程加入该互斥锁的等待队列里;而pthread_spin_lock()则可以理解为,在一个while(1)循环中用内嵌的汇编代码实现的锁操作(在linux内核中pthread_spin_lock()操作只需要两条CPU指令,unlock()解锁操作只用一条指令就可以完成)。
    • 对于自旋锁来说,它只需要消耗很少的资源来建立锁;随后当线程被阻塞时,它就会一直重复检查看锁是否可用了,也就是说当自旋锁处于等待状态时它会一直消耗CPU时间;
    • 对于互斥锁来说,与自旋锁相比它需要消耗大量的系统资源来建立锁;随后当线程被阻塞时,线程的调度状态被修改,并且线程被加入等待线程队列;最后当锁可用时,在获取锁之前,线程会被从等待队列取出并更改其调度状态;但是在线程被阻塞期间,它不消耗CPU资源。

    因此自旋锁和互斥锁适用于不同的场景。自旋锁适用于那些仅需要阻塞很短时间的场景,而互斥锁适用于那些可能会阻塞很长时间的场景。

    操作函数

    pthread_spinlock_t是锁类型,用来定义自旋锁。

    自旋锁的初始化

    int pthread_spin_init(pthread_spinlock_t *lock, int pshared);

    自旋锁的销毁

    int pthread_spin_destroy(pthread_spinlock_t *lock);

    上锁

    int pthread_spin_lock(pthread_spinlock_t *lock);

    判断是否上锁

    int pthread_spin_trylock(pthread_spinlock_t *lock);

    解锁

    int pthread_spin_unlock(pthread_spinlock_t *lock);

    C++实现自旋锁

    C++11提供了对原子操作的支持,其中std::atomic是标准库提供的一个原子类模板。

    对于lock函数,需要CAS的原子操作,可以使用std::atomic类模板的成员函数compare_exchange_strong();

    对于unlock函数,可以使用std::atomic类模板的成员函数store来以原子操作的方式将flag置false。

    #include <atomic>
     
    class spin_lock {
    private:
            std::atomic<bool> flag = ATOMIC_VAR_INIT(false);
    public:
            spin_lock() = default;
            spin_lock(const spin_lock&) = delete;
            spin_lock& operator=(const spin_lock) = delete;
            void lock(){
                    bool expected = false;
                    while(!flag.compare_exchange_strong(expected, true));        
                    //比较flag是否等于expected,相等的话,将flag设置为true(desired)
                    //flag被改变了返回true,否则返回false
                            expected = false;    
            }   
            void unlock(){   //release spin lock
                    flag.store(false);
            }   
    };
    

    参考文章:自旋锁与互斥锁的对比、手工实现自旋锁C++11实现自旋锁

     

    线程同步

    条件变量

    条件变量就是一个变量,用于线程等待某件事情的发生,当等待事件发生时,被等待的线程和事件一起继续执行。等待的线程处于睡眠状态,直到另一个线程将它唤醒,才开始活动,条件变量用于唤醒线程。

    互斥锁一个明显的缺点就是它只有两种状态:锁定和非锁定。而条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,它常和互斥锁一起使用。

    操作函数

    pthread_cond_t是条件变量类型,用于定义条件变量。

    条件变量初始化函数

    int pthread_cond_init(pthread_cond_t * restrict cond, const pthread_condattr_t * restrict attr);

    函数说明:cond参数为条件变量指针,通过该函数实现条件变量赋初值;attr参数通常为NULL。

    线程同步等待函数(睡眠函数)

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

    函数说明:cond参数为条件变量,mutex参数为互斥量。

    说明:哪一个线程执行pthread_cond_wait,哪一个线程就开始睡眠,在睡眠时同时先解开互斥锁,以便让其他线程可以继续执行。

    发送条件信号(唤醒函数)

    int pthread_cond_signal(pthread_cond_t * cond);

    函数说明:cond参数为条件变量。

    说明:在另一个线程中使用,当某线程符合某种条件时,用于唤醒其他线程,让其他线程同步运行。其他线程被唤醒后,马上开始加锁,如果此时锁处于锁定状态,则等待被解锁后向下执行销毁。

    条件变量销毁

    int pthread_cond_destory(pthread_cond_t * cond);

    函数说明:cond参数为条件变量。

    例子:

    #include <stdio.h>
    #include <pthread.h>
    
    pthread_mutex_t mutex;
    pthread_cond_t cond;
    bool test_cond = false;
    
    void *run1(void){
            pthread_mutex_lock(&mutex);
            while(!test_cond){
                    pthread_cond_wait(&cond, &mutex);
            }
            pthread_mutex_unlock(&mutex);
    }
    
    void *run2(void){
            pthread_mutex_lock(&mutex);
            test_cond = false;
            pthread_cond_signal(&cond);
            pthread_mutex_unlock(&mutex);
    }
    
    int main()
    {
            pthread_mutex_init(&mutex, NULL);
            pthread_cond_init(&cond,NULL);
            pthread_t tid1,tid2;
    
            pthread_create(&tid1,NULL,run1,NULL);
            pthread_create(&tid2,NULL,run2,NULL);
    
            pthread_join(tid1,NULL);            //默认属性joinable下,必须join()函数释放资源
            pthread_join(tid2,NULL);
    
            pthread_mutex_destory(&cond);
            pthread_mutex_destory(&mutex);
    }

    这里讲一下:互斥量与条件变量为什么要配合使用?

    条件变量用在某个线程需要在某种条件才会进行某些操作的情况下,从而避免了线程不断轮询检查该条件是否成立而降低效率的情况,这是实现了效率提高。

    条件变量这个变量其实本身不包含条件信息,条件的判断不在pthread_cond_wait函数功能中,而需要外面进行条件判断。这个条件通常是多个线程或进程的共享变量,这样就很清楚了,对于共享变量很可能产生竞争条件尤其还对共享变量加了条件限制,所以从这个角度看,必须对共享变量加上互斥锁。

    加上互斥锁,就会出现因为要不断地判断条件是否成立而不断地开锁、解锁的过程。比如,线程2的任务是输出“Hello World”,但是必须满足条件iCount等于100。由于iCount是线程共享变量,所以必须使用互斥锁:

    //线程1
    while(1){
            pthead_mutex_lock(&mutex);
            if (100 < iCount)
                    iCount++;
            pthread_mutex_unlock(&mutex);
    }
    
    //线程2
    while(1){
            pthead_mutex_lock(&mutex);
            if (100 == iCount){
                    printf("Hello World\n");
                    iCount = 0;
                    pthread_mutex_unlock(&mutex);
            }else{
                    pthread_mutex_unlock(&mutex);
            }
    }

    这是很复杂的,线程2需要不断地开锁、检查、解锁,浪费了很多的系统资源。这个时候就有一个办法了,让线程2堵塞休眠,当线程1达到条件的时候直接通知线程2,把它唤醒,这样就避免浪费了。

    同时还要注意一个过程:

    pthread_cond_wait():这个函数的过程我们必须了解,首先对互斥锁进行解锁;然后自身堵塞等待;当等待条件达成,注意这时候函数并未返回,而是重新获得锁并返回。

    所以:pthread_cond_wait()本身提供的重要功能是,保证这些操作一定是原子操作不可分割。

    试想一下,如果进程A调用了pthread_cond_wait(),先进行了解锁,这时候由于进程A时间片到期,轮换到进程B,进程B一直想要这把锁,现在终于拿到了,它干完了事情,调用pthread_cond_signal()想唤醒A但是A并未完成睡眠等待条件达成,所以这个唤醒信号就丢失了。

    参考文章:关于条件变量和互斥锁为何配合使用的思考

     

    信号量

    信号量本质上是一个非负的整数计数器,它被用来控制对公共资源的访问,也被称为PV原子操作。

    PV原子操作,广泛用于进程或线程之间的通信的同步和互斥。其中,P是通过的意思,V是释放的意思,不可中断的过程,则由操作系统来保证P操作和V操作。PV操作时针对信号量的操作,就是对信号量进行加减的过程。

    • P操作,即信号量sem减一的过程,如果sem小于等于0,P操作被堵塞,直到sem变量大于0为止。P操作即加锁过程。
    • V操作,即信号量sem加一的过程。V操作即解锁过程。

    操作函数

    信号量初始化

    int sem_init(sem_t *sem, int pshared, unsigned int value);

    函数说明:sem参数是信号量指针;pshared参数为共享方式,0表示信号量只是在当前进程中使用(线程),1表示信号量在多进程中使用;value参数表示信号量的初始值,一般为1。

    P操作,减少信号量

    int sem_wait(sem_t *sem);

    V操作,增加信号量

    int sem_post(sem_t *sem);

    销毁信号量

    int sem_destory(sem_t *sem);

    获取信号量的值

    int sem_getvalue(sem_t *sem, int *sval);

    例子:

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <pthread.h>
    #include <semaphore.h>
    
    sem_t sem;
    
    void *run1(void){
            while(1){
                    sem_wait(&sem);
                    printf("run1 id=%d",pthread_self());
                    sem_post(&sem);
                    usleep(1);
            }
    }
    
    void *run2(void){
            while(1){
                    sem_wait(&sem);
                    printf("run2 id=%d",pthread_self());
                    sem_post(&sem);
                    usleep(1);
            }
    }
    
    int main()
    {
            pthread_t tid1,tid2;
            sem_init(&sem,0,1);
    
            pthread_create(&tid1,NULL,run1,NULL);
            pthread_create(&tid2,NULL,run2,NULL);
    
            pthread_join(tid1,NULL);            //默认属性joinable下,必须join()函数释放资源
            pthread_join(tid2,NULL);
    
            sem_destory(&sem);
    }

     

    展开全文
  • Linux多线程

    万次阅读 2018-06-22 11:11:04
    Linux多线程 1. 线程? 线程是一个进程内部的控制序列。线程共享进程数据,但线程也拥有自己私有的上下文数据、私有栈结构。是程序执行的最小单位。 2. Linux中的线程 Linux中的线程是用进程模拟的。在Linux...

    Linux多线程

    1. 线程?

    线程是一个进程内部的控制序列。线程共享进程数据,但线程也拥有自己私有的上下文数据、私有栈结构。是程序执行的最小单位。

    2. Linux中的线程

    Linux中的线程是用进程模拟的。在Linux中,操作系统没有专门的结构来管理线程,而是用给线程分配一个PCB(更准确地说应该叫TCB)。我们知道,Linux在创建进程时做了有如下工作:
    1. 分配一个进程描述符,新建PCB
    2. 复制父进程的环境
    3. 分配系统资源
    4. 复制父进程的代码
    5. 置成就绪状态,放入就绪队列

    而在创建线程时,首先新建一个“PCB”,然后从进程所拥有的资源中为线程分配它运行时必要的一些资源。下面以图示来进行说明:
    线程
    一个进程拥有自己的PCB,PCB中包含了虚拟地址空间的信息,而虚拟地址空间通过页表完成对物理内存的映射。当该进程创建出线程后,便从进程的PCB中拷贝一份形成TCB,因此TCB中的虚拟地址空间的信息是不变的。所以进程和线程共享一块虚拟地址空间。正是因为如此,如果定义一个函数,那么该函数保存在虚拟地址空间的代码段,在各线程中都可以调用;如果定义一个全局变量,那么在各线程中都可以访问到。由于线程都是从进程得来的,因此没有“父线程子线程”一说,只有“新线程”与“旧线程”,即:一个进程下的所有线程都是平等地位的。

    总的而言,线程共享的资源有:
    1. 定义的函数
    2. 全局变量
    3. 文件描述符
    4. 函数指针数组handler
    5. 当前工作目录
    6. 用户id和组id

    线程私有的资源有:
    1. 线程id
    2. 调度优先级
    3. 位图block
    4. 上下文数据
    5. 私有栈结构

    3. 线程的注意事项

    第一点,线程之所以被引入,是为了弥补计算机I\O时CPU处于等待状态而无法充分利用CPU的缺点。
    事实上,不管是线程还是进程,任意时刻绝大部分都是睡眠的,即现代计算机大多数是IO密集型,涉及IO必然要等待,进程调度下,进程一旦等待,必然要切换另一个进程执行,而在线程调度下,一个线程等待,可以调度另一个线程运行。如果一个线程阻塞,在没有同步关系的情况下完全可以调度其他的线程运行,粒度更小,相当于间隙更小,这样程序运行势必更快。但是这只适用于I\O密集型应用。如果是计算密集型应用,若不是多处理机,则多线程就没有意义。

    第二点,对于一个进程而言,由它产生的线程是共享相当一部分资源的。若是在运行过程中某个线程发生崩溃,则会导致共享的资源发生不可预知的问题,发生连锁反应,进而导致整个进程发生崩溃。而被引发崩溃的进程,则由它的父进程进行回收。

    4. 线程ID

    在Linux中,目前线程实现是Native POSIX Thread Libaray,简称NPTL。在这种实现下,线程又被成为轻量级线程,每一个用户态的线程,在内核中对对应一个调度实体,也拥有自己的进程描述符(task_struct结构体)。

    没有线程之前,一个进程对应内核中的一个进程ID。但是引入线程概念之后,情况发生了变化,一个用户进程下管辖N个用户态线程,每个线程作为一个独立的调度实体在内核中有自己的进程描述符,进程和内核的关系变成了1:N的关系。

    为了更好地将线程组织起来,Linux内核引入了线程组的概念。

    多线程的进程,被称为线程组,线程组内的每一个线程在内核中都存在一个进程描述符与之对应。进程描述符结构体中的pid,表面上看是对应着进程ID,实际上它对应的是线程ID。进程描述符中的tgid,含义是Thread Group ID, 该值对应的是用户层面的进程ID。

    线程组内的第一个线程,在用户态被称为主线程,在内核中被称为group leader,内核在创建第一个线程时,会将线程组ID的值设置成第一个线程的线程ID,group_leader指针则指向自身,即指向主线程的进程描述符。所以线程组内存在一个线程,它的ID等于进程ID,而该线程即为线程组的主线程。

    展开全文
  • Linux多线程编程

    千次阅读 2019-06-21 17:40:29
    作为多任务实现的一种机制,多线程应用得非常广泛,相对于多进程,多线程不仅运行效率高,而且还可以提高系统资源的使用效率。虽然网上关于多线程的讲解已经有一大堆,但出于学习的心态,有必要在这里做一下笔记。 ...

    作为多任务实现的一种机制,多线程应用得非常广泛,相对于多进程,多线程不仅运行效率高,而且还可以提高系统资源的使用效率。虽然网上关于多线程的讲解已经有一大堆,但出于学习的心态,有必要在这里做一下笔记。

    一、多线程编程常用函数

    1. int pthread_create(pthread_t * thread,const pthread_attr_t * attr,void * (*start_routine)(void *), void *arg);

    作用:创建一个新线程

    参数:

     thread:线程ID

     attr:线程属性(包括调度策略,调度参数,堆栈地址,堆栈大小等)。一般设置为NULL,即采用系统默认属性

     void * (*start_routine):线程函数指针

     *arg:线程函数参数的指针

     返回值:

    0:表示创建成功

    其他:表示创建失败失败

     2. int pthread_join(pthread_t th, void **thread_return);

    作用:等待其他线程终止

    参数:

    th:需要等待的线程的ID

    thread_return:所等待的线程的返回值

    返回值:

    0:表示成功

    其他:表示失败

    3. void pthread_exit(void *retval);

    作用:终止当前线程

    参数:

    retval:线程的返回值

    4. int pthread_mutex_lock(pthread_mutex_t *mutex);

    作用:上锁,如果锁不可用则会阻塞当前线程直到锁可用

    参数:

    mutex:互斥变量

    5. int pthread_mutex_unlock(pthread_mutex_t *mutex);
    作用:与pthread_mutex_lock()相反

    6. pthread_t pthread_self(void);

    作用:返回当前线程ID

    返回值:

    当前线程ID

    7. int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

    作用:解锁mutex,等待条件变量cond被发出信号

    参数:

    cond:条件变量

    mutex:互斥锁对象

          在调用此函数之前要先获得互斥锁,调用此函数时会自动释放互斥锁以防止死锁,这就是为什么此函数与互斥锁联系在一起的原因。另外此函数调用成功后当前线程会被挂起并放弃CPU,因此在等待的过程中是不会占用CPU资源的,当条件满足(被唤醒)时会重新上锁。

    8. int pthread_cond_signal(pthread_cond_t *cond);

    作用:重新开始(唤醒)正在等待条件变量cond的线程

    参数:

    cond:条件变量

    二、实例

           要求:在主线程里创建两个子线程,其中一个子线程输出1,3,5,7,9,另一个子线程输出2,4,6,8,10,输出的顺序是1,2,3,4,5,6,7,8,9,10。两个子线程都退出后主线程才退出。

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


    //初始化互斥锁
    pthread_mutex_t count_mutex     = PTHREAD_MUTEX_INITIALIZER;
    //初始化条件变量
    pthread_cond_t  condition_var   = PTHREAD_COND_INITIALIZER;
    //初始化计数变量
    int num = 1;

    //线程1的线程函数
    void *thread1(void *arg)
    {
        while(1)
        {
            //上锁
            pthread_mutex_lock( &count_mutex );    
            //如果num是奇数
            if(num%2)
            {    
                //输出当前num的值
                printf("thread1 create: %d\n",num);
                num++;    
            }
            else
                //解锁,等待
                pthread_cond_wait( &condition_var, &count_mutex );
            //解锁
            pthread_mutex_unlock( &count_mutex );
            if(num > 10)
                return NULL;
        }
        
    }

    //线程2的线程函数
    void *thread2(void *arg)
    {
        while(1)
        {
            //上锁
            pthread_mutex_lock( &count_mutex );    
            //如果num是偶数
            if(!(num%2))
            {    
                //输出当前num的值
                printf("thread2 create: %d\n",num);
                num++;    
            }
            else
                //唤醒正在等待的线程
                pthread_cond_signal( &condition_var );
            
            //解锁
            pthread_mutex_unlock( &count_mutex );
            if(num > 10)
            {
                //最后再唤醒一次以防止死锁
                pthread_cond_signal( &condition_var );
                return NULL;
            }
        }

    }


    int main()
    {
        pthread_t t1,t2;
        int ret;
     
        //创建线程1
        ret = pthread_create(&t1,NULL,thread1,NULL);
        if(ret != 0)    
        {
            printf("pthread_create 1 failed!\n");
        }
        
        //创建线程2
        ret = pthread_create(&t2,NULL,thread2,NULL);
        if(ret != 0)    
        {
            printf("pthread_create 2 failed!\n");
        }

        //等待线程1结束
        ret = pthread_join(t1,NULL);
        if(ret != 0)
        {
            printf("pthread_join 1 failed!\n");        
        }    

        //等待线程2结束
        ret = pthread_join(t2,NULL);
        if(ret != 0)
        {
            printf("pthread_join 2 failed!\n");
        }

        return 0;
    }

    编译后运行,结果如下:

     

    展开全文
  • Linux多线程与同步

    千次阅读 2016-04-13 22:51:46
    Linux多线程与同步   典型的UNIX系统都支持一个进程创建多个线程(thread)。在LINUX基础中提到,Linux以进程为单位组织操作,Linux中的线程也都基于进程。尽管实现方式有异于其它的UNIX系统,但Linux...

    Linux多线程与同步


     

    典型的UNIX系统都支持一个进程创建多个线程(thread)。在LINUX基础中提到,Linux以进程为单位组织操作,Linux中的线程也都基于进程。尽管实现方式有异于其它的UNIX系统,但Linux的多线程在逻辑和使用上与真正的多线程并没有差别。

     

    多线程

    我们先来看一下什么是多线程。在Linux程序到进程中,我们看到了一个程序在内存中的表示。这个程序的整个运行过程中,只有一个控制权的存在。当函数被调用的时候,该函数获得控制权,成为激活(active)函数,然后运行该函数中的指令。与此同时,其它的函数处于离场状态,并不运行。如下图所示:


     

    我们看到,各个方块之间由箭头连接。各个函数就像是连在一根线上一样,计算机像一条流水线一样执行各个函数中定义的操作。这样的一个程序叫做单线程程序。


    多线程就是允许一个进程内存在多个控制权,以便让多个函数同时处于激活状态,从而让多个函数的操作同时运行。即使是单CPU的计算机,也可以通过不停地在不同线程的指令间切换,从而造成多线程同时运行的效果。如下图所示,就是一个多线程的流程:

    main()到func3()再到main()构成一个线程,此外func1()和func2()构成另外两个线程。操作系统一般都有一些系统调用来让你将一个函数运行成为一个新的线程。

     

    回忆我们在Linux程序到进程中提到的栈的功能和用途。一个栈,只有最下方的帧可被读写。相应的,也只有该帧对应的那个函数被激活,处于工作状态。为了实现多线程,我们必须绕开栈的限制。为此,创建一个新的线程时,我们为这个线程建一个新的栈。每个栈对应一个线程。当某个栈执行到全部弹出时,对应线程完成任务,并收工。所以,多线程的进程在内存中有多个栈。多个栈之间以一定的空白区域隔开,以备栈的增长。每个线程可调用自己栈最下方的帧中的参数和变量,并与其它线程共享内存中的Text,heap和global data区域。对应上面的例子,我们的进程空间中需要有3个栈。

    (要注意的是,对于多线程来说,由于同一个进程空间中存在多个栈,任何一个空白区域被填满都会导致stack overflow的问题。)

     

    并发

    多线程相当于一个并发(concunrrency)系统。并发系统一般同时执行多个任务。如果多个任务可以共享资源,特别是同时写入某个变量的时候,就需要解决同步的问题。比如说,我们有一个多线程火车售票系统,用全局变量i存储剩余的票数。多个线程不断地卖票(i = i - 1),直到剩余票数为0。所以每个都需要执行如下操作:

    复制代码
    /*mu is a global mutex*/

    while
    (1) {    /*infinite loop*/ if (i != 0) i = i -1 else { printf("no more tickets"); exit(); } }
    复制代码

    如果只有一个线程执行上面的程序的时候(相当于一个窗口售票),则没有问题。但如果多个线程都执行上面的程序(相当于多个窗口售票), 我们就会出现问题。我们会看到,其根本原因在于同时发生的各个线程都可以对i读取和写入。

    我们这里的if结构会给CPU两个指令, 一个是判断是否有剩余的票(i != 0), 一个是卖票 (i = i -1)。某个线程会先判断是否有票(比如说此时i为1),但两个指令之间存在一个时间窗口,其它线程可能在此时间窗口内执行卖票操作(i = i -1),导致该线程卖票的条件不再成立。但该线程由于已经执行过了判断指令,所以无从知道i发生了变化,所以继续执行卖票指令,以至于卖出不存在的票 (i成为负数)。对于一个真实的售票系统来说,这将成为一个严重的错误 (售出了过多的票,火车爆满)。

    在并发情况下,指令执行的先后顺序由内核决定。同一个线程内部,指令按照先后顺序执行,但不同线程之间的指令很难说清除哪一个会先执行。如果运行的结果依赖于不同线程执行的先后的话,那么就会造成竞争条件(race condition),在这样的状况下,计算机的结果很难预知。我们应该尽量避免竞争条件的形成。最常见的解决竞争条件的方法是将原先分离的两个指令构成不可分隔的一个原子操作(atomic operation),而其它任务不能插入到原子操作中。

     

    多线程同步

    对于多线程程序来说,同步(synchronization)是指在一定的时间内只允许某一个线程访问某个资源 。而在此时间内,不允许其它的线程访问该资源。我们可以通过互斥锁(mutex)条件变量(condition variable)读写锁(reader-writer lock)来同步资源。

     

    1) 互斥锁

    互斥锁是一个特殊的变量,它有锁上(lock)和打开(unlock)两个状态。互斥锁一般被设置成全局变量。打开的互斥锁可以由某个线程获得。一旦获得,这个互斥锁会锁上,此后只有该线程有权打开。其它想要获得互斥锁的线程,会等待直到互斥锁再次打开的时候。我们可以将互斥锁想像成为一个只能容纳一个人的洗手间,当某个人进入洗手间的时候,可以从里面将洗手间锁上。其它人只能在互斥锁外面等待那个人出来,才能进去。在外面等候的人并没有排队,谁先看到洗手间空了,就可以首先冲进去。

    上面的问题很容易使用互斥锁的问题解决,每个线程的程序可以改为:

    复制代码
    /*mu is a global mutex*/

    while (1) { /*infinite loop*/ mutex_lock(mu);       /*aquire mutex and lock it, if cannot, wait until mutex is unblocked*/ if (i != 0) i = i - 1; else { printf("no more tickets"); exit(); } mutex_unlock(mu);     /*release mutex, make it unblocked*/ }
    复制代码

    第一个执行mutex_lock()的线程会先获得mu。其它想要获得mu的线程必须等待,直到第一个线程执行到mutex_unlock()释放mu,才可以获得mu,并继续执行线程。所以线程在mutex_lock()和mutex_unlock()之间的操作时,不会被其它线程影响,就构成了一个原子操作

    需要注意的时候,如果存在某个线程依然使用原先的程序 (即不尝试获得mu,而直接修改i),互斥锁不能阻止该程序修改i,互斥锁就失去了保护资源的意义。所以,互斥锁机制需要程序员自己来写出完善的程序来实现互斥锁的功能。我们下面讲的其它机制也是如此。

     

    2) 条件变量

    条件变量是另一种常用的变量。它也常常被保存为全局变量,并和互斥锁合作。

     

    假设这样一个状况: 有100个工人,每人负责装修一个房间。当有10个房间装修完成的时候,老板就通知相应的十个工人一起去喝啤酒。

    我们如何实现呢?老板让工人在装修好房间之后,去检查已经装修好的房间数。但多线程条件下,会有竞争条件的危险。也就是说,其他工人有可能会在该工人装修好房子和检查之间完成工作。采用下面方式解决:

    复制代码
    /*mu: global mutex, cond: global codition variable, num: global int*/
    mutex_lock(mu) num
    = num + 1; /*worker build the room*/ if (num <= 10) { /*worker is within the first 10 to finish*/ cond_wait(mu, cond);     /*wait*/ printf("drink beer"); } else if (num = 11) { /*workder is the 11th to finish*/ cond_broadcast(mu, cond);        /*inform the other 9 to wake up*/ } mutex_unlock(mu);
    复制代码

    上面使用了条件变量。条件变量除了要和互斥锁配合之外,还需要和另一个全局变量配合(这里的num, 也就是装修好的房间数)。这个全局变量用来构成各个条件。

     

    具体思路如下。我们让工人在装修好房间(num = num + 1)之后,去检查已经装修好的房间数( num < 10 )。由于mu被锁上,所以不会有其他工人在此期间装修房间(改变num的值)。如果该工人是前十个完成的人,那么我们就调用cond_wait()函数。
    cond_wait()做两件事情,一个是释放mu,从而让别的工人可以建房。另一个是等待,直到cond的通知。这样的话,符合条件的线程就开始等待。

    当有通知(第十个房间已经修建好)到达的时候,condwait()会再次锁上mu。线程的恢复运行,执行下一句prinft("drink beer") (喝啤酒!)。从这里开始,直到mutex_unlock(),就构成了另一个互斥锁结构。

    那么,前面十个调用cond_wait()的线程如何得到的通知呢?我们注意到elif if,即修建好第11个房间的人,负责调用cond_broadcast()。这个函数会给所有调用cond_wait()的线程放送通知,以便让那些线程恢复运行。

     

    条件变量特别适用于多个线程等待某个条件的发生。如果不使用条件变量,那么每个线程就需要不断尝试获得互斥锁并检查条件是否发生,这样大大浪费了系统的资源。

     

    3) 读写锁

    读写锁与互斥锁非常相似。r、RW lock有三种状态: 共享读取锁(shared-read)互斥写入锁(exclusive-write lock), 打开(unlock)。后两种状态与之前的互斥锁两种状态完全相同。

    一个unlock的RW lock可以被某个线程获取R锁或者W锁。

    如果被一个线程获得R锁,RW lock可以被其它线程继续获得R锁,而不必等待该线程释放R锁。但是,如果此时有其它线程想要获得W锁,它必须等到所有持有共享读取锁的线程释放掉各自的R锁。

    如果一个锁被一个线程获得W锁,那么其它线程,无论是想要获取R锁还是W锁,都必须等待该线程释放W锁。

    这样,多个线程就可以同时读取共享资源。而具有危险性的写入操作则得到了互斥锁的保护。

     

    我们需要同步并发系统,这为程序员编程带来了难度。但是多线程系统可以很好的解决许多IO瓶颈的问题。比如我们监听网络端口。如果我们只有一个线程,那么我们必须监听,接收请求,处理,回复,再监听。如果我们使用多线程系统,则可以让多个线程监听。当我们的某个线程进行处理的时候,我们还可以有其他的线程继续监听,这样,就大大提高了系统的利用率。在数据越来越大,服务器读写操作越来越多的今天,这具有相当的意义。多线程还可以更有效地利用多CPU的环境。

    (就像做饭一样,不断切换去处理不同的菜。)

     

    本文中的程序采用伪C的写法。不同的语言有不同的函数名(比如mutex_lock)。这里关注的是逻辑上的概念,而不是具体的实现和语言规范。

     

    总结

    multiple threads, multiple stacks

    race condition

    mutex, condition variable, RW lock

    展开全文
  • linux多线程编程

    千次阅读 2011-09-28 19:42:01
    Linux 平台上的多线程程序开发相对应其他平台(比如 Windows)的多线程 API 有一些细微和隐晦的差别。不注意这些 Linux 上的一些开发陷阱,常常会导致程序问题不穷,死锁不断。本文中我们从 5 个方面总结出 Linux ...
  • LINUX 多线程调试方法

    万次阅读 2018-04-02 16:50:36
    LINUX环境下的 多线程调试方法与总结 作为开发者的天堂,Linux为程序员提供了极其便利的方法和技巧,同时随着程序规模的增加,线程之间的绕来绕去,程序调试变得极其不稳定,因此,如何判断程序的问题出在哪里变得...
  • Linux多线程下载工具Axel

    千次阅读 2018-05-13 11:44:42
    Linux 下我尝试了很多的下载工具,例如wget,you-get,wget不支持多线程和断点续传,you-get功能很强大可下载众多视频,但是有的链接是不能够下载的(例如百度云直链) 今天在这里给大家介绍的 Axel,是 Linux ...
  • linux 多线程 信号

    千次阅读 2013-10-28 15:03:17
    在开发linux mjpg-streamer程序的时候,使用signal,奇怪的是程序竟然退出了。后来读了曹老师的文章,才解决了这个问题。 所以收录到自己的博客里。...多线程中定时器的使用41865100619 2010-10-14 19:47:29 ——
  • linux 多线程信号总结(一) 1. 在多线程环境下,产生的信号是传递给整个进程的,一般而言,所有线程都有机会收到这个信号,进程在收到信号的的线程上下文执行信号处理函数,具体是哪个线程执行的难以获知。也就是说...
  • linux多线程编程学习心得

    千次阅读 2010-10-26 15:08:00
    网上有一篇《Linux下的多线程编程》介绍的比较详细,细读了一遍,颇有收获!linux下的多线程模型:可用getconf -a | grep GNU_LIBPTHREAD_VERSION查看,我的机器用的是redhat公司研发的NPTL 2.3.4。pthread实现的...
  • linux 多线程信号总结

    千次阅读 2013-01-10 11:05:16
    1. 在多线程环境下,产生的信号是传递给整个进程的,一般而言,所有线程都有机会收到这个信号,进程在收到信号的的线程上下文执行信号处理函数,具体是哪个线程执行的难以获知。  2 signal函数BSD/Linux的实现并...
  • Linux 多线程编程(三)

    千次阅读 2018-06-01 18:03:34
    1 线程安全多线程编程环境中,多个线程同时调用某些函数可能会产生错误结果,这些函数称为非线程安全函数。如果库函数能够在多个线程中同时执行并且不会互相干扰,那么这个库函数就是线程安全( thread-safe)函数 ...
  • Linux 多线程内存占用分析

    千次阅读 2014-08-11 22:39:11
    在一次偶然的应程序开发过程中,发现一个很奇怪的问题。大概现象为,一个很简单的程序逻辑,开启了几个线程,程序本身并没有过多的申请内存,或者说根本没有申请内存,但是在实际运行起来后,通过PS命令和status ...
  • linux多线程之pthread_cancel结束线程

    千次阅读 2012-07-10 14:14:43
    这篇文章主要从一个 Linux 下一个 pthread_cancel 函数引起的多线程死锁小例子出发来说明 Linux 系统对 POSIX 线程取消点的实现方式,以及如何避免因此产生的线程死锁。 目录: 1. 一个 pthread_cancel 引起的线程...
  • Linux多线程同步机制

    万次阅读 2011-09-10 21:18:00
    尽管在Posix Thread中同样可以使用IPC的信号量机制来实现互斥锁mutex功能,但显然semphore的功能过于强大了,在Posix Thread中定义了另外一套专门用于线程同步的mutex函数。 1. 创建和销毁  有两种方法创建互斥...
  • Linux多线程调度策略

    千次阅读 2018-07-09 11:01:22
    转自:http://blog.csdn.net/byperseverance/article/details/44522731 Linux线程的调度策略分为3个:SCHED_OTHER,SCHED_FIFO,SCHED_RR 讲策略之前,大家需要理解实时与非实时之分。实时就是指操作系统对一些...
  • Linux多线程实践(1) --线程理论

    千次阅读 2015-02-19 16:20:42
    线程概念 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列/指令序列”;  一切进程至少有一个执行线程;进程 VS. 线程   1.进程是资源分配(进程需要参与资源...
  • 线程概念  在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列/指令序列”;  对于每个进程至少有一个执行线程; 进程 VS. 线程   1.进程是...
  • 本文分析linux多线程调试的实现机制和代码框架,给出基于jtag调试方式的ecos多任务调试的代码框架和实现机制的设计。
  • Linux多线程编程小结——干货

    千次阅读 2015-08-22 22:01:05
    对于linux下的多线程而言,这里我们需要区分几个概念: 1、信号量 2、互斥变量(递归和非递归) 3、条件变量 4、共享锁(读写锁)(适用于读的次数远大于写的情况)   信号量(sem)相当于是操作系统中PV操作...
  • linux 多线程并发服务器(TCP)

    千次阅读 2018-08-27 09:24:10
    linux 多线程并发服务器(TCP) ​ 所谓多线程并发服务器就是基于线程,每个客户端来了创建一个线程,由线程去处理客户端的请求。相对于多线程服务器来说,多进程服务器在创建进程时要消耗较大的系统资源,所以我们...
  • linux多线程环境下对同一变量进行读写时,经常会遇到读写的原子性问题,即会出现竞争条件。为了解决多个线程对同一变量访问时的竞争条件问题,操作系统层面提供了锁、信号量、条件变量等几种线程同步机制。如果对...
  • 我现在有个困惑:大致就是在一个TCP服务器中创建两个线程,一个接收消息并进行相应的处理,一个发送消息并进行相应的处理。 现在的情况就是我如果在两个线程之间加上usleep(100)的话...就单单这过程有什么门道吗?()
  • linux多线程编程是指基于Linux操作系统下的多线程编程,包括多任务程序的设计,并发程序设计,网络程序设计,数据共享等。Linux系统下的多线程遵循POSIX线程接口,称为pthread。编写Linux下的多线程程序,需要使用...
  • Linux 多线程编程线程分类线程按照其调度者可以分为用户级线程和内核级线程两种。内核级线程在一个系统上实现线程模型的方式有好几种,因内核和用户空间提供的支持而有一定程度的级别差异。最简单的模型是在内核为...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 228,368
精华内容 91,347
关键字:

linux多线程过程

linux 订阅