有关线程创建:【Linux 编程】线程编程
当多个控制线程共享相同的数据时,需要确保每一个线程看到一致的数据。当一个线程修改数据变量时,其他线程在读取这个变量值时就可能会看到不一致的数据。在变量修改时间多于一个存储访问的处理结构中,当存储器读取或存储器写这两个周期交叉时,潜在的数据不一致性就会出现。为了解决这种数据不一致性的问题,线程利用锁机制来保证在保证数据的一致性问题。若读取数据或写数据操作是原子操作,则不存在数据竞争或数据不一致性问题。
1. 互斥锁(pthread_mutex_t)
互斥锁保证同一时间只允许一个线程访问数据。任何其他试图获取已加锁的互斥锁,就会被阻塞直到当前线程释放该互斥锁。若有多个线程被阻塞,在互斥锁被释放后,将唤醒所有阻塞线程,并采取先到先服务的策略来分配互斥锁的使用权。
互斥锁的初始化:
- 使用常量PTHREAD_MUTEX_INITIALIZER来初始化静态分配的互斥量;
- 针对动态分配的互斥量,则调用函数pthread_mutex_init()进行初始化;调用函数pthread_mutex_destroy()来去初始化。需要注意,这两个函数成对出现,使用方式类似于malloc/free。
互斥锁的使用:
- 函数pthread_mutex_lock()用来对互斥锁加锁。若互斥锁已上锁,则当前线程将被阻塞;
- 若系统线程在不能获取互斥锁时被阻塞,则调用函数pthread_mutex_trylock()尝试对互斥锁进行加锁。若成功获取互斥锁,则锁住互斥锁并返回0;若失败,则返回非0,并设置错误码为EBUSY。
- 函数pthread_mutex_unlock()用来对互斥锁进行解锁。
若线程试图对同一个互斥锁加锁两次,那么它自身将会陷入死锁状态。在使用锁机制时,需要预先设计加锁的顺序来避免死锁的发生。
例子:
1 #include <unistd.h> 2 #include <pthread.h> 3 #include <stdio.h> 4 5 pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; 6 7 void *start_routine(void *arg) 8 { 9 int res; 10 int times = 0; 11 pthread_t thread_id = pthread_self(); 12 13 while (1) { 14 res = pthread_mutex_trylock(&lock); 15 if (res != 0) 16 { 17 ++times; 18 printf("Now the mutex lock is using!"); 19 printf("And thread ID 0x%x is trying %d times!\n", 20 (unsigned int) thread_id, times); 21 sleep(2); 22 continue; 23 } 24 25 printf("OK, 0x%x get mutex\n", (unsigned int) thread_id); 26 times = 0; 27 sleep(10); 28 pthread_mutex_unlock(&lock); 29 30 if (times == 0) 31 break; 32 } 33 34 pthread_exit((void *)thread_id); 35 } 36 37 int main() 38 { 39 pthread_t id0, id1; 40 int res; 41 42 res = pthread_create(&id0, NULL, start_routine, NULL); 43 if (res < 0) 44 { 45 perror("pthread_create error!"); 46 return (-1); 47 } 48 49 res = pthread_create(&id1, NULL, start_routine, NULL); 50 if (res < 0) 51 { 52 perror("thread_create error!"); 53 return (-1); 54 } 55 56 sleep(20); 57 printf("MutexDemo is over!\n"); 58 59 return 0; 60 61 }
运行结果:
在多线程设计过程中不仅需要考虑加锁的顺序,还需要考虑锁的粒度大小问题,即加锁中处理数据的过程。若粒度太粗,就会发生出现很多线程阻塞等待相同的锁,则导致并发性的性能改善微乎其微。如果锁的粒度太小,那么过多的锁开销会使系统性能受到影响,而且代码会变得更加复杂。在设计加锁时,需要在满足锁需求的情况下,在代码复杂性和优化性能找到好的平衡点。
2. 读写锁(pthread_rwlock_t)
不同于互斥锁一次只能有一个线程可以对其加锁。读写锁具有三种状态:读模式下加锁状态、写模式下加锁状态、不加锁状态。一次只有一个线程可以占有写模式的读写锁,但多个线程可以同时占有读模式的读写锁。因此,读写锁也可以叫做共享-独占锁。当读写锁以读模式锁住时,它是以共享模式锁住的;当它以写模式锁住时,它是以独占模式锁住的。
若读写锁以读模式锁住状态时,如果有另外的线程试图从写模式加锁,将阻塞随后的读模式锁请求。若多个线程共同以写模式加锁,处理方式与互斥锁处理方式相同。
读写锁的初始化使用pthread_rwlock_init()来完成,在释放它们底层内存之前需要调用函数pthead_rwlock_destroy()来去初始化。
读写锁的使用:
- 读模式加锁,pthread_rwlock_rdlock()和pthread_rwlock_tryrdlock();
- 写模式加锁,pthread_rwlock_wrlock()和pthread_rwlock_trywrlock();
- 解锁操作,pthread_rwlock_unlock()。
3.条件变量(pthread_cond_t)
条件变量与互斥锁一起使用的,允许线程以无竞争的方式等待特定的条件发生。线程在改变条件状态之前首先锁住互斥锁,其他线程在获取互斥锁之前不会察觉到这种改变,因此必须锁定互斥锁以后才能计算条件。
条件变量的初始化:
- 用常量PTHREAD_COND_INITIALIZER赋给静态分配的条件变量;
- 若条件变量是动态分配的,则利用pthread_cond_init()函数进行初始化;在释放底层内存空间之前,必须使用pthread_cond_destroy()函数对条件变量去除初始化。
条件变量使用函数pthread_cond_wait()等待条件为真。函数中的互斥锁对条件进行保护,调用者把锁住的互斥锁传给函数。函数把调用线程放在等待条件的线程列表中,然后对互斥锁解锁,这两个操作是原子操作。函数在返回时,将再次获得互斥锁的控制权。函数pthread_cond_timedwait()在满足pthread_cond_wait()函数的处理方式的基础上,加上超时处理。当在规定时间内条件不能满足,则生成一个错误码并返回。
有两个函数用于通知线程条件已满足。pthread_cond_signal()函数将唤醒等待该条件的某个线程,而pthread_cond_broadcast()函数将唤醒等待该条件的所有线程。
有关条件变量的编程例子:【Linux 编程】pthead_cond_t 的使用