tags:

views:

84

answers:

2

When I use pthread in cocoa, and want to access cocoa control in pthread function(setBtnState), it doesn't work. What's the problem?

The following is the source code:

AppController.h

 1  //
 2  //  AppController.h
 3  //  PThreadTest
 4  //
 5  //  Created by zhu on 10-9-5.
 6  //  Copyright 2010 __MyCompanyName__. All rights reserved.
 7  //
 8  
 9  #import <Cocoa/Cocoa.h>
10  
11  
12  @interface AppController : NSObject {
13      IBOutlet NSButton *btnNew;
14      IBOutlet NSButton *btnEnd;
15  }
16  
17  -(IBAction)newThread:(id)sender;
18  -(IBAction)endThread:(id)sender;
19  
20  @end

AppController.m

 1  //
 2  //  AppController.m
 3  //  PThreadTest
 4  //
 5  //  Created by zhu on 10-9-5.
 6  //  Copyright 2010 __MyCompanyName__. All rights reserved.
 7  //
 8  
 9  #import "AppController.h"
10  #import <pthread.h>
11  
12  
13  @implementation AppController
14  
15  struct mydata {
16      pthread_mutex_t mutex;
17      pthread_cond_t cond;
18      int stop;
19      NSButton *btnNew;
20      NSButton *btnEnd;
21  };
22  
23  struct mydata adata;
24  struct mydata *ptr;
25  
26  void setBtnState(struct mydata *p) {
27      BOOL stop = NO;
28      if (p->stop) {
29          stop = YES;
30      }
31      [p->btnNew setEnabled:stop];
32      [p->btnEnd setEnabled:!stop];
33  }
34  
35  void* mythread(void* arg) {
36      NSLog(@"new thread start...");
37      ptr->stop = 0;
38      setBtnState(ptr);
39      pthread_mutex_lock(&ptr->mutex);
40      while (!ptr->stop) {
41          pthread_cond_wait(&ptr->cond, &ptr->mutex);
42      }
43      pthread_mutex_unlock(&ptr->mutex);
44      setBtnState(ptr);
45      NSLog(@"current thread end...");
46  }
47  
48  -(id)init {
49      self = [super init];
50      ptr = &adata;
51      pthread_mutex_init(&ptr->mutex, NULL);
52      pthread_cond_init(&ptr->cond, NULL);
53      ptr->stop = 0;
54      ptr->btnNew = btnNew;
55      ptr->btnEnd = btnEnd;
56      return self;
57  }
58  
59  -(IBAction)newThread:(id)sender {
60      pthread_t pid;
61      pthread_create(&pid, NULL, mythread, NULL);
62  }
63  
64  -(IBAction)endThread:(id)sender {
65      pthread_mutex_lock(&ptr->mutex);
66      ptr->stop = 1;
67      pthread_mutex_unlock(&ptr->mutex);
68      pthread_cond_signal(&ptr->cond);
69  }
70  
71  @end

Thanks to Chris. In the backgroud thread in order to update control's state I use performSelectorOnMainThread to communicate with the main UI thread.

But when btnEnd is pressed, the debugger console show the following infomation:

2010-09-12 23:36:29.255 PThreadTest[1888:a0f] -[AppController setBtnState]: unrecognized selector sent to instance 0x100133030

Why it doesn't work after I updated AppController.m as the following:

 1  //
 2  //  AppController.m
 3  //  PThreadTest
 4  //
 5  //  Created by zhu on 10-9-5.
 6  //  Copyright 2010 __MyCompanyName__. All rights reserved.
 7  //
 8  
 9  #import "AppController.h"
10  #import <pthread.h>
11  
12  
13  @implementation AppController
14  
15  struct mydata {
16      pthread_mutex_t mutex;
17      pthread_cond_t cond;
18      int stop;
19      NSButton *btnNew;
20      NSButton *btnEnd;
21      id obj;
22  };
23  
24  struct mydata adata;
25  struct mydata *ptr;
26  
27  void* mythread(void* arg) {
28      NSLog(@"new thread start...");
29      ptr->stop = 0;
30      pthread_mutex_lock(&ptr->mutex);
31      while (!ptr->stop) {
32          pthread_cond_wait(&ptr->cond, &ptr->mutex);
33      }
34      pthread_mutex_unlock(&ptr->mutex);
35      [ptr->obj performSelectorOnMainThread:@selector(setBtnState) withObject:@"YES" waitUntilDone:NO];
36      NSLog(@"current thread end...");
37  }
38  
39  -(void)setBtnState:(id)aobj {
40      BOOL stop = NO;
41      if ([aobj isEqualToString:@"YES"]) {
42          stop = YES;
43      }
44      [btnNew setEnabled:stop];
45      [btnEnd setEnabled:!stop];
46  }
47  
48  -(id)init {
49      self = [super init];
50      ptr = &adata;
51      pthread_mutex_init(&ptr->mutex, NULL);
52      pthread_cond_init(&ptr->cond, NULL);
53      ptr->stop = 0;
54      ptr->obj = self;
55      //  ptr->btnNew = btnNew;
56      //  ptr->btnEnd = btnEnd;
57      return self;
58  }
59  
60  - (void)awakeFromNib {
61      ptr->btnNew = btnNew;
62      ptr->btnEnd = btnEnd;
63  }
64  
65  -(IBAction)newThread:(id)sender {
66      [self setBtnState:@"NO"];
67      pthread_t pid;
68      pthread_create(&pid, NULL, mythread, NULL);
69  }
70  
71  -(IBAction)endThread:(id)sender {
72      pthread_mutex_lock(&ptr->mutex);
73      ptr->stop = 1;
74      pthread_mutex_unlock(&ptr->mutex);
75      pthread_cond_signal(&ptr->cond);
76  }
77  
78  @end
79  
A: 

