先决条件: C语言中的多线程
线程同步 定义为一种机制,确保两个或多个并发进程或线程不会同时执行某些特定的程序段,称为临界段。进程对关键部分的访问是通过使用同步技术来控制的。当一个线程开始执行 临界截面 (程序的序列化段)另一个线程应该等到第一个线程完成。如果不采用适当的同步技术,可能会导致 竞赛条件 变量的值可能是不可预测的,并且取决于进程或线程的上下文切换的计时。
线程同步问题 研究同步问题的示例代码:
#include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> pthread_t tid[2]; int counter; void * trythis( void * arg) { unsigned long i = 0; counter += 1; printf ( " Job %d has started" , counter); for (i = 0; i < (0xFFFFFFFF); i++) ; printf ( " Job %d has finished" , counter); return NULL; } int main( void ) { int i = 0; int error; while (i < 2) { error = pthread_create(&(tid[i]), NULL, &trythis, NULL); if (error != 0) printf ( "Thread can't be created : [%s]" , strerror (error)); i++; } pthread_join(tid[0], NULL); pthread_join(tid[1], NULL); return 0; } |
如何编译上述程序? 要使用gcc编译多线程程序,我们需要将其与pthreads库链接。以下是用于编译程序的命令。
gfg@ubuntu:~/$ gcc filename.c -lpthread
在本例中,创建了两个线程(作业),在这些线程的start函数中,维护了一个计数器,以获取关于作业编号的日志,该作业编号已启动,以及何时完成。
输出:
Job 1 has started Job 2 has started Job 2 has finished Job 2 has finished
问题: 从最后两个日志可以看出,日志 作业2已经完成 “在没有日志的情况下重复两次” 作业1已完成 “我看见了。
为什么会这样? 通过仔细观察和可视化代码的执行,我们可以看到:
- 原木 工作2已经开始了 “刚刚打印出来” 工作1已经开始了 ‘因此很容易得出结论,当线程1正在处理时,调度程序调度了线程2。
- 如果我们假设上述假设成立,那么 柜台 ‘变量在作业1完成之前再次递增。
- 所以,当作业1实际完成时,计数器的错误值产生了日志 作业2已经完成 “然后是” 作业2已经完成 ‘用于实际作业2,反之亦然,因为它依赖于调度程序。
- 所以我们发现问题不在于重复日志,而在于“计数器”变量的错误值。
- 实际问题是,当第一个线程正在使用或即将使用变量“counter”时,第二个线程使用了该变量。
- 换句话说,我们可以说,在使用共享资源“计数器”时,线程之间缺乏同步导致了问题,或者一句话,我们可以说这个问题是由于两个线程之间的“同步问题”造成的。
如何解决?
实现线程同步最常用的方法是使用 互斥量 .
互斥
- 互斥锁是我们在使用共享资源之前设置的锁,在使用后释放。
- 设置锁后,其他线程无法访问代码的锁定区域。
- 所以我们看到,即使线程2被调度,而线程1没有访问共享资源,并且代码被线程1使用互斥锁锁定,那么线程2甚至无法访问该代码区域。
- 因此,这确保了代码中共享资源的同步访问。
互斥操作
- 假设一个线程使用互斥锁锁定了一个代码区域,并正在执行该代码段。
- 现在,如果调度器决定进行上下文切换,那么准备执行同一区域的所有其他线程都将被解锁。
- 所有线程中只有一个线程可以执行,但如果该线程尝试执行已锁定的同一代码区域,那么它将再次进入睡眠状态。
- 上下文切换将一次又一次地进行,但在释放代码上的互斥锁之前,任何线程都无法执行代码的锁定区域。
- 互斥锁将仅由锁定它的线程释放。
- 因此,这确保了一旦一个线程锁定了一段代码,那么其他线程就不能执行相同的区域,直到锁定它的线程解锁它。
因此,该系统在处理共享资源时确保线程之间的同步。
互斥锁被初始化,然后通过调用以下两个函数实现锁定: 第一个函数初始化互斥锁,通过第二个函数可以锁定代码中的任何关键区域。
- int pthread_mutex_init(pthread_mutex_t*restrict mutex,const pthread_mutextatr_t*restrict attr): 创建一个由互斥体引用的互斥体,其属性由attr指定。如果attr为NULL,则使用默认的互斥属性(非递归)。
返回值 如果成功,pthread_mutex_init()将返回0,互斥锁的状态将被初始化并解锁。 如果失败,pthread_mutex_init()将返回-1。
- int pthread_mutex_lock(pthread_mutex_t*mutex): 锁定一个互斥对象,该对象标识互斥对象。如果互斥锁已被另一个线程锁定,该线程将等待互斥锁可用。锁定互斥锁的线程将成为其当前所有者,并在同一线程解锁互斥锁之前一直保持所有者身份。当互斥锁具有递归属性时,锁的使用可能会有所不同。当这种互斥锁被同一个线程多次锁定时,计数就会增加,不会发布等待的线程。拥有线程的线程必须调用相同次数的pthread_mutex_unlock(),以将计数减为零。
返回值 如果成功,pthread_mutex_lock()将返回0。 如果失败,pthread_mutex_lock()将返回-1。
通过调用以下两个函数,可以解锁和销毁互斥锁: 第一个函数释放锁,第二个函数销毁锁,以便将来不能在任何地方使用。
- int pthread_mutex_unlock(pthread_mutex_t*mutex): 释放互斥对象。如果一个或多个线程正在等待锁定互斥对象,pthread_mutex_unlock()会导致其中一个线程从pthread_mutex_lock()返回并获取互斥对象。如果没有线程在等待互斥锁,互斥锁将在没有当前所有者的情况下解锁。当互斥锁具有递归属性时,锁的使用可能会有所不同。当这种互斥锁被同一个线程多次锁定时,unlock将减少计数,并且不会发布等待线程继续使用锁运行。如果计数减为零,则释放互斥锁,如果有线程正在等待,则发布互斥锁。
返回值 如果成功,pthread_mutex_unlock()将返回0。 如果失败,pthread_mutex_unlock()将返回-1
- int pthread_mutex_destroy(pthread_mutex_t*mutex): 删除标识互斥对象的互斥对象。互斥锁用于保护共享资源。互斥被设置为无效值,但可以使用pthread_mutex_init()重新初始化。
返回值 如果成功,pthread_mutex_destroy()将返回0。 如果失败,pthread_mutex_destroy()将返回-1。
一个演示如何使用互斥锁进行线程同步的示例
#include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> pthread_t tid[2]; int counter; pthread_mutex_t lock; void * trythis( void * arg) { pthread_mutex_lock(&lock); unsigned long i = 0; counter += 1; printf ( " Job %d has started" , counter); for (i = 0; i < (0xFFFFFFFF); i++) ; printf ( " Job %d has finished" , counter); pthread_mutex_unlock(&lock); return NULL; } int main( void ) { int i = 0; int error; if (pthread_mutex_init(&lock, NULL) != 0) { printf ( " mutex init has failed" ); return 1; } while (i < 2) { error = pthread_create(&(tid[i]), NULL, &trythis, NULL); if (error != 0) printf ( "Thread can't be created :[%s]" , strerror (error)); i++; } pthread_join(tid[0], NULL); pthread_join(tid[1], NULL); pthread_mutex_destroy(&lock); return 0; } |
在上述代码中:
- 互斥锁在主函数的开头初始化。
- 在使用共享资源“计数器”时,相同的互斥锁被锁定在“trythis()”函数中。
- 在函数“trythis()”的末尾,相同的互斥锁被解锁。
- 在主函数的末尾,当两个线程都完成时,互斥锁被销毁。
输出:
Job 1 started Job 1 finished Job 2 started Job 2 finished
因此,这一次两个作业的开始和完成日志都存在。所以线程同步是通过使用互斥来实现的。
本文由 基什莱·维尔马 .如果你喜欢GeekSforgek,并想贡献自己的力量,你也可以使用 贡献极客。组织 或者把你的文章寄到contribute@geeksforgeeks.org.看到你的文章出现在Geeksforgeks主页上,并帮助其他极客。
如果您发现任何不正确的地方,或者您想分享有关上述主题的更多信息,请写下评论。