线程同步

线程同步

如果变量时只读的,多个线程同时读取该变量不会有一致性问题,但是,当一个线程可以修改的变量,其他线程也可以读取或者修改的时候,我们就需要对这些线程进行同步,确保它们在访问变量的存储内容时不会访问到无效的值。

每个进程中访问临界资源的那段程序称为临界区,临界资源是一次仅允许一个进程使用的共享资源。线程同步的核心在于,通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。在任意时刻只允许一个线程对共享资源进行访问,如果有多个线程试图访问公共资源,那么在有一个线程进入后,其他试图访问公共资源的线程将被挂起,并一直等到进入临界区的线程离开,临界区在被释放后,其他线程才可以抢占。

互斥与同步

互斥与同步是我们在讨论线程安全时常常会碰到的概念:

  • 互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。

  • 同步:主要是流程上的概念,是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源。

互斥锁、条件变量和信号量的区别: 互斥锁:互斥,一个线程占用了某个资源,那么其它的线程就无法访问,直到这个线程解锁,其它线程才可以访问。 条件变量:同步,一个线程完成了某一个动作就通过条件变量发送信号告诉别的线程,别的线程再进行某些动作。条件变量必须和互斥锁配合使用。 信号量:同步,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作。而且信号量有一个更加强大的功能,信号量可以用作为资源计数器,把信号量的值初始化为某个资源当前可用的数量,使用一个之后递减,归还一个之后递增。

另外还有以下几点需要注意: 1、信号量可以模拟条件变量,因为条件变量和互斥量配合使用,相当于信号量模拟条件变量和互斥量的组合。在生产者消费者线程池中,生产者生产数据后就会发送一个信号 pthread_cond_signal 通知消费者线程,消费者线程通过 pthread_cond_wait 等待到了信号就可以继续执行。这是用条件变量和互斥锁实现生产者消费者线程的同步,用信号量一样可以实现! 2、信号量可以模拟互斥量,因为互斥量只能为加锁或解锁(0 or 1),信号量值可以为非负整数,也就是说,一个互斥量只能用于一个资源的互斥访问,它不能实现多个资源的多线程互斥问题。信号量可以实现多个同类资源的多线程互斥和同步。当信号量为单值信号量时,就完成一个资源的互斥访问。前面说了,信号量主要用做多线程多任务之间的同步,而同步能够控制线程访问的流程,当信号量为单值时,必须有线程释放,其他线程才能获得,同一个时刻只有一个线程在运行(注意,这个运行不一定是访问资源,可能是计算)。如果线程是在访问资源,就相当于实现了对这个资源的互斥访问。 3、互斥锁是为上锁而优化的;条件变量是为等待而优化的;信号量既可用于上锁,也可用于等待,因此会有更多的开销和更高的复杂性。 4、互斥锁,条件变量都只用于同一个进程的各线程间,而信号量(有名信号量)可用于不同进程间的同步。当信号量用于进程间同步时,要求信号量建立在共享内存区。 5、互斥量必须由同一线程获取以及释放,信号量和条件变量则可以由一个线程释放,另一个线程得到。 6、信号量的递增和减少会被系统自动记住,系统内部的计数器实现信号量,不必担心丢失,而唤醒一个条件变量时,如果没有相应的线程在等待该条件变量,此次唤醒会被丢失。

同步原语

同步原语是一种软件机制,提供了两个或者多个并行进程或者线程在不同时刻执行一段相同的代码段的能力。例如下面的代码片段:

mutex_lock(&clocksource_mutex);
...
...
...
clocksource_enqueue(cs);
clocksource_enqueue_watchdog(cs);
clocksource_select();
...
...
...
mutex_unlock(&clocksource_mutex);

出自 kernel/time/clocksource.c 源文件。这段代码来自于 __clocksource_register_scale 函数,此函数添加给定的 clocksource 到时钟源列表中。这个函数在注册时钟源列表中生成两个不同的操作。例如 clocksource_enqueue 函数就是添加给定时钟源到注册时钟源列表——clocksource_list 中。注意这几行代码被两个函数所包围:mutex_lock 和 mutex_unlock。

这些函数展示了基于互斥锁 (mutex) 同步原语的加锁和解锁。当 mutex_lock 被执行,允许我们阻止两个或两个以上线程执行这段代码,而 mute_unlock 还没有被互斥锁的处理拥有者锁执行。换句话说,就是阻止在 clocksource_list 上的并行操作。

事实上,Linux 内核提供了一系列不同的同步原语:

spinlock; mutex; semaphores; seqlocks; atomic operations;

互斥量(Mutex),信号量(Semaphore),事件(Event)都可以被跨越进程使用来进行同步数据操作,而其他的对象与数据同步操作无关,但对于进程和线程来讲,如果进程和线程在运行状态则为无信号状态,在退出后为有信号状态。所以可以使用 WaitForSingleObject 来等待进程和线程退出。

临界资源与临界区

所谓的临界资源,即一次只允许一个进程访问的资源,多个进程只能互斥访问的资源。临界资源的访问需要同步操作,比如信号量就是一种方便有效的进程同步机制。但信号量的方式要求每个访问临界资源的进程都具有 wait 和 signal 操作。这样使大量的同步操作分散在各个进程中,不仅给系统管理带来了麻烦,而且会因同步操作的使用不当导致死锁。管程就是为了解决这样的问题而产生的。

操作系统中管理的各种软件和硬件资源,均可用数据结构抽象地描述其资源特性,即用少量信息和对该资源所执行的操作来表征该资源,而忽略它们的内部结构和实现细节。利用共享数据结构抽象地表示系统中的共享资源。而把对该共享数据结构实施的操作定义为一组过程,如资源的请求和释放过程 request 和 release。进程对共享资源的申请、释放和其他操作,都是通过这组过程对共享数据结构的操作来实现的,这组过程还可以根据资源的情况接受或阻塞进程的访问,确保每次仅有一个进程使用该共享资源,这样就可以统一管理对共享资源的所有访问,实现临界资源互斥访问。