You want to move:

ptr->btnNew = btnNew;
ptr->btnEnd = btnEnd;

... to awakeFromNib if you are using outlets and loading from a nib file. There is no guarantee that the outlets will be resolved until awakeFromNib is called and init is called before awakeFromNib.

I'm not certain you can use pthreads to communicate with the UI. I'm pretty sure you have to use NSThread and notifications to talk to controls on the main thread.

TechZen
Thank you very much, and your advice of NSThread. When I move it to awakeFromNib, it readlly work! And I debug it, init is called before awakeFromNib, this is the point.
py_zhu
While using -awakeFromNib is the way to guarantee that btnNew and btnEnd are non-nil, interacting with them from a non-main-thread is bad news and will probably still have issues.
Chris Hanson
+2  A: 

You should only interact with your UI from the main thread, not from background threads.

This isn't just a matter of locking around interaction with your own controls; your controls may interact with other objects in the UI behind your back. (For example, your button may interact with your window.) This can lead to race conditions, deadlocks, invalid/mixed state, and other concurrency problems.

When you're doing some processing work on a background thread, and it needs to communicate results (whether intermediate or final) to the user, it will need to push that communication through the main thread. There are a few mechanisms in Cocoa with which to do this:

  1. The -performSelectorOnMainThread:withObject:waitUntilDone: method lets you say "run this other method on the main thread," optionally waiting until it's done. You should almost never pass YES for the waitUntilDone: argument though, as that's a recipe for deadlock.

  2. Starting in Mac OS X 10.6 and iOS 4.0, there is +[NSOperationQueue mainQueue], which returns an instance of NSOperationQueue associated with the main thread: You can place operations on this queue and they will run on the main thread.

    This is useful if you're using several operations on a background queue and you have some "finishing" operation to perform on the main thread that depends on all of them. You can just use NSOperation's dependency mechanism to set up dependencies between them even though they're on different queues.

    You can either subclass NSOperation or use Objective-C blocks for the bodies of your operations (via +[NSBlockOperation blockOperationWithBock:]).

  3. Also starting in Mac OS X 10.6 and iOS 4.0, there is Grand Central Dispatch which lets you perform a block on a main queue associated with the main thread. It's a slightly less verbose API than NSOperation, but at the cost of not having direct support for dependencies and some of the other features of NSOperationQueue, and of being built in plain C rather than in Objective-C.

One thing to remember when using any of these mechanisms is that you must not interact with the same data from multiple threads at the same time without appropriate concurrency control (such as locking, or the use of specialized lock-free data structures and primitives). You can't get away with "Oh, I'm just reading, so I don't need to take a lock," or "Oh, I just need to enable a button, I don't really need to push that to the main thread."

A good way to avoid this issue is to do as much work as possible by batching it up into discrete units, processing those units in the background, and then relaying the results to the main thread.

So your threaded code is not written like this:

  • spin off a thread to:
    • lock the document
    • get some data from the document
    • unlock the document
    • process the data
    • tell the main thread whether to enable or disable the document's Foo button

Instead your threaded code is written like this:

  • get some data from the document
  • spin off a thread to:
    • process the data
    • then tell the main thread the result of the processing
  • on the main thread:
    • determine from the result of the processing whether to enable or disable the document's Foo button

The difference is that the latter is written in terms of the units of work being done, rather than in terms of the user interface, and will be both easier to understand and more robust in the face of your application's life (such as adding features) - it's essentially "MVC applied to threads."

Chris Hanson
Chris, moderators are janitors, not professors. Besides, we don't have the ability to change the accepted answer of a question. If you believe the selected answer is wrong, press your case here. Also, could you have a seat over here?
Will
Thanks to Chris. Does that mean cocoa or it's controls is not thread safed. But if I want to changed state of my controls in another thread(not main UI thread), like enable one special Button after a large calculation in a new thread, how can I do this in objc or cocoa? In swing, in order to do this we can use SwingUtilities.invokeLater(Runnable doRun).
py_zhu
"Thread-safe" is too simplistic an adjective. There is documentation about what is and is not safe to do from a non-main thread in Cocoa; if something isn't listed as safe, you should generally assume it is unsafe. I'll update my answer to include some information on having the main thread do something on a background thread's behalf.
Chris Hanson
I updated my answer to include ways to pass information to the main thread so the work can be done there, and how to leverage that best in application design.
Chris Hanson
Thank you. Change the state of cocoa contols on backgroud thread is totally a stupid way in multi-threading application. And I updated my code, why it doesn't work?
py_zhu