这里的同步就是对程序的执行进行控制,因为如果不进行控制就会出现错误的问题,这里的控制是为了保证程序的正确性。
线程同步指的是当一个线程在对某个临界资源进行操作时,其他线程都不可以对这个资源进行操作,直到该线程完成操作, 其他线程才能操作,也就是协同步调,让线程按预定的先后次序进行运行。
线程同步的方法有四种:互斥锁、信号量、条件变量、读写锁。
代码如下:
运行结果:
第二次运行结果:
由结果可以看出,并不是每个线程每次输出的都是5000,而是会有低于5000的情况,这是因为可能会有两个线程在并行运行的时候同时去访问变量g_count,两个线程拿到的g_count的值是相同的,同时执行g_count++,这样就会导致两个线程并不会把g_count的值分别加两次,属于两个线程同时对g_count加1,相当于g_count只加了一次,就会使我们少一个数字。
使用信号量解决上面这一问题。
线程中使用的基本信号量函数有4个。
(1)sem_init()初始化信号量
参数解释:
第一个参数sem:信号量的变量。
第二个参数pshared:控制信号量的类型,如果其值为0,就表示整个信号量是当前进程的局部信号量,不让该信号量在多个进程之间共享。否则这个信号量就可以在多个进程之间共享。一般设置为0,不让信号量在进程间共享。
第三个参数value:无符号类型,设置信号量的初始值。
(2)sem_wait()P操作
参数解释:
参数sem:传所定义的信号量的地址。
该将对该信号量执行P(-1)操作。
(3)sem_post()V操作
参数解释:
参数sem:传所定义的信号量的地址。
该函数将对该信号量执行V(+1)操作。
(4)sem_destroy()销毁信号量
参数解释:
参数sem:传所定义的信号量的地址。
该函数将销毁所指向的信号量。
将上述示例中的代码修改之后如下:
运行结果:
此时,在每一个线程中都加入了信号量,在g_count++之前进行p操作获取资源,如果获取不到则说明另外四个线程中的其中一个正在使用信号量,使用完之后执行了v操作,这时当前这个线程才可以成功执行p操作,才可以执行g_count++操作,执行完g_count++操作之后再执行v操作释放资源,此时其他线程才可以使用信号量。这样就避免了两个或者多个线程同时获取g_count的值进行++操作,然后导致g_count的值只被加了一次。这样就会输出正确的g_count在5个线程中加了1000次之后的值5000。
互斥锁也称为互斥量,互斥锁是多线程程序中的同步访问方法的一种。互斥锁允许程序员锁住某一个对象,使得每次只能有一个线程访问它。为了控制对关键代码的访问,必须在进入这段代码之前设置一个互斥锁,然后在完成操作之后解锁它。
【注意】加锁会使程序的性能下降,但是可以保证程序的正确性。
(1)pthread_mutex_init()初始化互斥锁
参数解释:
第一个参数mutex:传入互斥锁变量的地址。
第二个参数mutexattr:互斥锁的属性,一般传NULL。
(2)pthread_mutex_lock()加锁
这个函数又可能发生阻塞,如果发现锁已经被加上了,这个时候就加锁不成功,就会发生阻塞。
参数解释:
参数mutex:传入所定义的互斥锁变量的地址。为该互斥锁加锁。
(3)pthread_mutex_unlock()解锁
参数解释:
参数mutex:传入所定义的互斥锁变量的地址。为该互斥锁解锁。
(4)pthread_mutex_destory()销毁锁
参数解释;
参数mutex:传入所定义的互斥锁变量的地址。销毁该互斥锁。
代码如下:
运行结果:
在这个程序中,互斥锁的使用和信号量一样,在每一个线程中都加入了互斥锁,在g_count++之前执行加锁操作,使每次只能有一个线程访问变量g_count,如果访问不到则说明已经被别的线程加锁,这时当前这个线程不可以对锁着的变量g_count进行++操作,会发生阻塞,只有等别的线程对变量g_count解锁之后,当前的线程才可以成功执行加锁操作,才可以执行g_count++操作,执行完g_count++操作之后再执行解锁操作,此时其他线程才可以继续对变量g_count进行加锁操作。这样就避免了两个或者多个线程同时获取g_count的值进行++操作,然后导致g_count的值只被加了一次。这样就会输出正确的g_count在5个线程中加了1000次之后的值5000。
但是有些场景,加锁并不是为了去修改数据,而是为了读取数据。比如说有一些线程要读取数据,有一些线程要修改数据,如果一个线程想读取,另外一个线程想修改,那么这两个线程肯定是不能同时执行的;如果两个线程都想修改数据,那么这两个线程也不可以同时执行;但是如果两个线程都想读取数据,这时这两个线程就可以同时执行。
而在程序中我们大量的需求可能只是读取数据,偶尔会修改数据。对于互斥锁来说,一旦加锁,就知道当前那个线程读取数据,别的线程都不可以读取数据,这样的话比较繁琐。如果使用读写锁,就可以在一定程度上把力度分的更细一点,这样的话读取数据的时候,多个线程可以同时执行,同时读取数据。如果要修改数据的话,就不可以,就会阻塞,必须要等别的线程读取完数据之后才可以修改数据。
(1)pthread_rwlock_init()初始化读写锁
(2)pthread_rwlock_rddlock()加读锁
(3)pthread_rwlock_wrlock()加写锁
(4)pthread_rwlock_unlock()解锁
无论哪个锁,都可以解
(5)pthread_rwlock_destroy()销毁锁