管程就是代表共享资源的数据结构以及由对该共享数据结构实施操作的一组过程所组成的资源管理程序共同构成的一个操作系统的资源管理模块。管程被请求和释放临界资源的进程所调用。管程定义了一个数据结构和能为并发进程所执行(在该数据结构上)的一组操作,这组操作能同步进程和改变管程中的数据。

屏障(Barrier)

屏障是指用户可以协调多个线程并行工作的同步机制。屏障允许每个线程等待,直到所有的合作线程都到达某一点,然后从改点继续执行。pthread_join 函数就是一种屏障,它允许一个线程等待,直到另一个线程退出。屏障允许任意数量的线程等待,直到所有的线程完成处理工作,而线程不需要退出。所有线程达到屏障后可以接着工作。如果我们要让主线程在所有工作线程完成之后再做某项任务,一般把屏障计数值设为工作线程数加 1,主线程也作为其中一个候选线程。

屏障对象的概念更广,它们允许任意数量的线程等待,直到所有的线程完成处理工作,而线程不需要退出。所有线程达到屏障后可以接着工作。

可以使用 pthread_barrier_init 函数对屏障进行初始化,用 thread_barrier_destroy 函数反初始化。

#include <pthread. h>

//两个函数的返回值:若成功,返回0;否则,返回错误编码
int pthread_barrier_init(pthread_barrier_t *restrict barrier, const pthread_barrierattr_t *restrict attr, unsigned int count);
int pthread_barrier_destroy(pthread_barrier_t *barrier);

//返回值:若成功,返回0或者PTHREAD_BARRIER_SERIAL_THREAD;否则,返回错误编码
int pthread_barrier_wait(pthread_barrier_t *barrier);

初始化屏障时,可以使用 count 参数指定,在允许所有线程继续运行之前,必须到达屏障的线程数目。可以设置 attr 为 NULL,用默认属性初始化屏障。如果使用 pthread_barrier_init 函数为屏障分配资源,那么在反初始化屏障时可以调用 pthread_barrier_destroy 函数释放相应的资源。可以使用 pthread_barrier_wait 函数来表明,线程已完成工作,准备等所有其他线程赶上来。

调用 pthread_barrier_wait 的线程在屏障计数(调用 pthread_barrier_init 时设定)未满足条件时,会进入休眠状态。如果该线程是最后一个调用 pthread_barrier_wait 的线程,就满足了屏障计数,所有的线程都被唤醒。对于一个任意线程,pthread_barrier_wait 函数返回了 PTHREAD_BARRIER_SERIAL_THREAD。剩下的线程看到的返回值是 0。这使得一个线程可以作为主线程,它可以工作在其他所有线程已完成的工作结果上。一旦达到屏障计数值,而且线程处于非阻塞状态,屏障就可以被重用。除非调用了 pthread_barrier_destroy 函数之后,又调用了 pthread_barrier_init 函数对计数进行初始化,否则屏障计数不会改变。

在一个任务上合作的多个线程之间如何用屏障进行同步。

/*
程序功能:
创建一个动态数组,给数组赋初值,创建NUM_THREAD个线程,为长度为SIZE的数组初始化,通过(线程屏障同步)
当所有线程处理完后则打印整个数组的值。通过时间计数来比较所花费的时间。
当采用8个线程时花费sort took 0.0958 seconds
当采用四个线程时
    thread -1249961072 done job.
    thread -1260450928 done job.
    thread -1239471216 done job.
    thread -1270944880 done job.
    sort took 0.1104 seconds
当采用一个线程时
    thread -1239983216 done job.
    sort took 0.2567 seconds
*/
#include <pthread.h>
#include <stdlib.h>
#include <stdio.h>
#define SIZE 8000000L //数组长度
#define NUM_THREAD 8 //线程个数
#define SIZE_PER (SIZE/NUM_THREAD)//每个线程要处理的数组长度
pthread_barrier_t barrier;//定义屏障
int *a;
/*每个线程的线程处理函数*/
void * thr_fun(void *arg)
{
    long n = (long)arg;
    long i;
    for(i=n;i<n+SIZE_PER;i++) {
        a[i] = i;
    }
    printf("thread %d done job.\n",pthread_self());
    pthread_barrier_wait(&barrier); //阻塞等待直到主线程满足了屏障计数
    return ((void *)1);
}
int main()
{
    pthread_t tid;
    struct timeval start,end;
    long long startusec,endusec;
    double elapsed;
    int i;
    a = (int *)malloc(SIZE*sizeof(int)); //动态分配数组
    pthread_barrier_init(&barrier,NULL,NUM_THREAD+1);//初始化线程屏障计数为子线程个数加上主线程
    gettimeofday(&start,NULL);//获得起始时间
    for(i=0;i<NUM_THREAD;i++)
    {
        pthread_create(&tid,NULL,thr_fun,(void *)(i*SIZE_PER));//创建子线程
    }
    pthread_barrier_wait(&barrier);//等待所有子线程处理完成
    gettimeofday(&end,NULL);//获得结束时间
    for(i=0;i<SIZE;i++)//打印数组内容
    //printf("%d ",a[i]);
    startusec = start.tv_sec * 1000000 + start.tv_usec;
    endusec = end.tv_sec * 1000000 + end.tv_usec;
    elapsed = (double)(endusec-startusec)/1000000.0;//计算处理所花费的时间
    printf("sort took %.4f seconds\n",elapsed);
    return 0;
}
上一页