【Linux】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);
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值